Skip to content

install: fold the open bun install / pm / pack+publish PRs into one branch (171 PRs) - #39403

Draft
Jarred-Sumner wants to merge 258 commits into
mainfrom
claude/install-mega
Draft

install: fold the open bun install / pm / pack+publish PRs into one branch (171 PRs)#39403
Jarred-Sumner wants to merge 258 commits into
mainfrom
claude/install-mega

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

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:

  • bunx: serialize concurrent installs into the shared cache dir with a file lock #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.
  • install: stamp lockfileVersion 1 on new bun.lock files #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).
  • install: fix default-trusted lifecycle scripts being blocked after yarn.lock migration #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.
  • install: resolve .npmrc //host/ credentials by request URL #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.
  • install: migrate a package's own file: directories from package-lock.json relative to the package #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).
  • install: don't issue openat2/fchmodat2 on Android (seccomp SIGSYS) #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_satisfying replaces 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 (Hold a transitive update that would re-fork a deduped package #38919/install: keep transitive rows on the copy a direct dependency resolves to #38770) no longer re-implements the resolver's rules: it calls keep_locked_if_ahead, is_named_update_row (split out of should_update) and dedupe::applied_override directly, and its Plan/Planned/InstanceEdges/KeepLocked scaffolding is gone; reachability uses lockfile::reachable.
  • One checked NUL-terminated join (resolve_path::join_abs_string_buf_z_checked) and one spill helper replace bin.rs::join_z_checked, PackageManager::join_path_z, and pack's normalize_buf_spill; the bunx install lock (bunx: serialize concurrent installs into the shared cache dir with a file lock #37851) is a single InstallLock RAII type.
  • Isolated linker: one append_store_path_at(buf, entry, Which) family; PackageInstall::destination_dir_subpath_buf and the DestinationSubpath guard removed in favour of join_z_spill; staging installs re-target a PackageInstall instead of threading dest_subpath through every backend; remove_link shared.
  • 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-based copy_into_root, one workspaces object path; finish_migration builds the one error shape.
  • Options/registry: RegistryPath, set_scope_registry and the second credential lookup removed in favour of scope_for_package_name + UrlAuth::find; fmt::for_terminal(x) = EscapeControlChars(Redacted(x)) at every call site; MinimumReleaseAgeExcludes is 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)

Folded PRs

Peer binding, hoisting, bun.lock load/round-trip, resolver dedupe (21)

bun update / bun add package.json edits, overrides (7)

Lockfile migration (yarn / pnpm / npm) (17)

file: / link: / tarball / git local dependencies (16)

Path-buffer overflows: panic → error (15)

Terminal control-character escaping and credential redaction (9)

Registry auth, .npmrc, proxies, URL parsing (16)

bun pm subcommands, audit, help text (14)

bun pm pack / bun publish (13)

Linkers, bins, node_modules layout (9)

Lifecycle scripts, env, minimum-release-age, semver, auto-install (16)

Test-only changes (13)

Platform-specific (5)

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.ts read-only package.json cases (run as root; nobody cannot 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)

robobun and others added 30 commits August 17, 2026 01:04
…le_package_name subsumes; dependency.rs trims

(cherry picked from commit 52f11087637146ffaca82549ccb00d890cf6f699)
(cherry picked from commit da762b3a2d9efef269eef974141901794a3d2285)
…kfile::reachable over snapshot-patched resolutions

(cherry picked from commit b63643331b0b58a17d666c6e2c7f108f99e1981f)
…eferred field)

(cherry picked from commit 5ad0fa213e5010fc7821e8168fbaf3b1a033b487)
…estructuring

(cherry picked from commit 8386c70db7dc11902dadcc2a82031a18379bf938)
…ut the redundant pre-scan

(cherry picked from commit 1f36ab77d7321dcbaa26e20fc61f56f46126c924)
(cherry picked from commit c4d5f35ad6c0a1593ca638f04c189d1b37c75e23)
(cherry picked from commit 5665d9f6bdbb80e301ba1df47bdb7eb3ed91c44e)
…ocality checks

