You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on May 14, 2026. It is now read-only.
#432 landed Stage 1 of the global virtual store. Per-snapshot slots live at <store_dir>/links/<scope>/<name>/<version>/<hash> where the <hash> is pacquet_graph_hasher::calc_graph_node_hash over {engine, deps}.
Pacquet's calc_graph_node_hash already takes engine: Option<&str> — passing None produces a different hash than Some("darwin-arm64-node20") (the Option shape is intentional). But the call site in VirtualStoreLayout::new always passes Some(ENGINE_NAME), so every Node major bump invalidates the GVS slot for every package.
The "Out of scope" section of #432 flagged this as a dedicated follow-up.
builtDepPaths is the set of snapshots whose package matches the allowBuilds map (computeBuiltDepPaths).
transitivelyRequiresBuild walks the dep graph from depPath checking if any reachable child is in builtDepPaths (deps/graph-hasher/src/index.ts:225-260).
A package that neither builds itself nor depends on a builder gets engine: null → its slot survives every Node version bump on the same host.
Today
Every snapshot hashes engine: Some(ENGINE_NAME).
A node 22 → node 23 host upgrade re-extracts every package even though pure-JS packages are byte-identical across Node versions.
Cross-host store sharing (e.g. team member on arm64-macos vs. x64-linux) duplicates every slot.
Plan
Add a built_dep_paths: Option<&HashSet<PackageKey>> parameter to VirtualStoreLayout::new, or wrap the input as BuildPolicy { built: HashSet<PackageKey>, ... } so the call site can stay narrow.
Port transitively_requires_build into graph-hasher (the recursion shape is the same as calc_dep_graph_hash — already exists in dep_state.rs).
Switch VirtualStoreLayout::new's calc_graph_node_hash call to pass Some(engine) only when built_dep_paths is None (no allowBuilds map at all — the legacy non-GVS path) OR when the snapshot transitively requires a build.
InstallFrozenLockfile::run builds built_dep_paths from AllowBuildPolicy::from_config (which already exists) — every snapshot whose (name, version) matches the allowlist lands in the set.
Tests:
GVS hash for a pure-JS package is the same under Some("darwin-arm64-node20") and Some("linux-x64-node22") engines when the package isn't allow-listed.
GVS hash differs when the package or a transitive dep is allow-listed (engine is included).
The allowBuilds: {} default-when-GVS-on lands the layout in the gating regime automatically.
Out of scope
The build-time engine string for the side-effects-cache key (BuildModules::engine_name) is a separate concern and stays as-is — the cache is engine-specific by design.
Background
#432 landed Stage 1 of the global virtual store. Per-snapshot slots live at
<store_dir>/links/<scope>/<name>/<version>/<hash>where the<hash>ispacquet_graph_hasher::calc_graph_node_hashover{engine, deps}.Pacquet's
calc_graph_node_hashalready takesengine: Option<&str>— passingNoneproduces a different hash thanSome("darwin-arm64-node20")(theOptionshape is intentional). But the call site inVirtualStoreLayout::newalways passesSome(ENGINE_NAME), so every Node major bump invalidates the GVS slot for every package.The "Out of scope" section of #432 flagged this as a dedicated follow-up.
What upstream does
config/reader/src/index.ts:325-330defaultsallowBuildsto{}(an empty allowlist) whenenableGlobalVirtualStoreis on and the user hasn't set it explicitly. Then indeps/graph-hasher/src/index.ts:122-146thecalcGraphNodeHashwalk decides per-snapshot whether to includeengine:builtDepPathsis the set of snapshots whose package matches theallowBuildsmap (computeBuiltDepPaths).transitivelyRequiresBuildwalks the dep graph fromdepPathchecking if any reachable child is inbuiltDepPaths(deps/graph-hasher/src/index.ts:225-260).engine: null→ its slot survives every Node version bump on the same host.Today
engine: Some(ENGINE_NAME).node 22 → node 23host upgrade re-extracts every package even though pure-JS packages are byte-identical across Node versions.Plan
built_dep_paths: Option<&HashSet<PackageKey>>parameter toVirtualStoreLayout::new, or wrap the input asBuildPolicy { built: HashSet<PackageKey>, ... }so the call site can stay narrow.transitively_requires_buildintograph-hasher(the recursion shape is the same ascalc_dep_graph_hash— already exists indep_state.rs).VirtualStoreLayout::new'scalc_graph_node_hashcall to passSome(engine)only whenbuilt_dep_pathsisNone(noallowBuildsmap at all — the legacy non-GVS path) OR when the snapshot transitively requires a build.InstallFrozenLockfile::runbuildsbuilt_dep_pathsfromAllowBuildPolicy::from_config(which already exists) — every snapshot whose(name, version)matches the allowlist lands in the set.Some("darwin-arm64-node20")andSome("linux-x64-node22")engines when the package isn't allow-listed.allowBuilds: {}default-when-GVS-on lands the layout in the gating regime automatically.Out of scope
BuildModules::engine_name) is a separate concern and stays as-is — the cache is engine-specific by design.References
calcGraphNodeHashcomputeBuiltDepPathstransitivelyRequiresBuildallowBuilds ??= {}under GVScrates/graph-hasher/src/global_virtual_store_path.rs(theOption<&str>plumbing from feat: global-virtual-store foundation (#432 partial) #444),crates/package-manager/src/virtual_store_layout.rs(theSome(engine)call site from feat: activate global-virtual-store install path (#432) #449)pacquet install --frozen-lockfile#432 (Out of scope)Written by an agent (Claude Code, claude-opus-4-7).