Skip to content

install: read file: tarballs relative to the file: folder package that declares them - #39017

Closed
robobun wants to merge 5 commits into
mainfrom
farm/1d4e54b0/folder-dep-local-tarball-base
Closed

install: read file: tarballs relative to the file: folder package that declares them#39017
robobun wants to merge 5 commits into
mainfrom
farm/1d4e54b0/folder-dep-local-tarball-base

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • A file: folder dependency whose own package.json declares a local tarball fails to install (released 1.4.0 canary and main, both linkers): project package.json {"dependencies":{"lib":"file:./vendor/lib"}}, vendor/lib/package.json declaring "tool": "file:./tool.tgz", vendor/lib/tool.tgz present:
    error: ENOENT extracting tarball from tool
    error: tool@file:./tool.tgz failed to resolve
    
  • The path is read as <project>/tool.tgz. If that file happens to exist it is installed as tool instead of the folder's copy, with no error. The directory form of the same declaration ("tool": "file:./tool") is already resolved relative to vendor/lib.
  • Cause: enqueue_local_tarball (src/install/PackageManager/PackageManagerEnqueue.rs) picks the directory a local tarball path is relative to, and the only declarer it looked at was a workspace (get_workspace_pkg_if_workspace_dep). Every other declarer, including a file: folder package, fell through to the top-level dir.
  • The same choice was also wrong in the other direction: a root overrides / resolutions entry or catalog entry pointing a dependency at file:./x.tgz was read relative to the workspace when the dependency it applied to was declared by a workspace member, so overrides: { bar: "file:./bar.tgz" } with bar.tgz in the project root failed with the same ENOENT as soon as a workspace depended on bar. This is Feature: Workspace overrides with file: paths should resolve relative to workspace root's package.json #25835 (overrides) and Catalog entries with file: relative paths fail when referenced from workspace packages #25752 (catalogs); both reproduce as reported on the released build and install with this change. The directory form of an override is already resolved relative to the project (Folder arm of get_or_put_resolved_package).

Fixes #25835
Fixes #25752

Fix

  • enqueue_local_tarball now takes the base directory from local_tarball_base_dir: the directory of the declaring package when it is a workspace or a file: folder package and that package's own specifier is the tarball path being read; the top-level dir in every other case.
  • Why the declaring package: a path in a package.json means a file next to that package.json, which is what npm does for file: and what bun already does for workspace declarers and for the directory form. A file: folder package is read from the project like a workspace is, and its Resolution::Folder payload is its directory relative to the top-level dir (folder_resolver.rs, NewResolver { folder_path: rel }), the same shape as a workspace's Resolution::Workspace payload, so both are joined the same way. The only other Resolution::Folder packages are the stubs created for file: directories declared by something other than the root or a workspace (Folder arm of get_or_put_resolved_package); those carry no dependency list, so they are never the declarer of an edge.
  • Why the "own specifier" condition: overrides, resolutions and catalogs are only parsed from the root package.json (Package.rs, FEATURES.is_main), and applying one leaves the declaring package's stored edge untouched (the replacement is local to enqueue_dependency_with_main_and_success_fn). So when the stored edge's specifier is not the path being read, the root wrote the path and the top-level dir is the only directory it can mean. Without this condition, root overrides applied to a folder-declared dependency, which work today only because of the bug, would start being read from the folder.
  • Why it is decided from the edge and not from the resolve pass's version_was_replaced: enqueue_local_tarball is also reached from enqueue_tarball_for_reading when a project with a bun.lock is installed into an empty cache. The lockfile row keeps the path as declared ("tool": ["bar@./tool.tgz", ...] under "lib": [..., { "dependencies": { "tool": "file:./tool.tgz" } }]), and the edge's declarer and specifier are available there too, so both passes compute the same directory. Lockfile format is unchanged and existing lockfiles keep working; the path in the row is joined onto a different directory only for declarers that previously failed or installed the wrong file.
  • Declarers extracted from the cache (registry, git, tarball packages) still fall through to the top-level dir, as before; install: refuse local tarball dependencies declared by packages installed from the cache #38986 is changing what happens to those separately. Lockfile::get_parent_pkg_of_dependency is the same helper that PR adds.
  • Left as is: the lockfile identity of a local tarball is still the path as written (bar@./tool.tgz), so two project packages declaring the same relative path to two different files still share one row and one read, the limitation workspaces already have. A package.json inside a folder dependency that worked around this bug by writing a project-relative path (file:./vendor/lib/tool.tgz) will now need the path relative to itself, which is what npm requires for it as well.
  • Verified with test/cli/install/bun-install.test.ts, describe("file: tarball declared by a file: folder dependency"): the folder's tarball is installed with the hoisted and with the isolated linker, and a root override supplying the path is read from the project. Each test plants a different tarball at the other candidate path and runs a fresh install followed by --frozen-lockfile into an emptied cache, so both the resolve pass and the install from bun.lock have to read the right file. test/cli/install/bun-workspaces.test.ts, relative tarballs > from a root override / catalog entry applied to a workspace dependency, covers Feature: Workspace overrides with file: paths should resolve relative to workspace root's package.json #25835 and Catalog entries with file: relative paths fail when referenced from workspace packages #25752 the same way. The four folder/workspace tests install the wrong tarball without the src/ change (checked against a debug build of main and against the released build); the override-on-folder test passes before and after and pins that case.
  • Also run with this change: bun-workspaces.test.ts (74 pass), overrides.test.ts + nested-overrides.test.ts (151 pass), bun-lock.test.ts (40 pass), bun-install.test.ts -t "tarball|tgz|file:|folder|override|resolutions" (51 pass; should treat non-GitHub http(s) URLs as tarballs fails identically on the released build, it needs network), isolated-install.test.ts, bun-add.test.ts and bun-install-registry.test.ts filtered to tarball/file tests (all pass). cargo clippy -p bun_install is clean.