(cherry picked from commit b96464da224ca6d23c6471d8fcc20bbbc32a22be)
(cherry picked from commit 36508548f417e76b906bf6573ea51311a735696a)
(cherry picked from commit 4af66bcd9663549eef04908b2d58e4bedd222c8f)
(cherry picked from commit 51edcf2bf8e693538504d51f78eef52e77cd457a)
…git URL parsing, dedupe package.json parsing

- Entry derives Default; the four dependency maps become one array in
  DEPENDENCY_SECTIONS order, which the parser and the migrator both iterate
- parse_git_url/ParsedGitUrl folded into Entry::set_git_source (was
  duplicated for the version and resolved keys)
- root and workspace package.json parsing share append_package_json
- package ids are handed out from one (dedupe name, version) map instead of
  the package_versions/scoped_packages pair
- os/cpu list parsing and Meta arch/os construction each written once

(cherry picked from commit 903a5f328c680483a59c17dd3712b442b92c8962)
…kspaces object path, drop redundant trusted fill

- copy_into_root always merges into a (possibly new) root-level object, so
  copy_object/shallow_clone_prop go away; the bare patch-key completion that
  rewrite_bare_patch_keys did happens during that merge
- the three branches that wrote `workspaces` (array, existing object, new
  object) collapse into array-or-object
- add_trusted_dependencies no longer fills lockfile.trusted_dependencies:
  migration::record_trusted_dependencies reads the edited cache entry right
  after and records the same list
- trim multi-line comments

(cherry picked from commit 736bc1281c48622f69c7613a1015cbe3fdb89456)
…lder_relative_to_top_level_dir, drop the Scanned enum

- WorkspaceManifests::workspace_name_at_path resolves the linked directory
  with lockfile::package::folder_relative_to_top_level_dir (the same
  join+relative+posix steps it spelled out)
- process_workspace_name returns Option<Entry> and records a skipped
  nameless member itself, replacing Scanned/workspace_entry
- shorten doc comments

(cherry picked from commit 2abe25708470a1a78bb233953ccbe2a8258bfb94)
…ditor tightening

- CatalogMap::put_missing_from_pnpm_workspace is a method; the recorded
  default catalog is moved out with mem::take for the `catalogs.default`
  group instead of splitting get_or_put_group into a free helper
- OverrideMap: parse_count walks both fields in one loop (count_field
  inlined), RuleCount becomes two ParseContext fields, put_scoped keeps
  push_scoped's original shape with a `keep` bound
- migration.rs: finish_migration takes the migrator's Result and builds the
  Migrating LoadResultErr in one place (migrating_error)
- PackageJSONEditor: literal_tag() for the seven infer(trim_literal(..)) sites
- shorter doc comments in package_json_write_back

(cherry picked from commit 39c13fbb5a8750e6e9184286e451bf93acfeb5cd)

@alii alii left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went through the fold with the head built locally and CI build 99835 pulled apart. Leaving these as comments, not a block, since the shape is Jarred's call.

