Skip to content

install: keep a bundled file: dependency bundled when installing from bun.lock - #42884

Merged
Jarred-Sumner merged 3 commits into
mainfrom
robobun/13c4c301/bundled-file-dep-lockfile
Sep 17, 2026
Merged

Jarred-Sumner merged 3 commits into
mainfrom
robobun/13c4c301/bundled-file-dep-lockfile

Conversation

@robobun

@robobun robobun commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun install from an existing bun.lock installs a bundled file: dependency a second time. The second copy lands as self-referencing symlinks (package.json -> package.json) inside the bundling package's node_modules, and the import fails with Cannot find module. The first bun add works. Fixes Frozen install creates self-referencing symlinks for a bundled file #42882.
  • The bundle-root pre-scan in src/install/lockfile/bun.lock.rs:2545 read the info object at index 2 of every package entry. That index is right only for an npm resolution ([res, registry, {info}, integrity]). A file:, git, tarball or workspace resolution has no registry string, so its info object is at index 1 and the "bundled": true marker was never seen.
  • The symlink install fallback in src/install/PackageInstall.rs:1803 linked an entry to its own basename when the destination file already existed. That is where the package.json -> package.json links come from.

Fix

  • The pre-scan takes the first object element of the entry as the info object. This matches what the main parse loop reads for every resolution kind.
  • The symlink retry after EEXIST links to the same cache path as the first attempt.
  • With the marker seen, parse_append_dependencies sets Behavior::BUNDLED on the edge and the installer skips it, as it does on a fresh resolve. The bundled copy that the tarball shipped stays in place.
  • Verified: two new tests in test/cli/install/bun-install-registry.test.ts (bundledDependencies block). Both fail on the released bun. Also ran all of bun-install-registry.test.ts, bun-lock.test.ts and isolated-install.test.ts.

Background

  • A bundled dependency ships inside its parent's tarball under node_modules/. The installer must not install it again. bun.lock records this with "bundled": true on the dependency's own entry, and the parser marks the parent's edge BUNDLED from that entry.
  • A transitive file: dependency of an npm package is installed with the symlink method: each file in the destination is a symlink to the cached source (PackageInstall.rs:2316). When node_modules is new, the installer skips the delete before install, so files the tarball already shipped are still there and the EEXIST path runs.
Notes

New fixture bundled-file (generated by test/cli/install/registry/packages/create-bundled-file-packages.ts):

  • 1.0.0 depends on bundled-file-dep via file:vendor/bundled-file-dep and bundles it. The tarball ships vendor/bundled-file-dep/ and node_modules/bundled-file-dep/. The test installs, checks the lockfile carries "bundled-file/bundled-file-dep": ["bundled-file-dep@file:vendor/bundled-file-dep", { "bundled": true }], deletes node_modules, installs with --frozen-lockfile, and checks the install reports 1 package and the bundled files are regular files. Released bun reports 2 packages and leaves self-referencing links.
  • 2.0.0 ships the same files but does not bundle the dependency. The test checks the file: dependency is linked to the vendor copy and require("bundled-file") works. Released bun produces package.json -> package.json.

The same two bugs exist in the Zig source before the Rust port, which matches the report that 1.2.19 also fails.


no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/install/bun-install-registry.test.ts

… bun.lock

The bundle-root pre-scan in the bun.lock parser read the info object at
index 2 of every package entry. That index is right only for an npm
resolution. A file:, git, tarball or workspace resolution has no registry
string, so its info object is at index 1 and the `"bundled": true` marker
was never seen. The installer then installed the bundled dependency a
second time on top of the copy the tarball shipped.

That second install went through the symlink method. When a file already
existed, the fallback linked the entry to its own basename, which produced
self-referencing symlinks such as `package.json -> package.json`. The
retry now links to the same cache path as the first attempt.

Fixes #42882
@coderabbitai

coderabbitai Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: d7fec795-8ade-4c0f-990d-7d9408aa6841

📥 Commits

Reviewing files that changed from the base of the PR and between 769c998 and 5a614b4.

📒 Files selected for processing (1)
  • src/install/lockfile/bun.lock.rs

Included review availability: Your plan provides up to 10 included reviews per hour; 1 remains after this review.


Walkthrough

The installer now detects bundled metadata across package-info layouts and retries replacement symlink creation correctly. Registry fixtures and regression tests cover bundled and unbundled file: dependencies during frozen reinstalls.

Changes

Bundled file dependency installation

