Skip to content
This repository was archived by the owner on May 14, 2026. It is now read-only.
This repository was archived by the owner on May 14, 2026. It is now read-only.

Engine-agnostic GVS hash gating via allowBuilds-driven built_dep_paths (#432 follow-up) #459

@zkochan

Description

@zkochan

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

What upstream does

config/reader/src/index.ts:325-330 defaults allowBuilds to {} (an empty allowlist) when enableGlobalVirtualStore is on and the user hasn't set it explicitly. Then in deps/graph-hasher/src/index.ts:122-146 the calcGraphNodeHash walk decides per-snapshot whether to include engine:

const includeEngine = builtDepPaths === undefined ||
  transitivelyRequiresBuild(graph, builtDepPaths, buildRequiredCache, depPath, new Set())
const engine = includeEngine ? ENGINE_NAME : null
  • 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

  1. 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.
  2. Port transitively_requires_build into graph-hasher (the recursion shape is the same as calc_dep_graph_hash — already exists in dep_state.rs).
  3. 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.
  4. 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.
  5. 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.

References


Written by an agent (Claude Code, claude-opus-4-7).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions