Skip to content

fix(linker): resolve git deps in global virtual store - #857

Merged
jdx merged 7 commits into
aubepkg:mainfrom
rubnogueira:fix/gvs-exotic-bug
Jun 13, 2026
Merged

jdx merged 7 commits into
aubepkg:mainfrom
rubnogueira:fix/gvs-exotic-bug

Conversation

@rubnogueira

@rubnogueira rubnogueira commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

fix(global-virtual-store): materialize git/remote-tarball deps in the shared store and key dependent siblings correctly

Summary

With enableGlobalVirtualStore: true, a registry package that depends on a git or remote-tarball dependency gets a dangling sibling symlink to that dependency. At runtime the dependency resolves to nothing, so any code reaching into it throws. pnpm links the same graph correctly.

The canonical trigger is request-promise → request-promise-core, which does require('request').Request.prototype when request is sourced from git:

TypeError: Cannot read properties of undefined (reading 'prototype')
    at .../request-promise-core@1.1.4_request@git+<hash>_.../configure/request2.js

(Depending on how the dep was recorded, the same defect can also surface as Error: Cannot find module 'request'.)

Root cause

Two independent defects in the isolated linker's global-virtual-store (GVS) path:

  1. Shareable local sources were never materialized into the shared store. git / remote-tarball packages are content-pinned and shared across projects exactly like registry packages, but they were only ever materialized into the per-project .aube/. A registry dependent living in the shared store points its dependency siblings at the hashed global path, so that sibling symlink dangled.

  2. Dependents keyed the sibling under the wrong identity. pnpm records a git/tarball dep inside a snapshot's dependencies: map by its resolved spec (<url>#<sha> / tarball URL), but keys the package itself under the short name@git+<hash> / name@url+<hash> form. The linker's naive format!("{name}@{value}") sibling lookup therefore pointed at a virtual-store path that was never materialized.

Changes

  • aube-lockfile/src/source.rs — add LocalSource::is_globally_shareable() (Git | RemoteTarball), with rationale on why these are shared-store-eligible while file:/link:/portal:/exec: stay per-project.
  • aube-linker/src/materialize.rs
    • add shared_local_dep_path() — canonicalizes a recorded git/tarball spec to the same dep_path the parser keys the package under (promoting #committish → resolved for git so the hash matches; strips pnpm's (peer@ver) suffix).
    • add ensure_shared_local_in_global_store() — materializes a shareable local source into the shared store and symlinks .aube/<dep_path> at it, clearing stale per-project dirs/links left by older aube versions.
    • fix the materialize_into sibling loop to use shared_local_dep_path.
  • aube-linker/src/link.rs — both link_all and link_workspace now route shareable local sources through ensure_shared_local_in_global_store when GVS is enabled.
  • aube-linker/src/hoisted.rs — apply the same dep_path canonicalization so the hoisted layout doesn't silently drop the dep's subtree.

Behavior (before → after)

For request-promise-core's request sibling under GVS:

  • Before: request -> ../../request@https+++codeload…/node_modules/request → dangling.
  • After: request -> ../../request@url+<hash>/node_modules/request (or git+<hash>) → resolves.

Reproduction

A minimal project that forces request-promise onto a git-sourced request:

pnpm-workspace.yaml

enableGlobalVirtualStore: true
catalog:
  request: "github:request/request#<commit-sha>"
packageExtensions:
  request-promise@*:
    dependencies:
      request: "github:request/request#<commit-sha>"

package.json deps: { "request": "catalog:", "request-promise": "4.2.6" }, then:

aube install
node -e "require('request-promise'); console.log('ok')"
  • Before: throws (dangling request).
  • After: prints ok.

Testing

  • 6 new unit tests for shared_local_dep_path (github shorthand → git+, shorthand/URL convergence, committish→resolved promotion, codeload tarball → url+, peer-suffix stripping, None for non-shareable specs).
  • Verified end-to-end on an isolated store: stock build leaves a dangling sibling and Node fails; patched build resolves it and request-promise loads.
  • Full suites green: 89 aube-linker + 285 aube-lockfile tests, no regressions.

Compatibility / risk

  • Only affects the GVS code path; non-GVS installs are unchanged.
  • Self-healing: ensure_shared_local_in_global_store clears stale per-project entries from older aube versions on the next install.

Repro: https://github.com/rubnogueira/aube-repro-gvs-exotic-hash

Summary by CodeRabbit

  • Bug Fixes

    • Prevented git and remote-tarball dependencies from being silently dropped due to key mismatches during transitive resolution.
    • Ensure consistent symlink targets and removal of stale per-project entries when using a global virtual store.
    • Fail loudly when a globally-shared source requires a package index but none is available.
  • New Features

    • Promote git/remote-tarball local sources into a shared global virtual store with freshness checks and automated materialization.
    • Add content fingerprints so shared source-backed packages with differing on-disk content get distinct cache/virtual-store identities; cached hashes are avoided when these fingerprints are present.
  • Tests

    • Added regression test ensuring missing index errors for shareable source deps.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2c65f875-2918-4bf6-ba7e-a4f93b0d666c

📥 Commits

Reviewing files that changed from the base of the PR and between 2991bd6 and 44097e1.

📒 Files selected for processing (4)
  • crates/aube-linker/src/link.rs
  • crates/aube-linker/src/tests.rs
  • crates/aube-lockfile/src/source.rs
  • crates/aube/src/commands/install/link.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/aube-lockfile/src/source.rs
  • crates/aube/src/commands/install/link.rs
  • crates/aube-linker/src/link.rs

📝 Walkthrough

Walkthrough

Classify Git/RemoteTarball as globally shareable, derive canonical dep_path keys for them, add index content fingerprinting, thread content hashes into graph-hash computation, materialize shared-store content with symlink wiring, and route linking/hoisting to use the shared global virtual store when enabled.

Changes

Global Virtual Store for Git and Remote-Tarball Dependencies

Layer / File(s) Summary
Source classification contract
crates/aube-lockfile/src/source.rs
LocalSource::is_globally_shareable() returns true only for Git and RemoteTarball; false for file:, link:, portal:, exec:.
Shared local dependency path normalization & tests
crates/aube-lockfile/src/source.rs, crates/aube-lockfile/src/lib.rs
shared_local_dep_path() canonicalizes pnpm-recorded dep values to share-store dep_path keys (peer-suffix stripping, committish→resolved promotion, codeload mapping); tests added and helper re-exported.
Graph hashing with content fingerprints
crates/aube-lockfile/src/graph_hash.rs
Adds ContentHashFn, uses canonical child_dep_path for URL-shaped children, threads content_hash into recursive hash calc, and appends :content:<hex> to full_pkg_id when present; tests added.
Package index content fingerprinting
crates/aube-store/src/index.rs, crates/aube-store/src/lib.rs
Adds index_content_fingerprint() to deterministically fingerprint a PackageIndex and re-exports it from aube-store.
Install-phase integration for content-hashed graph computation
crates/aube/src/commands/install/link.rs
run_link_phase builds per-dep content hashes for globally-shareable sources, restricts prewarm reuse when content hashes exist, and invokes compute_graph_hashes_full with both patch and content callbacks.
Shared store materialization & hoisted key alignment
crates/aube-linker/src/materialize.rs, crates/aube-linker/src/hoisted.rs
Adds Linker::ensure_shared_local_in_global_store() to reconcile .aube/<dep_path> with shared virtual-store, materialize or prune as needed, create directory symlinks; hoisted planning and transitive symlink targets use shared_local_dep_path() for git/tarball keys (fallback to name@ver).
Link flow routing to shared vs project-local storage
crates/aube-linker/src/link.rs
link_all and link_workspace call ensure_shared_local_in_global_store() and skip per-project .aube/ materialization for globally-shareable local sources when global virtual store mode is enabled; comments updated.
Regression test
crates/aube-linker/src/tests.rs
Adds gvs_shareable_source_dep_without_index_errors_loudly asserting missing PackageIndex yields Error::MissingPackageIndex for computed dep_path.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • jdx/aube#809: Also modifies plan_importer / hoisting logic in crates/aube-linker/src/hoisted.rs and may overlap with transitive placement changes.
  • jdx/aube#858: Related changes to graph hashing and source-backed dep identity handling in aube-lockfile/src/graph_hash.rs.

🐰 I hop through .aube with careful art,

Git and tarballs now know their part.
Keys aligned, no packages stray,
Symlinks point the shared-store way,
Happy rabbit, build and play!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(linker): resolve git deps in global virtual store' accurately describes the main change—fixing materialization and canonicalization of git/remote-tarball dependencies in the global virtual store to resolve dangling sibling symlinks.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@greptile-apps

greptile-apps Bot commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two independent defects in the isolated linker's global-virtual-store (GVS) path that caused dangling sibling symlinks when a registry package depended on a git or remote-tarball dep: (1) shareable local sources were never materialized into the shared store, and (2) dependents looked up the sibling by its raw resolved spec rather than the hashed name@git+<hash> / name@url+<hash> key the parser actually inserted.

  • Adds LocalSource::is_globally_shareable() and shared_local_dep_path() to canonicalize the URL-recorded dep value to the same dep_path key the parser uses; both the sibling-symlink writer and graph hasher now call this helper so content fingerprints and engine taint cascade correctly into parent hashes.
  • Adds ensure_shared_local_in_global_store which materializes git/tarball sources into the shared virtual store with a per-project .aube/<dep_path> symlink, including self-healing cleanup of stale entries.
  • Adds index_content_fingerprint threaded through compute_graph_hashes_full so the same commit installed with vs. without prepare lands at distinct GVS paths.

Confidence Score: 5/5

Safe to merge — the fix is scoped entirely to the global-virtual-store code path; non-GVS installs are structurally unchanged.

The implementation correctly mirrors pnpm::read::push_direct keying at every call site, backward compatibility is preserved via a no-op content hash delegation, and the test suite covers canonical spec forms, content-fingerprint cascade, engine-taint cascade, and the fail-loud contract for missing indices. The only observation is a diagnostic quality nit in stale-entry cleanup with no correctness impact.

No files require special attention; materialize.rs has one minor diagnostic quality note but no correctness issue.

Important Files Changed

Filename Overview
crates/aube-lockfile/src/source.rs Adds is_globally_shareable() predicate and shared_local_dep_path() translation helper; 6 unit tests cover all spec forms.
crates/aube-linker/src/materialize.rs Adds ensure_shared_local_in_global_store and fixes sibling-symlink keying. Core logic sound; minor diagnostic quality concern with silently-ignored try_remove_entry result.
crates/aube-linker/src/link.rs Both link_isolated and link_workspace route globally-shareable source deps through the new function; missing index errors loudly.
crates/aube-lockfile/src/graph_hash.rs Adds compute_graph_hashes_full with content fingerprint support; backward-compatible via no-op delegation.
crates/aube-store/src/index.rs Adds deterministic index_content_fingerprint using sorted BLAKE3 over (path, hex_hash, executable) tuples.
crates/aube/src/commands/install/link.rs Builds content fingerprints and bypasses prewarm reuse when any globally-shareable source dep is present.
crates/aube-linker/src/hoisted.rs Applies shared_local_dep_path canonicalization so URL-recorded git/tarball children are not dropped from the hoisted subtree.
crates/aube-linker/src/tests.rs New test verifies fail-loud behavior for missing indices; existing tests retained.
crates/aube-store/src/lib.rs Re-exports index_content_fingerprint. Trivial change.

Reviews (6): Last reviewed commit: "fix: apply ai review suggestions" | Re-trigger Greptile

Comment thread crates/aube-linker/src/materialize.rs Resolved
@github-actions

Copy link
Copy Markdown

This PR currently has failing checks. If this continues for 7 days, it will be closed automatically.

This is warning day 1 of 7.

Please update the PR when you have a chance. Feel free to reopen or create a new PR if it is closed and you'd like to continue working on it.

This comment was generated by an automated workflow.

@jdx jdx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for digging into this. I think the core linker fix is heading in the right direction: materializing git/remote-tarball deps into the shared GVS and canonicalizing sibling dep paths in materialize.rs / hoisted.rs matches the failure mode described here.

One thing I think still needs fixing before merge: crates/aube-lockfile/src/graph_hash.rs still resolves child deps with raw format!("{alias}@{tail}") lookups in both calc_deps_hash and transitively_requires_build.

For real pnpm lockfile shapes, a git/tarball dependency value can be the resolved URL, while graph.packages is keyed as name@git+<hash> / name@url+<hash>. That means the graph hasher can skip that child. Two consequences:

  • the parent package’s GVS hash does not include the child’s content fingerprint
  • build/engine taint from an allowlisted git/tarball child does not cascade to the parent

I verified this locally by adding URL-shaped regression tests: current PR code keeps the parent hash identical for both content-fingerprint changes and engine changes. Applying the same canonical child dep-path logic in graph_hash.rs makes both tests pass.

Could we move the canonicalization helper into aube-lockfile (or add an equivalent there) and use it from calc_deps_hash, transitively_requires_build, and the linker? I’d also add regression coverage where the parent dependency value is URL-shaped, e.g. https://github.com/request/request.git#<sha> or a codeload tarball URL, not the already-canonical synthetic git+abc.

Minor: the PR title is pretty vague. Something like fix(linker): resolve git deps in global virtual store would fit the repo conventions better.

This comment was generated by Codex.

@rubnogueira rubnogueira changed the title fix: gvs exotic bug fix(linker): resolve git deps in global virtual store Jun 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/aube-lockfile/src/lib.rs (1)

600-603: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Canonicalize URL-shaped children in transitive_closure too.

filter_deps() and subset_to_importer() still rebuild every edge as name@tail. For git / remote-tarball entries, tail is the resolved URL while packages stores the node under shared_local_dep_path(...), so these BFS walks stop early and emit pruned graphs that are missing the whole transitive subtree.

Suggested fix
-                let child_key = format!("{child_name}@{child_version}");
+                let child_key = shared_local_dep_path(child_name, child_version)
+                    .unwrap_or_else(|| format!("{child_name}@{child_version}"));
                 if reachable.insert(child_key.clone()) {
                     queue.push_back(child_key);
                 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube-lockfile/src/lib.rs` around lines 600 - 603, The BFS in
transitive_closure builds child keys as format!("{child_name}@{child_version}")
which fails for git/remote-tarball nodes because packages stores those nodes
under shared_local_dep_path(...) rather than the raw resolved URL; change the
child_key construction in transitive_closure to canonicalize URL-shaped tails:
detect when child_version is a URL/remote-tarball/git-form (using the same
predicate used elsewhere) and instead compute the stored node key with
shared_local_dep_path(child_name, &child_version) (or the existing canonicalizer
used by filter_deps/subset_to_importer), otherwise keep the existing name@tail
form, then use that canonical child_key for reachable.insert and
queue.push_back. Also make the same canonicalization change in filter_deps and
subset_to_importer wherever edges are rebuilt so all BFS/walk code uses the same
stored-key format and the transitive subtree is not pruned.
crates/aube-linker/src/link.rs (1)

165-178: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use aube_dir_entry_name(dep_path) for the single-project local cache probe.

materialize_into(..., false) writes local packages under .aube/<aube_dir_entry_name(dep_path)>, but this fast path still checks .aube/<dep_path> raw. For any dep_path that needs escaping or flattening, warm reruns will miss the existing entry and rematerialize into an already-populated destination. The workspace path below already uses the transformed name, so link_all should match it.

Proposed fix
-            let aube_entry = aube_dir.join(dep_path);
+            let aube_entry = aube_dir.join(self.aube_dir_entry_name(dep_path));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube-linker/src/link.rs` around lines 165 - 178, The existence check
uses the raw dep_path but materialize_into(..., false) writes into the
transformed name, causing false negatives; change the aube_entry construction in
link_all to use aube_dir_entry_name(dep_path) (i.e. replace
aube_dir.join(dep_path) with aube_dir.join(aube_dir_entry_name(dep_path))) so
the fast path probes the same flattened/escaped path materialize_into uses and
correctly increments stats.packages_cached.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aube-lockfile/src/source.rs`:
- Around line 318-320: The current truncation using dep_value.split('(').next()
removes any '(' anywhere; instead only strip pnpm's trailing peer suffix: check
if dep_value ends_with(')') and rfind('(') to get the last '('; if the substring
after that '(' starts_with("peer@") then set classify to the slice before that
'(' otherwise leave classify as dep_value. Update the code that sets classify
(the line using dep_value.split('(').next().unwrap_or(dep_value)) to use this
conditional rfind/starts_with logic so only a terminal "(peer@...)" is removed.

---

Outside diff comments:
In `@crates/aube-linker/src/link.rs`:
- Around line 165-178: The existence check uses the raw dep_path but
materialize_into(..., false) writes into the transformed name, causing false
negatives; change the aube_entry construction in link_all to use
aube_dir_entry_name(dep_path) (i.e. replace aube_dir.join(dep_path) with
aube_dir.join(aube_dir_entry_name(dep_path))) so the fast path probes the same
flattened/escaped path materialize_into uses and correctly increments
stats.packages_cached.

In `@crates/aube-lockfile/src/lib.rs`:
- Around line 600-603: The BFS in transitive_closure builds child keys as
format!("{child_name}@{child_version}") which fails for git/remote-tarball nodes
because packages stores those nodes under shared_local_dep_path(...) rather than
the raw resolved URL; change the child_key construction in transitive_closure to
canonicalize URL-shaped tails: detect when child_version is a
URL/remote-tarball/git-form (using the same predicate used elsewhere) and
instead compute the stored node key with shared_local_dep_path(child_name,
&child_version) (or the existing canonicalizer used by
filter_deps/subset_to_importer), otherwise keep the existing name@tail form,
then use that canonical child_key for reachable.insert and queue.push_back. Also
make the same canonicalization change in filter_deps and subset_to_importer
wherever edges are rebuilt so all BFS/walk code uses the same stored-key format
and the transitive subtree is not pruned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 07e09b60-903b-45c7-9752-3b86368b2b46

📥 Commits

Reviewing files that changed from the base of the PR and between 9aeff92 and 2991bd6.

📒 Files selected for processing (6)
  • crates/aube-linker/src/hoisted.rs
  • crates/aube-linker/src/link.rs
  • crates/aube-linker/src/materialize.rs
  • crates/aube-lockfile/src/graph_hash.rs
  • crates/aube-lockfile/src/lib.rs
  • crates/aube-lockfile/src/source.rs

Comment on lines +318 to +320
// pnpm appends a `(peer@ver)` suffix to some spec values; the parser
// strips it before classifying the source, so strip it here too.
let classify = dep_value.split('(').next().unwrap_or(dep_value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Strip only a trailing peer suffix.

split('(').next() truncates any ( that belongs to the real spec, not just pnpm's terminal (peer@ver) suffix. A tarball URL like .../pkg(1).tgz or a git selector like ...&path:/packages/foo(bar) will canonicalize to a different dep_path than the parser inserted, so the linker/hasher can miss that child again.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube-lockfile/src/source.rs` around lines 318 - 320, The current
truncation using dep_value.split('(').next() removes any '(' anywhere; instead
only strip pnpm's trailing peer suffix: check if dep_value ends_with(')') and
rfind('(') to get the last '('; if the substring after that '('
starts_with("peer@") then set classify to the slice before that '(' otherwise
leave classify as dep_value. Update the code that sets classify (the line using
dep_value.split('(').next().unwrap_or(dep_value)) to use this conditional
rfind/starts_with logic so only a terminal "(peer@...)" is removed.

@jdx
jdx merged commit d7ed45c into aubepkg:main Jun 13, 2026
17 checks passed
@cursor cursor Bot mentioned this pull request Jun 13, 2026
@rubnogueira
rubnogueira deleted the fix/gvs-exotic-bug branch June 13, 2026 12:11
colinhacks pushed a commit to nubjs/aube that referenced this pull request Jun 17, 2026
…ith canonical sshurl resolved

Upstream aubepkg#857 reclassified github/gitlab/bitbucket-shorthand git deps to
resolve via a codeload archive — LocalSource::RemoteTarball { git_hosted:
true } rather than LocalSource::Git. The npm writer's supplementary
canonical-map patch filtered Git-only, so the git-hosted dep was dropped
from package-lock.json entirely; real `npm ci` then rejected the lock with
`Missing: <dep> from lock file`.

Two coordinated edits, both default-preserving (a plain non-git
RemoteTarball is unaffected; standalone aube's non-codeload paths are
byte-for-byte unchanged):

- write.rs: widen the canonical-map filter (now via is_git_local_source)
  to also include RemoteTarball { git_hosted: true } so the dep is placed.
- source.rs: npm_resolved_field emits npm's canonical
  git+ssh://git@<host>/<owner>/<repo>.git#<sha> for a git-hosted codeload
  dep (inverting the codeload archive URL per provider) rather than the
  codeload tarball URL, matching real npm 11.13.0 so `npm install` doesn't
  churn the line.
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.

2 participants