This page documents Bun's package installation pipeline: how packages are resolved and installed from start to finish, how the binary lockfile (bun.lockb) and text lockfile (bun.lock) are structured and serialized, how the lockfile is loaded on subsequent installs, and how lifecycle scripts and patch installs integrate into the process.
For dependency version resolution and range matching specifically, see Dependency Resolution. For registry network communication and tarball download, see NPM Registry and Network Layer. For workspace and monorepo install behavior, see Workspace and Monorepo Support. For CLI flags such as bun install, bun add, and bun remove, see Install and Package Commands.
The top-level install function is installWithManager in src/install/PackageManager/install_with_manager.zig which is called after CLI argument parsing completes. It takes a *PackageManager, a Command.Context, the path to package.json, and the original working directory.
Installation Pipeline — installWithManager to lockfile save
Sources: src/install/PackageManager/install_with_manager.zig src/install/PackageManager/PackageManagerEnqueue.zig src/install/lockfile.zig218-400 src/install/install.zig193-229
| Phase | Code Location | Description |
|---|---|---|
| Lockfile load | Lockfile.loadFromDir | Opens bun.lock, bun.lockb, or migrates |
| Root parse | PackageManager init | Reads root package.json with Features.main |
| Resolution | enqueueDependencyWithMain | Enqueues dependencies; fetches npm manifests |
| Tarball extraction | ExtractTarball.run | Verifies integrity, extracts to cache |
| Lockfile compaction | Lockfile.cleanWithLogger | Rebuilds lockfile with only reachable packages |
| File install | PackageInstaller / Installer | Copies files into node_modules |
| Bin linking | Bin (bin.zig) | Creates symlinks in node_modules/.bin |
| Lifecycle scripts | LifecycleScriptSubprocess | Spawns pre/post-install scripts |
| Save | Lockfile.Serializer / TextLockfile.Stringifier | Writes lockfile to disk |
Each package being installed tracks its state via PreinstallState src/install/install.zig193-202:
Per-package PreinstallState transitions
Sources: src/install/install.zig193-202 src/install/patch_install.zig
Hoisted (default): Packages are installed into node_modules with maximum hoisting. The Lockfile.Tree structure in buffers.trees encodes which tree node each package belongs to; tree ID 0 is the root. PackageInstaller src/install/PackageInstaller.zig walks this tree.
Isolated (--linker=isolated): Packages are materialized once into a content-addressed virtual store (<cache>/links/). Each package in node_modules/.bun/<storepath> is a symlink into the store. This prevents phantom dependencies. Implemented in installIsolatedPackages src/install/isolated_install.zig using Installer src/install/isolated_install/Installer.zig and Store src/install/isolated_install/Store.zig
The global_store_path field in Installer src/install/isolated_install/Installer.zig28-29 holds the absolute path to the global virtual store directory.
File copy backends (controlled by PackageInstall.Method):
| Backend | macOS | Linux | Notes |
|---|---|---|---|
clonefile | default | — | CoW copy, fastest on APFS |
hardlink | fallback | default | Shares inode across cache/node_modules |
symlink | opt-in | opt-in | Symlinks entire package |
copyfile | fallback | fallback | Full byte copy |
Lockfile TypeLockfile is defined in src/install/lockfile.zig1-32 as a self-contained struct holding all package graph data.
Lockfile struct and key relationships
Sources: src/install/lockfile.zig1-100
PackageID = u32 (index into packages list; invalid = maxInt(u32))
DependencyID = u32 (index into buffers.dependencies)
PackageNameHash = u64 (bun.hash of package name)
Defined in src/install/install.zig95-111
Lockfile.Buffers stores all flat arrays indexed by PackageID / DependencyID:
dependencies — flat array of Dependency.External (all dependency edges from all packages)resolutions — parallel array of PackageID (resolved package for each dependency)trees — array of Tree nodes describing the hoisting structurestring_bytes — all string data; strings are (offset, length) pairs into this bufferextern_strings — ExternalString entries used for bin mapsThis struct-of-arrays layout is shared between the binary format on disk and the in-memory representation.
Bun supports two lockfile formats, selected automatically based on what is present on disk or configured in bunfig.toml.
| File | Format | Encoding | Version field |
|---|---|---|---|
bun.lockb | Binary | Custom binary, alignment-padded | FormatVersion enum |
bun.lock | Text | JSON (JSONC-compatible) | TextLockfile.Version enum |
The default file name constant is Lockfile.default_filename = "bun.lockb" src/install/lockfile.zig52
bun.lockb)The binary format is Bun-specific. It serializes the Lockfile struct's fields directly using Lockfile.Serializer.load / save. Alignment padding between typed sections is handled by Aligner src/install/install.zig113-126:
Padding bytes use alignment_bytes_to_repeat_buffer (144 zero bytes) src/install/install.zig49
The FormatVersion enum prevents loading lockfiles written by incompatible versions. The meta_hash field captures a hash of the resolved package graph; it is used to detect whether the lockfile is up-to-date.
bun.lock)The text lockfile is JSONC (JSON with comments and trailing commas allowed). It is parsed by JSON.parsePackageJSONUTF8 and then converted into the in-memory binary lockfile representation via TextLockfile.parseIntoBinaryLockfile src/install/lockfile.zig306
To convert in the other direction (binary → text for saving), TextLockfile.Stringifier.saveFromBinary is used src/install/lockfile.zig347
The text lockfile is the preferred new default format (as of recent versions of Bun) because it is human-readable and diff-friendly. Analytics are tracked via bun.analytics.Features.text_lockfile.
Lockfile.loadFromDir src/install/lockfile.zig228-370 implements the following resolution order:
Lockfile load priority — Lockfile.loadFromDir
Sources: src/install/lockfile.zig228-370 src/install/migration.zig1-80
LoadResultLockfile.LoadResult src/install/lockfile.zig106-216 is a tagged union:
| Variant | Meaning |
|---|---|
.not_found | No lockfile exists anywhere |
.err | File exists but could not be read/parsed |
.ok | Successfully loaded; includes format, migrated, loaded_from_binary_lockfile |
The .ok variant's migrated field can be .none, .npm, .yarn, or .pnpm.
When no Bun lockfile exists, detectAndLoadOtherLockfile src/install/migration.zig3-80 checks for third-party lockfiles in priority order:
package-lock.json → migrateNPMLockfile (requires lockfileVersion 2 or 3)yarn.lock → migrateYarnLockfilepnpm-lock.yaml → migratePnpmLockfile (requires v7+)Each migration function reads the foreign lockfile, maps its package graph into Bun's Lockfile data structures in memory, and returns a LoadResult.ok. Bun then saves the lockfile in its own format on the next write.
A migration that succeeds prints a message like:
migrated lockfile from package-lock.json
Sources: src/install/migration.zig
LoadResult.saveFormat src/install/lockfile.zig157-193 determines which format to write based on:
| Situation | Result |
|---|---|
| No previous lockfile | Text (bun.lock) by default; binary if save_text_lockfile: false in bunfig |
Loaded from bun.lock | Text |
Loaded from bun.lockb | Binary (unless save_text_lockfile: true in bunfig) |
| Migrated from npm/yarn/pnpm | Text |
| Error, but format was known | Respects bunfig override, else keeps original format |
The save_text_lockfile option can be configured in bunfig.toml under [install].
Before saving, Lockfile.cleanWithLogger src/install/lockfile.zig637 compacts the lockfile by traversing the package graph from the root and cloning only reachable packages into a fresh Lockfile. The Cloner struct manages this traversal. Package IDs are remapped via a package_id_mapping: []PackageID array.
maybeCloneFilteringRootPackages src/install/lockfile.zig430-465 is called for production installs to filter out dev dependencies before compaction.
The Lockfile.Tree structure stored in buffers.trees encodes the hoisting decisions. Each Tree node has a dependency_id linking it to the dependency that introduced this node_modules scope. Tree ID 0 is the root node_modules.
The DependencyInstallContext struct src/install/install.zig217-221 carries tree_id and path to determine where a package lands on disk:
PackageInstaller src/install/PackageInstaller.zig walks the tree and calls into PackageInstall for each package, using the appropriate file copy backend.
Lockfile.Scripts src/install/lockfile.zig54-100 collects per-package lifecycle scripts. The names constant defines the execution order:
preinstall → install → postinstall → preprepare → prepare → postprepare
At most MAX_PARALLEL_PROCESSES = 10 scripts run concurrently src/install/lockfile.zig55
LifecycleScriptSubprocessDefined in src/install/lifecycle_script_runner.zig this struct:
bun exec <script> (Windows) or $SHELL -c <script> (POSIX)OutputReader (buffered async I/O)current_script_indexalive_count (atomic) for concurrency limitingLifecycleScriptSubprocess — script spawn flow
Sources: src/install/lifecycle_script_runner.zig121-230
Lifecycle scripts are only executed for packages that appear in a trust list. This is controlled by two sources:
src/install/default-trusted-dependencies.txt — a curated list of well-known packages that commonly need to run scripts (e.g., @prisma/engines, better-sqlite3, canvas).trustedDependencies array in the root package.json.The Lockfile.trusted_dependencies field src/install/lockfile.zig26 stores the combined set as a TrustedDependenciesSet. Features.trusted_dependencies src/install/install.zig141 controls whether to read this field during package loading.
Bun supports bun patch <package> to apply local patches to installed packages. Patched packages get a special cache directory tagged with a hash of the patch content.
The bun_hash_tag prefix ".bun-tag-" src/install/install.zig3 is prepended to a hex-encoded hash to form unique cache subdirectory names for patched packages:
<cache>/<package>-<version>.bun-tag-<hex_patch_hash>/
buntaghashbuf_make src/install/install.zig11-20 formats this string into a stack buffer.
Patched packages go through additional PreinstallState stages (calc_patch_hash, calcing_patch_hash, apply_patch, applying_patch) before reaching done. These are handled by PatchTask src/install/patch_install.zig
The patched_dependencies: PatchedDependenciesMap field in Lockfile src/install/lockfile.zig27 stores the mapping from package name hash to patch file path and hash.
Sources: src/install/install.zig3-19 src/install/patch_install.zig src/install/lockfile.zig27
| Type | File | Role |
|---|---|---|
PackageManager | PackageManager.zig | Orchestrates the entire install process |
Lockfile | lockfile.zig | In-memory and on-disk package graph |
Lockfile.Package | lockfile.zig | Per-package metadata (MultiArrayList row) |
Lockfile.Buffers | lockfile.zig | Flat arrays: dependencies, resolutions, trees, strings |
Lockfile.Tree | lockfile.zig | Hoisting tree node |
Lockfile.Serializer | lockfile.zig | Binary format read/write |
TextLockfile | lockfile/bun.lock.zig | Text format read/write |
Dependency | dependency.zig | Dependency edge (name, version spec, behavior) |
Resolution | resolution.zig | Resolved package (npm version, git ref, tarball URL, etc.) |
PackageInstaller | PackageInstaller.zig | Hoisted install file writer |
isolated_install.Installer | isolated_install/Installer.zig | Isolated install orchestrator |
Store | isolated_install/Store.zig | Content-addressed virtual store |
LifecycleScriptSubprocess | lifecycle_script_runner.zig | Runs lifecycle scripts |
ExtractTarball | extract_tarball.zig | Tarball integrity check and extraction |
PatchTask | patch_install.zig | Applies patch files to cached packages |
Bin | bin.zig | node_modules/.bin symlink management |
Sources: src/install/install.zig src/install/lockfile.zig src/install/dependency.zig src/install/resolution.zig src/install/lifecycle_script_runner.zig src/install/extract_tarball.zig
Refresh this wiki
This wiki was recently refreshed. Please wait 2 days to refresh again.