install: fold the open bun install / pm / pack+publish PRs into one branch (171 PRs)
Folds the open bun install / bun pm / pack+publish PRs from Aug 12–17 into one branch so they can be reviewed and landed together. Each original PR is one squashed commit here (<title> (#N)), in dependency order, so git log/git blame attribute every hunk to its PR; conflict resolutions between overlapping PRs live in the later PR's commit. Cross-PR API reconciliation is in the mega-merge fixup: commits at the end, so the per-PR commits after #38804 (e2cf8c4305) do not build on their own until 0adcafaf8b — bisect across the fold with --first-parent on the fixups, not inside it.
171 PRs folded, 3 excluded (see below); #38557 was re-ported by hand onto the newer audit_command.rs. A simplification pass then roughly halved the net src/ addition (see below). cargo check --workspace and cargo clippy --workspace are clean, the debug build links, and all 94 test/cli/install/** files pass locally on Linux (debug+ASAN); see Verification.
Not folded
| PR | Why |
|---|---|
| #37925 | semantic conflict with #39002 (which needs optional peers bound at load); #39002 is newer and explicitly calls this out |
| #38777 | security screen: make_executable() follows an absolute bin-target symlink out of a file:/link: package and chmods the real file (CVE-2019-16775 class) when the folder dep resolves to an ancestor dir; needs rework before merging |
| #38810 | dropped on request: installing a git dependency's devDependencies before prepare is rarely needed and runs a nested install (fetching and, via prepare, executing third-party devDependencies chosen by the git repo) outside the project's bunfig protections — too much attack surface for the benefit |
Security screen
Every folded diff was reviewed for regressions (path traversal, credential leakage, TLS/integrity weakening, lifecycle-script trust widening, terminal injection, new unsafe on untrusted input). One PR was dropped for it (#38777 above) and one more on review (#38810). Items a reviewer may still want to eyeball — none were judged exploitable regressions:
- #37851 (concern, low): bunx serialises concurrent installs into the shared /bunx-- dir with an flock on /.bunx-lock; the lock file is opened by path with O_CREAT|O_RDWR and without O_NOFOLLOW, adding a small symlink-following primitive inside the pre-existing TOCTOU window of the cache-dir trust check.
- #38741 (concern, low): New bun.lock files are stamped lockfileVersion 1 instead of 2, so the two v2-only parse-time fail-closed checks (off-registry npm tarball URL without a supported integrity hash; unsafe git
.bun-tag) no longer apply to any lockfile created after this lands. The reduction is defence-in-depth only: the checks are keyed on a digit the lockfile tamperer also controls, main already stamps v1 whenever bun's own output would trip them, a tamperer can supply a matching integrity for their own tarball anyway, and the git tag is re-validated at checkout/extract time (repository.rs:923, extract_tarball.rs:484; github tags stay checked at every version). - #38795 (concern, low): Default-trusted check now ignores the URL fragment; the claim that this is exactly as strict as before is slightly off because bun_url lets a
?after#leak the fragment into the request path, though no attacker-chosen tarball results. - #38800 (concern, low): .npmrc //host/path/ credentials are now resolved by request URL (npm nerf-dart semantics): scopes without credentials pick them up by registry URL, and tarballs on a host other than the registry get that host's own .npmrc credentials. Scope credentials still never leave the scope origin, but the by-URL match is scheme-agnostic and path-unnormalized, which slightly widens where a registry-controlled dist.tarball can make bun present a token.
- #38990 (concern, low): package-lock
file:targets inside a registry/tarball/git package are now migrated as package-relative folder rows without normalising the sub-path, so a hand-edited key with..is no longer skipped at migration time and containment relies on the installer (hoisted linker checks it, with an overrides-name exemption; isolated linker does not). - #39084 (concern, low): Skips openat2/fchmodat2 on Android (kernel release contains 'android' OR both ANDROID_ROOT and ANDROID_DATA env vars set); symlink-safety semantics are preserved by the fallbacks, but the env-var trigger lets any environment downgrade RESOLVE_BENEATH/RESOLVE_IN_ROOT hardening on ordinary Linux.
Product-behaviour changes that need a maintainer decision before this lands: #38741 (new bun.lock files are stamped lockfileVersion: 1 again — its own PR defers this to a maintainer, and it obsoletes the open #36464; the sibling snapshots are already re-stamped inside the fold, so pulling it back out is one revert + fixlv), #38797 (libc filtering needs the full registry document for every optional dependency on every OS, since npm's abbreviated document omits libc; measured: @esbuild/linux-x64 43 KB → 54 KB gzipped, @rollup/rollup-linux-x64-gnu 40 KB → 58 KB, one-time per package per manifest cache, in exchange for not downloading the other libc's multi-MB tarballs on Linux and a host-independent lockfile), #38804 (a migrated lockfile and its package.json edits are only written when the command saves a lockfile), #38923 (bun pm ls lists what is installed and exits 1 without node_modules), #38813 (pack/publish resolve workspace:/catalog: from package.json files, not bun.lock).
Simplification pass (after folding)
The PRs were written independently, so the folded diff carried duplicate helpers and a lot of narrative comments. A follow-up pass (the simplify(...) commits) cut the src/ diff against main from +12,081 / −6,723 (net +5,358) to +10,284 / −7,559 (net +2,725) with no behaviour change intended and the full test/cli/install/** suite green before and after. Main items:
- One
Lockfile::packages_named/package_satisfying/patched_package_satisfyingreplaces nine "package_index → first candidate whose resolution satisfies a range and a predicate" helpers spread over lockfile.rs, bun.lock.rs, PackageManagerEnqueue.rs, update_transitive.rs and prune.rs. update_transitive.rs(#38919/#38770) no longer re-implements the resolver's rules: it callskeep_locked_if_ahead,is_named_update_row(split out ofshould_update) anddedupe::applied_overridedirectly, and itsPlan/Planned/InstanceEdges/KeepLockedscaffolding is gone; reachability useslockfile::reachable.- One checked NUL-terminated join (
resolve_path::join_abs_string_buf_z_checked) and one spill helper replacebin.rs::join_z_checked,PackageManager::join_path_z, and pack'snormalize_buf_spill; the bunx install lock (#37851) is a singleInstallLockRAII type. - Isolated linker: one
append_store_path_at(buf, entry, Which)family;PackageInstall::destination_dir_subpath_bufand theDestinationSubpathguard removed in favour ofjoin_z_spill; staging installs re-target aPackageInstallinstead of threadingdest_subpaththrough every backend;remove_linkshared. - yarn.rs: one dependency-section table instead of four loops, git URL parsing folded into
Entry::set_git_source, root/workspace package.json parsing shared; pnpm.rs: one merge-basedcopy_into_root, oneworkspacesobject path;finish_migrationbuilds the one error shape. - Options/registry:
RegistryPath,set_scope_registryand the second credential lookup removed in favour ofscope_for_package_name+UrlAuth::find;fmt::for_terminal(x)=EscapeControlChars(Redacted(x))at every call site;MinimumReleaseAgeExcludesis one list. - Multi-line justification comments added by the PRs trimmed to one line or removed throughout (SAFETY lines kept).
Two small behaviour fixes fell out of the review agents' cross-checks and are their own commits: bun audit fix now rewrites a matching rule in resolutions as well as overrides (the parser reads both since #38811), and a failed root package.json write says failed to write package.json: <errno> instead of for workspace ''.
Conflict resolutions that changed a PR's code (not just context)
- #37713 × #38832: kept #37713's rule that a deferred peer never range-matches on sight (no
*exemption); #38832'sAppendedFor/settled_package_countmodel replaces theexact_pinnedguard, with #38770's direct-dependency clause folded into the reusable predicate. - #38768 × #38892:
resolve_peer_dep_version_based_amongkeeps #38768's candidate filter but drops the highest-candidate fallback #38892 removed. - #38767/#39002 vs #37925: #37925 excluded (see above).
- #38857 vs main's
collect_bundled_depsrefactor: took #38857's per-versionbundled_dependencieslist. - #38878 (yarn migrator rewrite) absorbed #38767's yarn index change; #38803/#38948/#38985/#39001 re-applied onto the rewritten migrator (#39001's id remap now runs right after the append loop).
- #38754/#38775/#38780/#38804 in
pnpm.rs: oneupdate_package_json_after_migration(lockfile: Option<&mut Lockfile>, …); everything is reported as “copied … in package.json” (pnpm keeps reading its own block), the write is deferred to lockfile save (#38804), andbun pm migrateon a read-only package.json reports the failed write but keeps the migrated bun.lock (#38745's intent). - #38322 re-based onto #38800's
url_authmodel instead of adding a second.npmrccredential table;--registrygoes through the sameset_default_registrypath unless the URL itself carries credentials (#38834). - #38631/#38977 at shared print sites resolve to
EscapeControlChars(redacted(…))(redact inside, escape outside); oneEscapeControlCharsimplementation (the multiline-capable one from #38536) is kept. - #38369 on top of the refactored isolated linker:
symlink_dependenciesreturnsResult<bool, TaskError>soLinkPathTooLongstill names the package. - #38180:
fail_fnremoved as in the PR; the invalid-name path from #38615 now logs like the other root-resolution errors. - #38045 × #38369 (isolated linker): symlink entries build their target through
append_dependency_path, which now joins with the..-normalising checked join, so alink:/file:target that only overflows once joined reportsLinkPathTooLonginstead of hitting the "packages have no store path" unreachable. - #38780 × #38783:
count_blocked_scriptsreads lifecycle scripts from the final node_modules location for local (non-global-store) entries. - #38770 × #38919:
bun updateno longer treats a workspace member that was removed fromworkspacesas a direct-dependency owner, so its former deps can move. - #38954 × #38504: the long-bin-value tests set up their own directory per test (the file now runs under
describe.concurrent). - #38754 × #38804: with the migrated package.json written at flush time, a read-only package.json fails the install with
failed to write package.json: EACCESafter the lockfile is saved (test updated). - #38185:
bun update mookeeps a>=0.1.0literal that still covers the new version (main's behaviour); the test asserted^0.2.0. - #38766: the pthread_create LD_PRELOAD shim cannot interpose on ASAN builds (the executable's own interceptor wins), so that file skips under ASAN like it already does on musl.
- #38504: tests other PRs added to bun-install-registry.test.ts against the old shared
packageDirnow callsetupTest().
Folded PRs
Peer binding, hoisting, bun.lock load/round-trip, resolver dedupe (21)
- #38767 install: bind peer edges in the hoister, the way loading bun.lock binds them
- #38770 install: keep transitive rows on the copy a direct dependency resolves to
- #38869 install: load root and workspace dependencies from bun.lock the way package.json parses them
- #38892 install: keep a peer nothing in bun.lock satisfies where the file records it
- #38901 install: re-resolve a peer row instead of binding it to a stale bun.lock leftover
- #39002 install: keep the packages bun.lock holds only through optional peers when an install re-resolves
- #39343 install: bind unbound required peers while hoisting, like optional peers
- #39347 install: bind a multi-path package's edges from the bun.lock row that prints the binding
- #38849 install: only re-resolve rows a package still owns when overrides or catalogs change
- #37713 install: resolve deferred
*peers after their siblings, not on arrival order - #38768 install: rebind ranged peers whose target the saved tree drops
- #38832 install: resolve a range onto an existing version only when every install has it
- #38848 install: keep a bundle's dependency out of a slot the bundling package resolves through
- #38858 test(install): pin both no-deps versions instead of racing a range against a pin
- #38837 install: keep bundled peer edges on their own bun.lock entry when loading
- #38857 install: keep bundleDependencies from every dependency group when parsing a registry manifest
- #38919 Hold a transitive update that would re-fork a deduped package
- #38976 install: stop hoisting a dependency cycle forever
- #39053 install: warn when the hoisted tree serves a peer from a root copy outside its range
- #38741 install: stamp lockfileVersion 1 on new bun.lock files
- #39190 test(install): establish the warm manifest cache before the unmet peer test re-resolves
bun update / bun add package.json edits, overrides (7)
- #38763 install: print the update row for catalog entries moved by bun update
- #38847 install: keep workspace and other non-registry entries as written on bun update
- #38918 Report package.json sync in the bun update summary instead of (no changes)
- #38331 install: ignore leading whitespace in dist-tag, bare folder and local tarball versions
- #38811 install: apply both "overrides" and "resolutions" when package.json has both
- #38866 install: keep the workspace: spelling given to bun add @workspace:
- #38309 install: ignore leading whitespace wherever a dependency literal is classified or re-parsed
Lockfile migration (yarn / pnpm / npm) (17)
- #38698 Fix package-lock.json migration for scope registries configured without a trailing slash
- #38773 install: record package.json trustedDependencies when migrating a lockfile
- #38775 install: keep the pnpm block in package.json when migrating from pnpm
- #38795 install: fix default-trusted lifecycle scripts being blocked after yarn.lock migration
- #38829 install: migrate the whole pnpm-workspace.yaml catalog into bun.lock
- #38990 install: migrate a package's own file: directories from package-lock.json relative to the package
- #38780 install: migrate pnpm onlyBuiltDependencies and report blocked scripts under the isolated linker
- #38754 install: import pnpm-workspace.yaml even without a migratable pnpm-lock.yaml
- #38781 install: skip workspace members whose package.json has no name
- #38791 install: migrate pnpm importer rows in the shape package.json parsing produces
- #38878 install: migrate workspace packages from yarn.lock
- #38803 install: fix yarn.lock migration panic on tarball URLs with "/-/" right after the host
- #38948 install: name yarn.lock npm: alias packages after the alias spec, not the tarball URL
- #38985 install: migrate yarn.lock registry entries that have no resolved field
- #39001 install: skip yarn.lock entries the migration cannot build a resolution for
- #38861 install: migrate npm-shrinkwrap.json like package-lock.json
- #38804 install: write a migrated lockfile and its package.json edits only when the command saves a lockfile
file: / link: / tarball / git local dependencies (16)
- #38045 install: fail the isolated install when a link: dependency's target is missing
- #38814 install: read the package.json of a file: dependency declared by a local file: package
- #38816 install: keep file: folder paths declared by git and tarball packages relative to the package
- #39011 install: drop the query string and fragment from a tarball URL's extraction folder name
- #39016 install: cache file: tarballs under the integrity the lockfile pins
- #38633 install: reject tarball, folder and git packages whose package.json name is invalid
- #38798 install: read git+git:// resolutions back from bun.lock
- #38850 install: resolve file: dependencies loaded from the lockfile relative to the top-level dir
- #38856 install: link file: dependencies of registry packages from inside the package with the isolated linker
- #38867 install: bound isolated store entry names; tarball URL credentials; file: tarballs relative to their folder package
- #38986 install: refuse local tarball dependencies declared by packages installed from the cache
- #38994 install(hoisted): link a file: override applied to a dependency of an installed package
- #39023 install: refuse link: paths with .. or an absolute path declared by packages installed from the cache
- #38681 install: name folder, tarball and git packages without a package.json name after their source
- #38114 install: resolve file: and link: dependencies on the same directory as separate packages
- #38702 install: stop leaking the displaced cache folder when a tarball is re-extracted
Path-buffer overflows: panic → error (15)
- #38359 install: stop aborting on link: specifiers and patch paths longer than the path buffers
- #38368 cli: stop aborting on --cwd and --tsconfig-override values longer than the path join buffer
- #38390 install: stop panicking when the cache directory setting does not fit the path buffer
- #38558 install: stop panicking when a progress bar name is longer than its buffer
- #38571 install: stop aborting on linked package names that do not fit the symlink buffers
- #38575 install: fail verification instead of panicking when /package.json does not fit the path buffer
- #38616 pm ls --all: stop panicking on resolutions longer than 512 bytes
- #38784 pack/publish: stop panicking on package.json bin and files entries longer than the path buffer
- #38954 install: stop panicking on bin values longer than the path buffer
- #38375 install: report ENAMETOOLONG for a --cwd value that does not fit the path buffer
- #38369 install: fail the isolated install with ENAMETOOLONG when a link: target does not fit the path buffer
- #38372 install: do not abort when $XDG_CONFIG_HOME or $HOME is too long for the .npmrc path buffer
- #38413 install: stop aborting on -g when $BUN_INSTALL, $XDG_CACHE_HOME or $HOME does not fit the global directory path buffer
- #38749 pack: report an error instead of panicking when --destination does not fit the path buffer
- #38753 publish: report ENAMETOOLONG for a tarball path that does not fit the path buffer
Terminal control-character escaping and credential redaction (9)
- #38536 pm view: escape control characters coming from the registry
- #38817 install: redact secrets in the URLs printed for failed manifest and tarball downloads
- #38997 install: redact the registry URL in the manifest/tarball URL errors and keep namedRegistries credentials out of bun.lock
- #38525 install: escape control characters in the bun pm untrusted/trust script listing
- #38615 install: reject dependency names containing control characters
- #38673 http: escape control characters in the verbose request/response trace
- #39358 pm licenses: escape control characters in text output, including the name@version column
- #38631 install: escape control characters in resolutions, specifiers, bin names and registry error text
- #38977 install: redact credentials in tarball and git URLs printed as resolutions and specifiers
Registry auth, .npmrc, proxies, URL parsing (16)
- #38001 install: keep registry credentials across redirects to the same hostname
- #38047 install: read NPM_CONFIG_USERCONFIG as the user-level .npmrc path
- #38075 publish, pm whoami: route registry requests through http_proxy
- #38310 install: make BUN_CONFIG_NO_VERIFY disable integrity verification when set
- #38744 install: honor BUN_CONFIG_MAX_HTTP_REQUESTS
- #38812 url: emit a single slash in href_without_auth() for root-path registry URLs
- #39000 url: parse the host of a protocol-relative URL instead of leaving the authority in the path
- #38759 install: say --network-concurrency defaults to 64 in help and docs
- #38766 http: report a refused HTTP client thread as an error instead of a crash
- #38834 install: send credentials embedded in a registry URL that comes from an env var
- #39063 install: send Basic auth for a registry URL with a username but no password
- #38916 install: replace an existing manifest cache entry atomically instead of unlinking it first
- #38484 install: warn when a registry manifest advertises an integrity hash bun cannot use
- #38093 publish, pm whoami, audit: honor NODE_TLS_REJECT_UNAUTHORIZED=0 like bun install
- #38043 Normalize http_proxy/https_proxy env values through the WHATWG parser
- #38800 install: resolve .npmrc //host/ credentials by request URL
bun pm subcommands, audit, help text (14)
- #37922 bun why: print a package's dependents once, mark repeats as *deduped
- #38025 pm pkg: support arrays in set and delete
- #38649 install: format versions with the string buffer they were parsed from (bun outdated table, minimum-release-age messages)
- #38836 audit: list --production, --omit, --dry-run and --cwd in bun audit --help
- #38893 install: say --ignore-scripts also skips trusted dependencies' scripts in --help
- #38952 pm: polish dedupe, prune, pm ls and pm licenses output
- #39216 prune: add --check, a CI gate that exits 1 when anything would be removed
- #39317 audit fix: never downgrade a package out of its installed release line
- #38671 pm view: match the requested range against the buffer it was parsed from
- #38923 Make bun pm ls list installed packages instead of lockfile entries
- #39165 cli: load a package.json and name a lockfile load step from one place
- #38915 install: describe the real --save default in bun link and bun unlink --help
- #38785 audit fix: report and rewrite the overrides rule that holds a vulnerable version
- #39217 Add --why to bun dedupe to show one level of dependents under each row
bun pm pack / bun publish (13)
- #38071 run: set npm_package_name/version/json and npm_config_local_prefix per run
- #38704 publish: resolve a relative tarball path against the invoking directory
- #38716 pack, publish: include entries on filesystems whose readdir reports DT_UNKNOWN
- #38721 pack: build the tarball in memory and write it out only once it is complete
- #38776 publish: accept basic auth credentials from .npmrc and bunfig.toml
- #38322 publish: honor publishConfig.registry and publishConfig["@scope:registry"]
- #38707 pack: skip bins reached through symlinks; publish: do not read the readme through a symlink
- #38715 pack: store tarball entry names byte for byte
- #38720 pack: pack a file listed under several bin names once, strip the trailing slash from directories.bin
- #38739 pm pack: resolve relative --destination and --filename against the invoking directory
- #38813 pack/publish: resolve workspace: and catalog: specs from the package.json files, not bun.lock
- #38835 pack: publish workspace: directory dependencies as the workspace's version
- #38788 pack: publish workspace: aliases as npm: aliases
Linkers, bins, node_modules layout (9)
- #38303 install(hoisted): track replaced folders instead of deferring and re-verifying nested skips
- #38756 Resolve dangling node_modules symlinks as missing modules; isolated installs unlink failed optional dependencies
- #38783 install: link packages into a staging directory and rename them into place
- #39096 bunx: resolve directories.bin relative to the package
- #38511 install: remove the node_modules links of a renamed or removed workspace
- #38779 install: reinstall node_modules when switching between the isolated and hoisted linkers
- #38972 install: keep linking the remaining bins when one bin or one dependency's bins fail to link
- #37757 install: link over an empty directory left at node_modules/ with the isolated linker
- #38018 install(windows): store an absolute target in .bunx shims when the package dir is on another drive
Lifecycle scripts, env, minimum-release-age, semver, auto-install (16)
- #38648 install: drop the duplicated warn: label from the verbose tarball retry notice
- #38098 install: fix node-gyp shim PATH entry after tempdir falls back to cache/.tmp
- #38110 install: set npm_config_local_prefix and npm_package_config_* for lifecycle scripts
- #38830 install: skip root lifecycle scripts and the yarn.lock write under --dry-run
- #38859 install: remove the same-filesystem probe from $TMPDIR when the rename into the cache fails
- #37895 install: print version before package name in the minimum-release-age error
- #37909 install: support name@version entries in minimumReleaseAgeExcludes
- #38659 install: do not resolve an exact version from an abbreviated manifest under minimum-release-age
- #38688 semver: print the build tag from the printed version's buffer in the colored version diff
- #38198 resolver: make auto-install use the version range from the project's package.json
- #38182 run, install: honor --no-env-file / --env-file for scripts run through the bun exec hop
- #38995 install: warn instead of failing when minimum-release-age blocks a peer dependency
- #38687 semver: record cloned prerelease/build tag offsets against the whole destination buffer
- #38180 auto-install: report why a root dependency could not be resolved
- #38745 install: only open package.json for writing in the commands that rewrite it
- #38797 install: filter optionalDependencies by libc and record it in the lockfile
Test-only changes (13)
- #37919 test(install): serve URL tarball dependencies locally in bun-install.test.ts
- #38185 test(install): keep bun-update-security-provider on the dummy registry, assert the update, run concurrently
- #38255 test(install): give each long-label bun patch test its own install cache
- #38467 test(bunx): install fixture packages from a local registry instead of real ones
- #38580 test(install): don't require the refetched manifest to be cached before install exits
- #38627 test(install): normalize the registry URL, not the bare port, in the lockb print snapshot
- #38891 test(harness): let verdaccio pick its own port and fail start() when it exits early
- #39022 test(install): run overrides.test.ts against the local registry
- #39115 test(install): serve GitHub tarball fixtures locally in the GHSA-pfwx-36v6-832x and lifecycle script tests
- #39232 test: add a harness isRoot and skip the chmod-based ls and Bun.file cases as root
- #38464 test(install): speed up bun-install.test.ts and tighten its assertions
- #38504 test(install): run bun-install-registry.test.ts concurrently and tighten its assertions
- #39243 test/docker: wait for the docker daemon in the coordinator instead of failing on a one-shot probe
Platform-specific (5)
- #37865 install: schedule the Windows hardlink task through the allocation's own pointer
- #38567 install.ps1: make the "another bun.exe is already in PATH" check work
- #39084 install: don't issue openat2/fchmodat2 on Android (seccomp SIGSYS)
- #37851 bunx: serialize concurrent installs into the shared cache dir with a file lock
- #39361 Fix bun install through subst drives, cross-drive junctions, and symlinked package.json
Verification
-
cargo check --workspace -
cargo clippy --workspace --no-deps - debug build (Linux x64, ASAN)
-
test/cli/install/**,test/cli/run/**,test/regression/issue/*install*locally with the debug build. Remaining local-only failures are environmental and reproduce on main in this sandbox:bun-pm.test.tsread-only package.json cases (run as root;nobodycannot exec a binary under/root),thread-spawn-failure(LD_PRELOAD shim vs ASAN). - CI (first run on the pre-simplification head: 3 failing files, all addressed above — thread-spawn shim under ASAN, pnpm read-only package.json wording, update-security-provider range)