Layer / File(s) Summary
Bundled metadata detection
src/install/lockfile/bun.lock.rs
Bundled package detection scans package-info entries for the first object with "bundled": true.
Symlink replacement handling
src/install/PackageInstall.rs
Unix retries use the computed target path. Windows removes an existing destination before retrying symlink creation.
Regression fixtures and coverage
test/cli/install/registry/packages/create-bundled-file-packages.ts, test/cli/install/registry/packages/bundled-file/package.json, test/cli/install/bun-install-registry.test.ts
Registry fixtures define bundled and unbundled package versions. Tests verify frozen reinstalls, file preservation, symlink targets, install counts, lockfile metadata, and runtime resolution.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 5a614

The reported self-referencing symlink failure is the behavior this change targets, and no additional concrete regression is established by the available evidence.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address #42882. bun.lock parsing now detects bundled metadata across resolution layouts. The symlink retry now uses the computed cached target instead of the destination basename. The …
Out of Scope Changes check ✅ Passed The source changes implement bundled dependency detection and correct symlink targets. The tests and registry fixtures provide regression coverage for #42882 and related file: dependency behavior. N…
Title check ✅ Passed The title clearly describes the primary fix: preserving bundled file dependencies during installation from bun.lock. It is specific and related to the changeset, although slightly repetitive.
Description check ✅ Passed The description explains the problem, fix, background, test coverage, and verification results. It does not use the template headings exactly, but it provides the required information and is mostly co…

Warning

Git: CodeRabbit could not clone the repository, so clone-backed analysis was skipped and this review may be incomplete. Verify repository clone access, such as SSH credentials, before requesting another full review. If clone access is intentionally unavailable, use path_filters to narrow the review scope.


Comment @coderabbitai help to get the list of available commands.

Comment thread src/install/lockfile/bun.lock.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked two things and ruled them out: the find_map pre-scan in bun.lock.rs cannot pick the wrong element, since every lockfile package tuple shape has exactly one object (resolution, registry and integrity are all strings); and the new Windows EEXIST arm in PackageInstall.rs calls sys::unlink_w(&WStr) and sys::symlink_w(dest, src, options) with signatures that match src/sys/lib.rs, so it should type-check on the Windows target.

Extended reasoning...

Three confirmed findings are already posted inline (discarded unlinkat result masking EISDIR on the retry path, an unchecked head2 write for long cache paths on macOS, and the pre-scan not recovering bundled: true markers that older bun versions already dropped from re-saved lockfiles), so this note only records what else was examined. The lockfile pre-scan's first-object selection was checked against the tuple layouts the writer emits and is unambiguous. The Windows EEXIST branch was checked for symbol existence and argument types against src/sys/lib.rs (symlink_w(dest: &WStr, target: &WStr, options) and unlink_w(from: &WStr)), and the unix retry now uses the target ZStr built at line 1793 that is in scope at the retry site. This is informational; a human should still weigh the inline findings before merging.

2 verified lower-impact observations (convention, logging or cleanup points) were not posted.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 src/install/PackageInstall.rs — macOS users with a deep file: dependency path get a Rust index-out-of-bounds panic and a crash report from bun install instead of an ENAMETOOLONG error. PackageInstall.rs:1788-1791 writes cache path plus entry.path into head2 with no length check; the buffer is a path_buffer_pool buffer of MAX_PATH_BYTES, which is 1024 on macOS (bun_core/util.rs:685). The Windows arm at :1817-1821 has the bounds check and returns ENAMETOOLONG. Fix: add the same entry.path.len() > head2.len() - to_copy_into2_offset check to the unix arm before the copy so the failure is a recoverable install error on every platform.

    Extended reasoning...

    install_with_symlink at :1735 takes buf2 from bun_paths::path_buffer_pool::get(), sized MAX_PATH_BYTES. On Linux that is 4096, on macOS 1024 (bun_core/util.rs:676-686). :1740 writes the absolute cache directory of the file: dependency into it, e.g.…

    Verification: pre-existing. Triggering condition: a file: dependency installed via Method::Symlink whose absolute cache path plus a relative entry path exceeds MAX_PATH_BYTES (1024 on macOS, 4096 on Linux per /home/claude/bun/src/bun_core/util.rs:676-686). Mechanism verified: /home/claude/bun/src/install/PackageInstall.rs:1735 takes buf2 from bun_paths::path_buffer_pool::get() (a `PathBuffer(pub [u8;…

Comment thread src/install/PackageInstall.rs
Comment thread src/install/lockfile/bun.lock.rs
@robobun

robobun commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator Author

On the macOS path length note: the unix arm of the symlink copy loop writes the cache path plus the entry path into a pool buffer with no length check. That is pre-existing and not part of this bug, so I am leaving it out of this PR to keep the diff at the two reported causes. The two inline threads are answered in place.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code review found no issues

No high-confidence issues detected in this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Frozen install creates self-referencing symlinks for a bundled file

2 participants