Background

  • A file: dependency on a directory is a Tag::Folder dependency; one on a .tgz is a Tag::Tarball dependency with a local URI. The latter resolves to Resolution::LocalTarball(<path as written>); the path string is both the task id for reading it and the package's identity in bun.lock.
  • bun reads the package.json of the root, of workspace members and of file: folder dependencies from disk (Package::parse), and stores for each of the latter two a Resolution::Workspace / Resolution::Folder whose payload is the package directory relative to the top-level dir. Packages from the registry, git or a tarball get their dependency lists from the manifest or the extracted archive instead and have no directory in the project while they resolve.
  • Dependency edges live in one flat buffer and every package owns a contiguous slice of it, which is how the package that declared an edge is found. The edge stores the specifier as declared; overrides, resolutions and catalogs replace it only for the duration of resolving that edge.
  • enqueue_local_tarball is called from two places: the Tarball arm of the resolve pass (no lockfile row yet, the tarball is read to learn the package's name and dependencies) and enqueue_tarball_for_reading during install (a row exists in bun.lock, but the extracted package is missing from the cache). It computes the on-disk path on the main thread and hands it to a thread pool task, which only reads the file.

…t declares them

A local tarball dependency declared in the package.json of a file: folder
dependency was read relative to the project directory instead of the
folder, so the install failed with ENOENT, or installed a same-named
tarball from the project directory when one existed. Only workspace
declarers had their directory joined in.

enqueue_local_tarball now asks local_tarball_base_dir for the base: the
directory of the declaring workspace or file: folder package when that
package's own specifier is the tarball path, and the top-level dir
otherwise. The second case covers paths substituted from the root
package.json (overrides, resolutions, catalogs), which were previously
read relative to the workspace when applied to a workspace dependency.
Both the resolve pass and the install from bun.lock go through the same
function, so they read the same file.
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 59bc8707-cc9a-43b5-a565-0d7200b3d8dc

📥 Commits

Reviewing files that changed from the base of the PR and between 732491c and 5ce5696.

📒 Files selected for processing (5)
  • src/install/PackageManager/PackageManagerEnqueue.rs
  • src/install/PackageManagerTask.rs
  • src/install/lockfile.rs
  • test/cli/install/bun-install.test.ts
  • test/cli/install/bun-workspaces.test.ts

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

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Status: fix pushed, waiting on CI and review.

Reproduced on the released build and on a debug build of main with a project depending on file:./vendor/lib whose package.json declares "tool": "file:./tool.tgz" next to itself: bun install fails with error: ENOENT extracting tarball from tool, and installs <project>/tool.tgz instead when that file exists. The same base-directory choice is behind #25835 and #25752 (root override / catalog tarball paths read relative to the workspace member that uses them); both shapes reproduce on the released build and install with this change. The new tests in test/cli/install/bun-install.test.ts and test/cli/install/bun-workspaces.test.ts install the wrong tarball without the src/ change and pass with it.

@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. Feature: Workspace overrides with file: paths should resolve relative to workspace root's package.json #25835 - A root overrides entry pointing a workspace member's dependency at file:vendored/x.tgz was joined onto the workspace member's directory instead of the project root; this PR's "own specifier" guard sends it to the top-level dir, and the case is covered verbatim by the new bun-workspaces.test.ts test.
  2. Catalog entries with file: relative paths fail when referenced from workspace packages #25752 - The pkg-b half (a catalog: entry whose value is file:./vendored/pkg-b-1.0.0.tgz, referenced from a workspace package) fails for exactly the reason this PR fixes: the old code joined get_workspace_pkg_if_workspace_dep's directory, while the new local_tarball_base_dir sees the stored edge's specifier is not the tarball path and resolves against the project root where the catalog was declared.

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #25835
Fixes #25752

🤖 Generated with Claude Code

Comment thread src/install/PackageManager/PackageManagerEnqueue.rs Outdated
Comment thread src/install/PackageManagerTask.rs Outdated
Comment thread src/install/PackageManager/PackageManagerEnqueue.rs Outdated
Comment thread src/install/PackageManagerTask.rs Outdated
Comment thread src/install/PackageManagerTask.rs Outdated
@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Checked the two issues suggested above: #25835 (root overrides entry pointing a workspace member's dependency at a local tarball) and #25752 (root catalog entry doing the same) both reproduce as reported on the released build and install with this change, since in both cases the edge's own specifier is not the tarball path, so the path is now read relative to the project. Added the catalog shape next to the override one in bun-workspaces.test.ts (relative tarballs) and the Fixes lines to the description.

The two flagged doc comments are gone: the condition they explained is now the declared_by_parent binding in local_tarball_base_dir with a one-line note, and the tarball_path field doc is a single line.

@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.

I reviewed this PR and didn't find any bugs. Because it changes user-visible path-resolution semantics in bun install (including an acknowledged break for folder packages that worked around the old behavior with project-relative paths), a human look would still be worthwhile.

What was reviewed:

  • Traced both call sites of enqueue_local_tarball (resolve pass and enqueue_tarball_for_reading) — the declared_by_parent guard correctly falls through to the top-level dir for overrides/catalogs on both paths, since the stored edge keeps the original specifier.
  • Checked root-declared tarballs still take the normalize=true path (ResolutionTag::Root is not matched, same as before).
  • get_parent_pkg_of_dependency is the same O(packages) shape as the get_workspace_pkg_if_workspace_dep it generalizes; no new perf concern.
  • Tests plant decoy tarballs at the wrong candidate path and cover both linkers plus --frozen-lockfile into an empty cache.
Extended reasoning...

Overview

The PR reworks how enqueue_local_tarball picks the base directory for a file:-tarball path. The old code only recognized workspace declarers and always joined onto the workspace directory; everything else (including file: folder packages) used the project root. The new local_tarball_base_dir helper (a) extends the declarer lookup to ResolutionTag::Folder, and (b) adds a declared_by_parent guard so that when the stored edge's specifier is not the tarball path being read (i.e. an override/resolution/catalog supplied it), the top-level dir is used instead. A new Lockfile::get_parent_pkg_of_dependency generalizes the existing per-package dependency-range scan. Five tests are added across bun-install.test.ts and bun-workspaces.test.ts; the LocalTarballRequest.tarball_path doc comment is shortened.

Security risks

None identified. The change only affects which on-disk file inside the user's own project tree is read for a locally-declared tarball; no new untrusted-input parsing, no network, no privilege boundary.

Level of scrutiny

Medium-high. This is core bun install path resolution and the PR itself calls out an intentional behavior change: projects that worked around the old bug by writing project-relative paths inside a folder dependency's package.json will now need folder-relative paths. That is the correct npm-compatible behavior, but it's a user-visible break that a maintainer should sign off on. The interaction with #38986 (which is separately changing what happens for registry/git/tarball declarers and adds the same get_parent_pkg_of_dependency helper) also deserves a human eye for coordination.

Other factors

  • The comment-cop bot fired several times on earlier revisions; the follow-up commits (ef99947, 223e0a4, 5ce5696) shortened the flagged comments and the current diff looks clean on that front.
  • Test coverage is strong: each new test plants a different tarball at the wrong candidate path so the failure mode is a wrong install rather than an ENOENT, and each runs both a fresh install and --frozen-lockfile into an emptied cache so both code paths into enqueue_local_tarball are exercised.
  • I verified the declared_by_parent guard works on the enqueue_tarball_for_reading path too: the stored edge for an overridden/catalog dep keeps its original tag (Npm/Catalog), so the tag check short-circuits before the string compare.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 11:12 AM PT - Aug 15th, 2026

@robobun, your commit 5ce5696 has 1 failures in Build #98048 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 39017

That installs a local version of the PR into your bun-39017 executable, so you can run:

bun-39017 --bun

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Folded into #38867.

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

Labels

Projects

None yet

2 participants