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.

Install runtime dependencies (node@runtime:, deno@runtime:, bun@runtime:) from pnpm-lock.yaml #437

@zkochan

Description

@zkochan

Background

pnpm v11 lets a project declare a JavaScript runtime as a dependency: node@runtime:22, deno@runtime:1.x, bun@runtime:1, either through pnpm add or via devEngines.runtime in package.json. The resolution lands in pnpm-lock.yaml as a BinaryResolution (one platform) or VariationsResolution (multiple platforms wrapped in variants), each pointing at a .tar.gz or .zip downloaded from nodejs.org / dl.deno.land / GitHub releases. On install, pnpm picks the variant that matches the host platform/arch/libc, extracts it into the CAS, and links the binary (node, deno, bun) into node_modules/.bin/.

Roadmap (#299) Stage 1 lists "Installation of runtimes" as a remaining checkbox. This issue scopes it to pacquet install --frozen-lockfile: handle BinaryResolution and VariationsResolution entries already present in a lockfile. Resolution (parsing runtime: specifiers, talking to nodejs.org for version lookups, picking the artifact filename) lives in engine/runtime/{node,deno,bun}-resolver and is Stage 2.

Today

Pacquet can't load a lockfile containing a runtime entry. crates/lockfile/src/resolution.rs enumerates Tarball | Registry | Directory | Git — no Binary, no Variations — and TaggedResolution is #[serde(deny_unknown_fields)], so a real v11 lockfile with type: binary or type: variations fails serde before the install dispatcher even runs.

Beyond the type gap there's no:

  • Platform/arch/libc selection.
  • Binary fetcher (zip extraction + tarball extraction + integrity check + ignore-pattern filtering).
  • "Strip Node.js's bundled npm/npx/corepack" pass.
  • Bin linking for runtime depPaths (the depPath itself takes a non-standard shape: node@runtime:22.0.0).

The current --frozen-lockfile path also asserts lockfile_version.major == 9 at crates/package-manager/src/install.rs:172. Verify whether v11 runtime lockfiles bumped the major — if so, that assertion needs updating; if the change is purely additive within v9, it stays.

Upstream references (pnpm v11 94240bc046)

  • Resolution shapes in the lockfile. lockfile/types/src/index.ts:127-163:
    • BinaryResolution { type: 'binary', url, integrity, bin: string | Record<string, string>, archive: 'zip' | 'tarball' }.
    • PlatformAssetResolution { resolution: Resolution, targets: PlatformAssetTarget[] }.
    • VariationsResolution { type: 'variations', variants: PlatformAssetResolution[] }.
    • PlatformAssetTarget is defined at resolving/resolver-base/src/index.ts:60.
  • Variant picking. Each variant's targets[] lists { os, cpu, libc? } combos. Match against the current platform; the first variant whose targets includes the host wins.
  • Binary fetcher. fetching/binary-fetcher/src/index.ts:
    • archive === 'tarball' → delegate to the existing remote-tarball fetcher with an ignoreFilePattern so npm/corepack files don't enter the CAS.
    • archive === 'zip' → download to a temp file, integrity-check with ssri, extract via AdmZip into a temp dir, run addFilesFromDir into the CAS. Per-entry path traversal validation (validatePathSecurity), stateless ignore-regex tester, directory entries skipped to keep the ignore filter effective.
  • Node's "strip bundled tooling" filter. engine/runtime/node-resolver/src/index.ts:25-30 defines NODE_EXTRAS_IGNORE_PATTERN excluding node_modules/{npm,corepack} and bin/{npm,npx,corepack} (plus .cmd/.ps1). The pattern is applied during binary fetch via archiveFilters[<pkg.name>]. ~2,800 of ~5,800 files are skipped.
  • Fetcher dispatch. fetching/pick-fetcher/src/index.ts:51-58: resolution.type === 'binary'binary fetcher. Variations are unwrapped one level higher (the install pipeline picks the matching variant first, then dispatches its inner BinaryResolution).
  • --no-runtime flag. installing/commands/src/install.ts:137-138 and installing/deps-installer/src/install/index.ts:1377-1387. Skips runtime fetch + bin-linking; the lockfile is left untouched so frozen-lockfile checks still pass. Runtime depPaths are recognised via the @runtime: substring.

Plan

A. Lockfile types

  • Add to crates/lockfile/src/resolution.rs:
    • BinaryResolution { url, integrity, bin: BinSpec, archive: BinaryArchive } where BinSpec = String | HashMap<String, String> and BinaryArchive = Tarball | Zip (serde tag = "archive" or as the existing tagged-enum mechanism). Tag: type: "binary".
    • PlatformAssetResolution { resolution: Resolution, targets: Vec<PlatformAssetTarget> }.
    • PlatformAssetTarget { os: String, cpu: String, libc: Option<String> }.
    • VariationsResolution { variants: Vec<PlatformAssetResolution> }. Tag: type: "variations".
  • Extend LockfileResolution and the ResolutionSerde/TaggedResolution helpers in the same file so round-trip serde stays exhaustive.
  • Confirm the lockfile-version major hasn't bumped (compare a v11 lockfile output containing a runtime against lockfile_version.major == 9 at crates/package-manager/src/install.rs:172). If it did, update the assertion and the parser; if not, leave the assertion alone.

B. Variant picking

  • Add pick_variant(variants: &[PlatformAssetResolution], host: PlatformAssetTarget) -> Option<&PlatformAssetResolution>. Match strategy mirrors upstream: first variant whose targets includes the host. libc is only checked on Linux (musl vs glibc) — verify exact semantics against engine/runtime/node-resolver since libc: 'musl' is the only value upstream sets.
  • Host detection: std::env::consts::{OS, ARCH} for pacquet; libc detection on Linux via the existing pacquet plumbing if any, else reading /proc/self/exe or inspecting ldd/dynamic loader. Stage-1 minimum: glibc fallback on Linux. Track musl detection as a follow-up checkbox.

C. Binary fetcher

  • New module — crates/binary-fetcher or under crates/tarball if reusing the same CAS-write helpers — porting fetching/binary-fetcher/src/index.ts:
    • Tarball archive. Reuse pacquet's existing remote-tarball fetcher with an extra ignore_file_pattern: Option<Regex> parameter. The pattern's threaded as a string today across upstream's worker boundary; pacquet can pass a compiled regex::Regex directly.
    • Zip archive. Download with integrity check (ssri-equivalent — pacquet already uses ssri crate for tarball integrity), extract entries with path-traversal validation. The existing zip dependency situation needs a decision before this lands: pacquet doesn't have a zip crate yet, so flag this as a workspace-dependency request rather than adding zip unilaterally. Per CLAUDE.md, raise the dep question with a human before adding.
  • Strip bundled tooling: when pkg.name == "node", apply NODE_EXTRAS_IGNORE_PATTERN (port the regex verbatim, including its post-prefix-strip semantics) during extraction. Similar archive-filter slots may exist for deno/bun; check upstream and mirror.
  • Validate every extracted entry against the target directory boundary (validatePathSecurity equivalent). Reject absolute paths; reject paths that resolve outside the target.

D. Install pipeline wiring

  • Extend the resolution dispatcher at crates/package-manager/src/install_package_by_snapshot.rs:110-115 and create_virtual_store.rs:513-520:
    • LockfileResolution::Binary(binary) → binary fetcher path.
    • LockfileResolution::Variations(v)pick_variant first, then recurse with the picked variant's inner resolution. Error with a clear "no variant matches host platform" if pick_variant returns None.
  • Bin linking for runtime depPaths. The bin field in BinaryResolution already names the executables; create <project>/node_modules/.bin/<binName> cmd-shims pointing at the extracted location. Pacquet's existing pacquet-cmd-shim infrastructure handles the cross-platform shim part — wire it for runtime entries.
  • Recognise the @runtime: substring in depPaths where needed (skip lists, reporter prefixes, etc.). Most pipeline code doesn't care, but the upstream skipRuntimes filter at installing/deps-installer/src/install/index.ts:1377-1387 uses it as the discriminator.
  • Store-index key: BinaryResolution has stable integrity, so the regular storeIndexKey(integrity, pkgId) shape works; nothing special.

E. --no-runtime

  • Add skip_runtimes: bool to Config and the CLI (--no-runtime).
  • When set, skip the binary fetch + bin-linking but keep the lockfile parse intact so the install still validates. Mirror upstream's behavior at the call site above: filter runtime depPaths out of dependencies_by_project_id after the lockfile walk.

F. Tests

  • Frozen-lockfile install with a fixture lockfile containing a VariationsResolution for node@runtime:22. Assert the host-matching variant is fetched, the CAS contains the extracted Node, node_modules/.bin/node resolves, and node_modules/<importer>/node_modules/node/ symlinks correctly. Use a recorded fixture archive (small slice) rather than hitting nodejs.org from CI.
  • Same as above with a single BinaryResolution (no variations wrapper).
  • npm/npx/corepack paths are absent from the imported file set when pkg.name == "node" (asserts the ignore pattern fired).
  • Variant mismatch: lockfile contains only darwin-arm64 variants but the test pretends to run on linux-x64 → clear error.
  • --no-runtime: lockfile contains a runtime entry, install skips the download + bin-link, the rest of the install succeeds, exit code is 0.
  • Integrity mismatch on the downloaded archive → TARBALL_INTEGRITY / equivalent error code, no partial state in the CAS.
  • Path-traversal-malicious zip → PATH_TRAVERSAL error before any extraction succeeds.
  • Port plans/TEST_PORTING.md "Installation Of Runtimes" entries (six checkboxes against installing/deps-installer/test/install/nodeRuntime.ts). Mark them as done as they land.

Out of scope (Stage 2 / follow-ups)

Related


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