Two things exist only because of the fold and are worth knowing before anything else: the isolated linker now panics on any link: dependency (#38045 x #38369, thread below), and lifecycle script counting on relink hits ENOENT (#38780 x #38783). Both reproduce with two-line projects on this head and neither reproduces on main or in the source PRs.

The "stays bisectable" claim does not hold today. 297ed01bf5 (the last folded PR before the fixups) does not compile: src/install/pnpm.rs has the same use crate::package_manager_real::update_package_json_and_install::print_package_json_into_cache_entry; twice and calls &mut lockfile.catalogs on an Option<&mut Lockfile>. The duplicate enters at e2cf8c4305 (#38804) and only leaves in 0adcafaf8b, so at least the last ~40 commits before the fixups do not build.

CI: build 99835 has 92 failed test-bun jobs on every Linux lane plus Windows, about 148 distinct failing assertion sites in ~25 files under test/cli/install; main's 99833 has none. Roughly a third are lockfileVersion 2 snapshots written after #38741, a handful are test-merge errors from the #38504 re-port, and the rest need a run to classify (bun update rows, peer binding in bun-lock.test.ts:2377/2757, minimum-release-age.test.ts:2344, the yarn migration warn lines).

If the goal is to land these, the shape I would push for is: the ~39 PRs whose own CI is green and which touch nothing another folded PR touches go in one by one; the peer-binding/bun.lock-load set, the yarn migrator set, the pnpm migrator set and pack/publish become one stack each with the reconciliation hunks moved into the stack that owns them; #38741, #38797, #38923 and #38813 stay out as their own maintainer-decision PRs. Happy to help cut the stacks.

Comment thread src/install/isolated_install.rs Outdated

// Same target check as the hoisted linker's `install_from_link`.
let mut link_target: AbsPath = AbsPath::init_top_level_dir();
installer.append_store_path(&mut link_target, entry_id);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a ResolutionTag::Symlink entry, and after #38369 append_store_path hits unreachable!("link: packages have no store path; see append_dependency_path") (Installer.rs:3138) for exactly that tag. Every isolated install with a link: dependency now aborts:

panic: internal error: entered unreachable code: link: packages have no store path; see append_dependency_path

Reproduced on this head with {"dependencies":{"lib":"link:../lib"}} and bun install --linker=isolated; main installs it. It is what takes down isolated-install.test.ts:508/4674/4714/4735/4759, bun-pm-licenses.test.ts:1083 and yarn-lock-migration.test.ts:2357 on every lane. Neither #38045 nor #38369 has this alone. The target check here wants the link target path (append_dependency_path builds it, and returns the ENAMETOOLONG error #38369 added), not the store path.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e4a0624: the symlink-entry check builds its target with installer.append_dependency_path(&mut link_target, entry_id) (which now joins with the ..-normalising checked join, so a link:/file: target that only overflows once joined reports LinkPathTooLong), never append_store_path. {"lib":"link:../lib"} with --linker=isolated installs on the current head; isolated-install.test.ts (111), bun-pm-licenses.test.ts (81) and yarn-lock-migration.test.ts (43) all pass.

pkg_res: &Resolution,
) -> u8 {
// A global-store entry stays in its staging dir until `Step::Binaries` renames it.
let mut pkg_dir = AutoAbsPath::init_top_level_dir();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the relink path nothing was copied into the staging dir, so Which::Staging names a directory that does not exist and get_list fails with ENOENT. Reproduces with isolated-relink.test.ts on this head:

error: failed to fill lifecycle scripts for uses-what-bin: ENOENT

Also behind bun-prune.test.ts:155 and the harness.ts:1578 hits in isolated-install.test.ts. #38780 was written against the pre #38783 layout, where the package was already at its final path when this ran; folded together the assumption in the comment above only holds for a fresh copy.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0ac90d6: count_blocked_scripts reads from Which::Final unless the entry lives in the global store, so the relink path (nothing staged) reads the package where it already is. isolated-relink, bun-prune and isolated-install pass on the current head.

/// `libc` recorded in the lockfile, so it does not depend on the host: a
/// lockfile written on macOS still filters on Linux.
pub(crate) fn needs_extended_manifest(&self, dependency: Behavior) -> bool {
self.needs_extended_manifest_to_pick_versions() || dependency.is_optional()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes every optionalDependencies resolve on every OS refetch the full packument: PackageManifestMap.rs:181-184 marks the cached abbreviated doc unusable when needs_extended is set, and the exact-version fast path is skipped too, so even a pinned optional dep goes back to the network with Accept: application/json and no conditional headers. On macOS and Windows the libc field is never consulted. There is no measurement in #38797. Worth timing a cold bun add vite on main vs this head, and gating on the host actually filtering by libc.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed the premise: npm's abbreviated document has no libc (@rollup/rollup-linux-x64-musl corgi keys are cpu, dist, name, os, version; the full doc has libc: ["musl"]), so #38797 has to read the full document to filter at all. Measured the delta for the packages this actually hits: @esbuild/linux-x64 43 KB → 54 KB gzipped, @rollup/rollup-linux-x64-gnu 40 KB → 58 KB. The refetch is one-time per package per manifest cache (the full doc is stored with has_extended_manifest and revalidated with conditional headers afterwards, same path minimumReleaseAge already uses), in exchange for not downloading the other libc's multi-MB tarball on Linux. Gating on the host having a concrete libc would make bun.lock's libc meta depend on which OS resolved the package, which I think is worse than the ~15 KB. I've left #38797 as written and moved it into the "needs a maintainer decision" list in the description next to #38741 rather than change its semantics inside the fold.

const real = buildTarball(Buffer.from('{"name":"pkg","version":"1.0.0"}\n'));
const other = buildTarball(Buffer.from('{"name":"other","version":"9.9.9"}\n'));

await using server = serveManifest(other.sha512, real.tgz);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#38484 changed serveManifest to take { integrity, shasum }, but these three calls from #38310 (here, :890, :914) still pass the bare string, so the spread produces {"0":"s","1":"h",...} and the manifest carries no integrity at all. The =1 case then passes for the wrong reason and BUN_CONFIG_NO_VERIFY=0 keeps the integrity check enabled fails on every lane. The runtime side is fine (checked by hand on this head: unset and 0 fail closed on a bad hash, 1 skips), it is the fixture that broke in the merge. Should be serveManifest({ integrity: other.sha512 }, real.tgz).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4cd0c85: all three calls pass { integrity: other.sha512 }; the =0 case fails closed and =1 skips for the right reason now (tarball-integrity 33/33).

loaded
} else {
Version::V1
if !lockfile.overrides.has_scoped() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flipping new lockfiles back to v1 is a release decision that #38741's own status comment defers to a maintainer, and it obsoletes the open #36464 (its has_scoped && !loaded.at_least(V3) hunk no longer exists) without saying so. It is also the single biggest source of red in 99835: about 15 inline snapshots written by PRs folded after this one still pin "lockfileVersion": 2 (bun-lock.test.ts:1022/2302, catalogs.test.ts:287, overrides.test.ts:247, nested-overrides.test.ts:1481/1513, bun-install-registry.test.ts:1293/1360/6264, migrate.test.ts:2330, minimum-release-age.test.ts:3109, bun-install.test.ts:12035). Either pull this one out and land it or #36464 on its own after the decision, or re-snapshot the siblings inside this commit.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed this is a release decision, not something the fold should make by accident. The sibling snapshots are already re-stamped to 1 inside the fold (that was fixlv in the fixups), so the branch is internally consistent either way; pulling #38741 back out is one revert plus re-stamping. I've called it out at the top of the description as needing a maintainer decision and noted that it obsoletes #36464.

Comment thread src/runtime/cli/pm_licenses_command.rs Outdated
@@ -715,7 +731,7 @@ fn print_text(entries: &[Entry], long: bool, checked: usize) {
"<d>{}<r> {}<d>@{}<r>",
if last { "└──" } else { "├──" },
BStr::new(&entry.name),
BStr::new(&entry.version)
bun_core::fmt::EscapeControlChars(redacted(BStr::new(&entry.version)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entry.name on the line above is not escaped (the version is). Same drop as why_command.rs:479.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9f1c929.

/// is reported and left as it was rather than failing the command.
pub fn write_migrated_root(manager: &mut PackageManager) {
if let Some(root) = take_migrated_root(manager) {
let _ = write_target(manager, &root);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the write result discarded, bun pm migrate on a read-only package.json exits 0 having written bun.lock but not the workspaces/overrides/catalog it moved out of pnpm-workspace.yaml. The next plain bun install rebuilds those from the package.json it can read (install_with_manager.rs:336-343) and drops the migrated pins without a word. #38754's own test pins the opposite contract (exit 1, no bun.lock). Pick one, and add a test that runs the follow-up bun install and --frozen-lockfile from the half-migrated tree.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with #38754's contract. 9f1c929: write_migrated_root now runs immediately before the migrated lockfile is saved in all four places (both install_with_manager save paths, bun pm migrate, bun pm trust) and exits 1 if the write fails, so a package.json that cannot take the moved fields fails the command before a bun.lock that depends on them exists. The read-only test asserts exit 1, package.json untouched, no bun.lock, no copied … line, and then chmods back and checks the same bun install completes the move and resolves the workspace link. Verified as a non-root user locally.

bun_sys::FileLockMode::Exclusive,
/* nonblocking */ true,
) {
let _ = bun_sys::Dir::cwd().delete_tree(tempdir_name);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.bunx-lock lives inside tempdir_name, and delete_tree retries on ENOTEMPTY, so a second bun x that just did O_CREAT on a fresh .bunx-lock in the half-deleted directory passes the identity check and then has its lock file unlinked from under it. Two bun x pkg after the 24h expiry can both install "under lock". Putting the lock next to the cache dir instead of inside it removes the race and the identity check.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9f1c929 moves the lock to <bunx cache dir>.lock (a sibling, so delete_tree never touches it), opens it with O_NOFOLLOW, and on POSIX refuses it unless it is a regular file owned by the current uid; the inode identity re-check and retry loop are gone. bunx / bunx-concurrent pass.

// The token only travels together with its registry.
if self.options.did_override_default_scope || !scope.token.is_empty() {
script_env.put(b"BUN_CONFIG_REGISTRY", scope.url.href())?;
if !scope.token.is_empty() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the default scope's URL and bearer token cross into the nested install. _auth/username:_password basic auth (scope.auth, scope.user), scoped registries, url_auth, ca/cafile, minimumReleaseAge and the security scanner do not, and the child then reads the checkout's own .env/.npmrc/bunfig. So a project on basic auth gets a whole-install failure from the child's 401, and a project with minimumReleaseAge gets devDependencies of the git dep resolved without it. Either forward the install config wholesale or say in the docs exactly what is inherited, with tests for token, _auth, scoped registry and minimumReleaseAge reaching the child.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#38810 is no longer in the fold (dropped at Jarred's request — nested install before prepare is rare to need and a lot of surface); this file is back to its pre-#38810 shape.

@@ -222,6 +228,128 @@ registry = http://localhost:${registry.port}/
const result = await publishDryRun(String(dir), { XDG_CONFIG_HOME: "" });
expect(result).toEqual(usesRegistry(1));
});

// npm's `userconfig` option: the path of the per-user .npmrc, settable through
// NPM_CONFIG_USERCONFIG like any other npm config option.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is #38047 at head 507c356173, the state I requested changes on 2026-08-14. The two tests asked for there are not in the fold: userconfig pointing at a missing file must end in "missing authentication" rather than falling back to ~/.npmrc, and a relative value run from a workspace member dir must resolve against the member dir. Both fit the existing publishDryRun helper.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added both in 9f1c929 with the existing publishDryRun helper: NPM_CONFIG_USERCONFIG naming a missing file ends in missing authentication with ~/.npmrc present, and a relative value run from a workspace member resolves against the member dir (root ci.npmrc = registry 1, pkg/ci.npmrc = registry 2, expects 2). Both pass on the folded #38047 code as-is.

…get_or_put_value for ids, tighter helpers

(cherry picked from commit 20cf6c9b56b59a5bd0157b279d2420d0d6b0f155)
…orm_list takes the optional field, tuple for the folder-in-dependent case, clippy fix

(cherry picked from commit 30671fe6c180a317f8228d78e8aa19a483933f68)
(cherry picked from commit 8233267302fb18a1d9709028a1fe6ce16aaf921b)
(cherry picked from commit 870ea6cecf7518598d82d62d947a3843b09df6e4)
(cherry picked from commit 8494fc54d3afa52f53274df5f143ddba2c7357de)
- yarn.rs: os/cpu fold into Negatable fields at parse time (platform_list
  gone); dependency sections write straight into entry.dependencies
- pnpm.rs: get_object for the pnpm/workspaces lookups, copy_into_root
  without the get/expect round-trip, root_package_json via entry().ok()
- CatalogMap: back to a split borrow (get_or_put_named_group) instead of
  take-and-restore of the default catalog
- npm_lock.rs: keep FolderInDependent as a named struct
- PackageJSONEditor: test the catalog tag before the resolution lookup

No-Verification-Needed: worktree has no build; install tests run centrally by the integrator
(cherry picked from commit a1c29a31366a0e9e3bbb75dc6c4fafe5feed9810)
…AN, root package.json write error wording (#38754 x #38804), bun update keeps a still-covering >= range (#38185)
…ontract), bunx lock next to the cache dir with O_NOFOLLOW + owner check, escape names in bun why / pm licenses, userconfig tests for #38047, strings::split lint
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

@alii thanks for pulling this apart. Status against your summary:

…too (test), mordant findings (dependency_buf naming, one tag->name helper, prune init without bare bools)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants