install: add --offline and --tarball-dir for installs without network access
What
Two install settings for running bun install without network access, shaped as strict offline controls rather than a separate mode:
--offline(bunfiginstall.offline, envBUN_CONFIG_OFFLINE): disallow network requests. Cached registry manifests are used regardless of staleness, and a package that is not available locally fails the install with an error naming it instead of a download.--tarball-dir <dir>(bunfiginstall.tarballDir): read npm package tarballs from a directory of<name>@<version>.tgzfiles, with scoped packages in a@scope/subdirectory. Files are verified against the lockfile's integrity hashes. Offline, the directory is the only source; online, the file is used when present and the network otherwise.
Together with --frozen-lockfile and a sandboxed BUN_INSTALL_CACHE_DIR, this supports hermetic (Bazel-style) builds: the build system pins the lockfile and pre-downloads hash-verified tarballs, and bun install --offline --tarball-dir=<dir> lays out node_modules with no network, erroring on any missing tarball or integrity mismatch.
bun install --frozen-lockfile --offline --tarball-dir=./vendor/tarballs
Closes #7956
How
--offline intercepts at the points where installs create network work:
- Manifests: a manifest loaded from the disk cache is classified as fresh when offline (
PackageManifestMap), since there is no network to revalidate against. When no usable cached manifest exists, the enqueue site logs an error instead of creating the task, mirroring the existing non-retryable manifest failure handling (error for required dependencies, warning for optional ones, failedbun addrequests marked). The message distinguishes a missing manifest from one that was demoted becauseminimumReleaseAgeneeds publish dates it lacks. The lockfile-migration manifest prefetch gets the same guard. - Tarballs (npm, GitHub, remote):
generate_network_task_for_tarballis the single funnel. Offline, it logs"x@1.2.3" is missing from the cache and network requests are disabled (--offline)with the applicable remedy, marks the dedupe entry failed, and returns a newForTarballError::NetworkDisabled, which every caller (hoisted and isolated installers, the resolve-phase github/remote arms, and the patch task's extract arm) handles with the same bookkeeping as its existing failure arms, so waiting entries drain and the install fails cleanly at the end. - Git: the clone task fails on the worker with the same style of error instead of spawning git (a cached bare repo would still run
git fetch, so any clone task means the network). Failing the task rather than skipping the enqueue keeps every waiting installer entry draining through the normal completion path. Git and GitHub dependencies must already be in the cache when installing offline. - The resolve-phase tarball prefetch is skipped offline; the install phase re-checks the cache and reports with full context.
--tarball-dir reuses the existing local-tarball machinery: a Tag::LocalTarball threadpool task whose ExtractTarball carries the npm resolution, so the file is read, checked against the lockfile integrity, and extracted into the package's normal npm cache folder under the same task id as a download. Waiting installers, patched dependencies, and both linkers behave identically to the network path. Online, the file is stat'd first (same path resolution as the read) to decide between the local file and the network. A non-boolean install.offline in bunfig is a config error rather than silently staying online.
Verification
test/cli/install/offline-install.test.ts (15 tests): fixtures are seeded through a local verdaccio with per-fixture cache directories, then the registry process is killed, so any accidental network request fails the test with a connection error instead of the behavior under test. Covered: warm-cache offline installs, bunfig/env equivalents (install.offline, install.tarballDir, BUN_CONFIG_OFFLINE), cold-cache failure messages, tarball-dir installs on both linkers (including a scoped package), missing-file and integrity-mismatch failures, online fallback, tarball-dir during resolve-phase prefetch (no lockfile), a patched dependency installed from the tarball dir with and without a lockfile plus its warm-cache offline success and cold-cache offline failure, the minimumReleaseAge offline message, offline resolution from a cached manifest without a lockfile, missing-manifest failure, and git/github dependency failures.
On a build without this change, most of the tests fail (the flags are ignored and installs hit the dead registry). Two paths were additionally verified to fail against their pre-change code: the resolve-phase prefetch (.expect("unreachable")) and the patch task's extract arm (unreachable!()), both of which assumed a network task always exists. bun bd test also passes for isolated-install.test.ts, bun-lock.test.ts, config-version.test.ts, and the frozen-lockfile tests in bun-install.test.ts.
[review] gate passed · iteration 2 · 20 files touched
fails on main (without fix)
ASAN without fix: 13 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/install/offline-install.test.ts
bun test v1.4.0 (635fd107c)
test/cli/install/offline-install.test.ts:
Verdaccio exited with code null and signal SIGKILL
194 |
195 | test.concurrent("--offline with a cold cache and no tarball dir fails naming the package", async () => {
196 | await clearCacheAndNodeModules(coldCacheNoSource);
197 |
198 | const { err, exitCode } = await runInstall(coldCacheNoSource.packageDir, ["--offline"]);
199 | expect(err).toMatch(/error: .*is missing from the cache and network requests are disabled \(--offline\)/);
^
error: expect(received).toMatch(expected)
Expected substring or pattern: /error: .*is missing from the cache and network requests are disabled \(--offline\)/
Received: "error: ConnectionRefused downloading tarball @types/no-deps@1.0.0\n\nerror: ConnectionRefused downloading tarball one-dep@1.0.0\n\nerror: ConnectionRefused downloading tarball no-deps@1.0.1\n"
at <anonymous> (/workspace/bun/test/cli/install/offline-install.test.ts:199:15)
(fail) --offline wit
... (truncated)
release without fix: all passed
bun test v1.4.0-canary.1 (54c0437b4)
test/cli/install/offline-install.test.ts:
Verdaccio exited with code null and signal SIGKILL
(pass) --offline fails to resolve a package with no cached manifest [14.21ms]
(pass) --offline fails a github dependency that is not cached [11.50ms]
(pass) --offline fails a git dependency that is not cached [17.89ms]
(pass) --offline installs from a warm cache with the registry unreachable [27.62ms]
(pass) --offline with a cold cache and no tarball dir fails naming the package [26.34ms]
(pass) --offline names the cached manifest's missing publish dates under minimumReleaseAge [30.60ms]
(pass) install.offline in bunfig and BUN_CONFIG_OFFLINE are equivalent to --offline [42.84ms]
(pass) --offline --tarball-dir fails when a tarball file is missing [43.01ms]
(pass) --tarball-dir covers tarballs fetched during resolution (no lockfile) [40.98ms]
(pass) --offline --tarball-dir rejects a tarball that fails the lockfile integrity check [43.95ms]
(pass) --offline resolves from the cached manifest without a lockfile [45.79ms]
(pass) --offline --tarball-dir installs with a cold cache (isolated linker) [51.67ms]
(pass) --offline --tarball-dir insta
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/install/offline-install.test.ts
bun test v1.4.0 (635fd107c)
test/cli/install/offline-install.test.ts:
Verdaccio exited with code null and signal SIGKILL
(pass) --offline with a cold cache and no tarball dir fails naming the package [363.95ms]
(pass) --offline installs from a warm cache with the registry unreachable [434.65ms]
(pass) --offline --tarball-dir fails when a tarball file is missing [321.90ms]
(pass) install.offline in bunfig and BUN_CONFIG_OFFLINE are equivalent to --offline [733.93ms]
(pass) --offline --tarball-dir rejects a tarball that fails the lockfile integrity check [505.19ms]
(pass) --offline --tarball-dir installs with a cold cache (hoisted) [1096.22ms]
(pass) --tarball-dir covers tarballs fetched during resolution (no lockfile) [494.56ms]
(pass) --offline names the cached manifest's missing publish dates under minimumReleaseAge [411.34ms]
(pass) --offline resolves from the cached manifest without a lockfile [586.58ms]
(pass) --tarball-dir without --offline uses the file when present, the network otherwise
... (truncated)
release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1071ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/5] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)
[1m[92m Compiling[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
[1m[92m Compiling[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
[1m[92m Compiling[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
[1m[92m Compiling[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
[1m[92m Compiling[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
[1m[92m Compiling[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
[1m[92m Compiling[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
[1m[92m Compiling[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
[1m[92m Compiling[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
[1m[92m Compiling[0m bun_brotli v0.0.0 (/workspace/bun/src/brotli)
[1m[92m Compiling[0m bun_output v0.0.0 (/workspace/bun/src/output)
[1m[92m Compiling[0m bun_clap
... (truncated)
diff hotspot
completions/bun-cli.json | 150 +++++++
docs/pm/cli/install.mdx | 29 ++
docs/runtime/bunfig.mdx | 22 +
docs/snippets/cli/install.mdx | 8 +
src/bunfig/bunfig.rs | 10 +
src/install/NetworkTask.rs | 5 +
src/install/PackageInstaller.rs | 15 +-
src/install/PackageManager/CommandLineArguments.rs | 15 +
.../PackageManager/PackageManagerDirectories.rs | 1 +
.../PackageManager/PackageManagerEnqueue.rs | 301 ++++++++++---
.../PackageManager/PackageManagerOptions.rs | 34 +-
.../PackageManager/PopulateManifestCache.rs | 14 +
src/install/PackageManager/runTasks.rs | 78 +++-
src/install/PackageManagerTask.rs | 24 ++
src/install/PackageManifestMap.rs | 10 +-
src/install/error.rs | 3 +
src/install/isolated_install.rs | 15 +-
src/install/patch_install.rs | 62 ++-
src/options_types/schema.rs | 2 +
test/cli/install/offline-install.test.ts | 467 +++++++++++++++++++++
20 files changed, 1173 insertions(+), 92 deletions(-)
gate history · 2 passed · 1 rejected · iteration 2
evidence per changed file
file reads edits tests
completions/bun-cli.json 0 0 0
docs/pm/cli/install.mdx 1 1 0
docs/runtime/bunfig.mdx 1 1 0
docs/snippets/cli/install.mdx 1 2 0
src/bunfig/bunfig.rs 2 4 0
src/install/NetworkTask.rs 2 3 0
src/install/PackageInstaller.rs 2 2 0
src/install/PackageManager/CommandLineArguments.rs 3 5 0
src/install/PackageManager/PackageManagerDirectories.rs 1 2 0
src/install/PackageManager/PackageManagerEnqueue.rs 9 11 0
src/install/PackageManager/PackageManagerOptions.rs 1 4 0
src/install/PackageManager/PopulateManifestCache.rs 1 2 0
src/install/PackageManager/runTasks.rs 5 4 0
src/install/PackageManagerTask.rs 1 3 0
src/install/PackageManifestMap.rs 2 1 0
src/install/error.rs 1 1 0
(+ 4 more files)