Zoltan Kochan ca88affd39 feat(package-is-installable): platform + engine check, skip optional incompatibles (#434 slice 1) (#439)
Slice 1 of #434 — foundational installability gate. Ports
[`@pnpm/config.package-is-installable`](https://github.com/pnpm/pnpm/tree/94240bc046/config/package-is-installable)
and threads the resulting skip-set through every phase of the
frozen-lockfile install pipeline.

Closes #266 once shipped (covers the "install respects every snapshot
— no os/cpu/libc filter" gap). Does **not** close #434 — that umbrella
has six more slices to follow.

Upstream reference: pnpm/pnpm@94240bc046.

## What landed

### New crate: `pacquet-package-is-installable`

Ports the upstream `config/package-is-installable` package's three
helpers:

- `check_platform` (`Option<&[String]>` for each `os`/`cpu`/`libc`
  axis, plus a `SupportedArchitectures` override) — returns
  `Option<UnsupportedPlatformError>` matching upstream's
  `ERR_PNPM_UNSUPPORTED_PLATFORM` code, message shape, and JSON
  payload. Handles negation entries (`!foo`), the `any` sentinel,
  the `current` placeholder, and the `currentLibc !== 'unknown'`
  skip from `checkPlatform.ts:38`.
- `check_engine` — evaluates `engines.node` / `engines.pnpm` via
  `node-semver`. Approximates npm-semver's `includePrerelease: true`
  via a strip-prerelease fallback; one over-acceptance edge case
  (`>=X.Y.Z` against `X.Y.Z-rc1`) is pinned in the known-failures
  integration test for follow-up.
- `package_is_installable` — composes the two, returns the
  tri-state verdict matching upstream's `boolean | null` (Installable
  / SkipOptional / ProceedWithWarning), plus an `Err` arm for
  `engine_strict` aborts and `ERR_PNPM_INVALID_NODE_VERSION`.

`InstallabilityOptions<'a>` borrows its host strings so a caller
running through many snapshots in a row can build the host part
once and only toggle `optional` per snapshot. `WantedPlatformRef<'a>`
plays the same role for the manifest axes so `check_platform` runs
the happy path without any allocation.

### New module: `pacquet_package_manager::installability`

`compute_skipped_snapshots` is the per-install entry point. For each
snapshot:

1. Look up the matching `PackageMetadata`.
2. Run `check_package` (cached per peer-stripped `metadata_key` so
   peer-resolved variants of the same package share one verdict).
3. Dispatch on `(verdict, snapshot.optional, host.engine_strict)`:
   - `Installable`: nothing to do.
   - `SkipOptional` + `optional`: add to `SkippedSnapshots`, emit
     `pnpm:skipped-optional-dependency` (deduped per metadata key,
     matching upstream's emit-per-pkgId).
   - `Incompatible` + non-optional + `engine_strict`: abort.
   - `Incompatible` + non-optional + non-strict: `tracing::warn!`
     and proceed. (Upstream's `pnpm:install-check` channel isn't
     wired into pacquet's reporter yet — slice 1 follow-up.)

`any_installability_constraint(packages)` is the caller-side fast
path: if no metadata row declares an `engines.{node,pnpm}` or a
non-empty / non-`["any"]` `cpu`/`os`/`libc`, the entire installability
pass is skipped. The probe runs synchronously in
`install_frozen_lockfile` *before* the `tokio::task::spawn_blocking`
that would invoke `node --version` — so the common no-constraints
lockfile pays nothing for the new pipeline, restoring main's overlap
of node-binary startup with extraction.

### Install-pipeline plumbing

The `SkippedSnapshots` set is threaded into every downstream phase
of `InstallFrozenLockfile::run`:

- `CreateVirtualStore`: installability-skipped snapshots are
  dropped from both `survivors` (no virtual-store slot extracted)
  and `skipped_entries` (no warm-cache row). Layered ahead of
  main's #442 already-installed-and-on-disk skip filter.
- `SymlinkDirectDependencies`: a direct dep whose resolved
  snapshot is in the skip set is omitted from `node_modules/<name>`
  (no symlink, no `pnpm:root added` event, no bin link).
- `LinkVirtualStoreBins`: per-slot bin link skips slots whose
  snapshot is installability-skipped (their virtual-store
  directories don't exist).
- `BuildModules` via `build_sequence`: `get_subgraph_to_build`
  consults `skipped` *before* recursion, so a skipped snapshot's
  subtree doesn't contribute to the build graph via that edge.
  Descendants reachable from a non-skipped root still build
  normally.

### Performance

CI integrated-benchmark on the 1352-package fixture, latest run:

| Scenario | `pacquet@HEAD` | `pacquet@main` | Relative |
|---|---|---|---|
| Frozen Lockfile (cold) | 2.476 ± 0.083 s | 2.442 ± 0.071 s | 1.01 ± 0.05 |
| Frozen Lockfile (Hot Cache) | 685.8 ± 59.3 ms | 700.2 ± 47.4 ms | 1.00 |

Earlier iterations of this PR showed a ~5% cold-install regression
from the `node --version` spawn landing on the extraction critical
path. Closed by hoisting the no-constraints fast-path probe to the
caller (commit `cf47ce51`) so the spawn is gated on actual
constraint presence.

Other perf passes folded in:

- `compute_skipped_snapshots` caches the per-metadata-row check
  verdict so peer-resolved variants share one `check_package` call.
- `check_platform` borrows its three wanted axes through
  `WantedPlatformRef<'a>`; the owned `WantedPlatform` only
  materialises in the error path.

## Tests

| Suite | Count | What it covers |
|---|---|---|
| `pacquet-package-is-installable::tests::check_platform` | 16 | Port of upstream `checkPlatform.ts` — `any`/`current` sentinels, negation, `supportedArchitectures` override, libc unknown-skip |
| `pacquet-package-is-installable::tests::check_engine` | 7 | Port of upstream `checkEngine.ts` — node/pnpm range checks, prerelease cases, `ERR_PNPM_INVALID_NODE_VERSION` |
| `pacquet-package-is-installable::tests::package_is_installable` | 6 | Tri-state verdict + optional/engine-strict dispatch |
| `pacquet-package-is-installable::tests::known_failures` | 1 | The `>=X.Y.Z` vs `X.Y.Z-rc1` over-acceptance, picked up by `just known-failures` |
| `pacquet_package_manager::installability::tests` | 11 | Per-install skip-set computation: skip on bad OS, skip on bad node engine, dedup events across peer variants, fast-path triggers, constraint predicate's edge cases (`engines.npm` only, `["any"]` sentinel, empty lists) |
| `pacquet_package_manager::build_sequence::tests` | 3 (new) | Skipped+patched doesn't enter build queue; skipped parent doesn't drag descendants in; descendant with non-skipped parent still builds |

All ported tests verified to catch regressions by temporarily
breaking the subject under test, observing the failure, then
reverting. The "test the tests" workflow from CLAUDE.md.

## Deferred to follow-up slices

- `.modules.yaml.skipped` write/read + headless re-check
  (slice 3).
- `supportedArchitectures` config + `--cpu` / `--os` / `--libc`
  CLI flags (slice 2).
- `pnpm:install-check` warn channel on the reporter side
  (currently `tracing::warn!`).
- Real libc detection — `host_libc()` returns `"unknown"` today;
  matches non-Linux host behavior, but on Alpine/musl this
  over-installs glibc-only optional packages. Slice 2.
- `engine_strict` config wiring — defaults to false today, so
  the error path is unreachable from production. Wired through
  end-to-end so the slice that flips the config doesn't churn
  the error enum.
2026-05-13 15:13:32 +02:00
Description
No description provided
MIT 291 MiB
Languages
Rust 60.5%
TypeScript 38.9%
JavaScript 0.5%