Commit Graph

2 Commits

Author SHA1 Message Date
Zoltan Kochan
13082fc1bf feat: apply patchedDependencies before postinstall (#397 item 9) (#427)
Completes pnpm/pacquet#397 item 9 (`patchedDependencies` support). Patches now actually land on disk: pacquet applies configured patches to extracted package directories before postinstall hooks run, includes patched-only packages in the build trigger, and surfaces the four upstream diagnostic codes.

Slices A + B (PR #425, merged) landed the foundation, Config plumbing, and the side-effects-cache key wiring. This PR wires the rest.

## Pure-Rust patch applier

`pacquet_patching::apply_patch_to_dir` ports upstream's [`applyPatchToDir`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/patching/apply-patch/src/index.ts) using the [`diffy`](https://crates.io/crates/diffy) crate (MIT OR Apache-2.0, pure Rust, no subprocess). Upstream's own comment notes that `patch` is unavailable on Windows and `git apply` is awkward inside an existing git repo — pnpm vendored `@pnpm/patch-package` for that reason; pacquet sidesteps it the same way with an in-process applier.

Supported `FileOperation`s: `Modify`, `Create`, `Delete`. `Rename` and `Copy` fall through to `ERR_PNPM_PATCH_FAILED` with a descriptive message — they don't appear in `patch-package`-style patches in practice.

## Build pipeline

- `build_sequence::get_subgraph_to_build` now considers `patch.is_some()` alongside `requires_build`. A patched-only package becomes a build candidate the same way upstream's filter at [`buildSequence.ts:47,60`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/buildSequence.ts#L40-L67) treats them.
- `BuildModules::run` per-snapshot loop refactored to mirror upstream's flow:
  - Inner build trigger becomes `requires_build || patch.is_some()`.
  - `allowBuilds` check only gates scripts (upstream's `if (node.requiresBuild)` at [`during-install/src/index.ts:88-101`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/index.ts#L82-L106)). Disallow / ignored sets `should_run_scripts = false` rather than `continue` — patches still apply.
  - `cache_key`'s `include_dep_graph_hash` is now `should_run_scripts` (was unconditionally `true`), so a patched-only snapshot's cache key omits the deps-hash, matching upstream's `includeDepGraphHash: hasSideEffects` at [line 202](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/index.ts#L199-L204).
  - Patch application happens before `run_postinstall_hooks`, mirroring [`during-install/src/index.ts:171-178`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/index.ts#L168-L191).
  - Cache-write gate becomes `(is_patched || has_side_effects) && side_effects_cache_write` (was just `side_effects_cache_write`), matching upstream's `(isPatched || hasSideEffects)` at line 199.

## Diagnostics

Four upstream codes surface via `BuildModulesError`:

- `ERR_PNPM_PATCH_NOT_FOUND` — patch file missing on disk
- `ERR_PNPM_INVALID_PATCH` — unified-diff parse error
- `ERR_PNPM_PATCH_FAILED` — hunk wouldn't apply, target missing, IO error on a target path
- `ERR_PNPM_PATCH_FILE_PATH_MISSING` — resolved patch entry has a hash but no path (lockfile-only shape with no live config); mirrors [`during-install/src/index.ts:172-176`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/index.ts#L172-L176)

## Dependency

New workspace dep: `diffy = "0.5.0"`. MIT OR Apache-2.0, both in the deny.toml allow-list. `cargo deny check licenses` ok.
2026-05-12 21:54:01 +02:00
Zoltan Kochan
f2d41415af feat: patchedDependencies foundation + cache key + AllowBuildPolicy from Config (#397 items 9 A+B, 5) (#425)
Combined slices A and B of pnpm/pacquet#397 item 9 (`patchedDependencies` support), plus the related #397 item 5 fix (`AllowBuildPolicy` moves off `package.json`).

This PR adds the foundation (`pacquet-patching` crate) and threads patches as far as the side-effects-cache key. The build-trigger update and the actual patch application are deferred to slice C — they only make sense paired together.

## Foundation (`pacquet-patching` crate)

Ports `@pnpm/patching.types` + `@pnpm/patching.config` + `calcPatchHashes` at pnpm SHA [`b4f8f47ac2`](https://github.com/pnpm/pnpm/tree/b4f8f47ac2):

- **Types** (`types.rs`) — `PatchInfo`, `ExtendedPatchInfo`, `PatchGroupRangeItem`, `PatchGroup`, `PatchGroupRecord` mirroring [`patching/types/src/index.ts`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/patching/types/src/index.ts).
- **Key parser** (`key.rs`) — minimal port of [`dp.parse`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/deps/path/src/index.ts#L120-L168).
- **Grouping** (`group.rs`) — `group_patched_dependencies` mirroring [`groupPatchedDependencies.ts`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/patching/config/src/groupPatchedDependencies.ts), with `ERR_PNPM_PATCH_NON_SEMVER_RANGE`.
- **Matcher** (`get_patch_info.rs`) — `get_patch_info` (exact → unique-range → wildcard) mirroring [`getPatchInfo.ts`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/patching/config/src/getPatchInfo.ts), with `ERR_PNPM_PATCH_KEY_CONFLICT`.
- **Verify** (`verify.rs`) — `all_patch_keys` + `verify_patches` with `ERR_PNPM_UNUSED_PATCH`.
- **Hashing** (`hash.rs`) — SHA-256 hex with CRLF→LF normalization (matters for cross-platform stability of the cache key).
- **Resolve helper** (`resolve.rs`) — `resolve_and_group(workspace_dir, raw_map)` resolves relative patch paths against the workspace dir, hashes, and groups.

## Config plumbing (workspace yaml is the source of truth)

pnpm v11 reads install settings from `pnpm-workspace.yaml`, not `package.json`. This PR matches that for `patchedDependencies` and (as the #397 item 5 fix) `allowBuilds` / `dangerouslyAllowAllBuilds` too. The misleadingly-named upstream [`getOptionsFromRootManifest.ts`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/config/reader/src/getOptionsFromRootManifest.ts) is called at [`config/reader/src/index.ts:814`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/config/reader/src/index.ts#L814) with the *workspace* manifest.

- `Config.workspace_dir: Option<PathBuf>` — recorded by `WorkspaceSettings::apply_to` so install-time code can resolve patch file paths against the same dir upstream uses.
- `Config.patched_dependencies: Option<BTreeMap<String, String>>` — raw map. Hashing + resolution deferred to `Config::resolved_patched_dependencies` so `apply_to` stays infallible.
- `Config.allow_builds: HashMap<String, bool>` and `Config.dangerously_allow_all_builds: bool` — replace `AllowBuildPolicy::from_manifest`'s `package.json` reads.
- `WorkspaceSettings` deserializes the matching three yaml keys (`patchedDependencies`, `allowBuilds`, `dangerouslyAllowAllBuilds`); `apply_to` pushes them.

## Install-pipeline threading

- `InstallFrozenLockfile::run` calls `config.resolved_patched_dependencies()` once per install, then for each snapshot calls `pacquet_patching::get_patch_info(&groups, name, version)` and collects matches into `HashMap<PackageKey, ExtendedPatchInfo>` keyed by the peer-stripped key. Mirrors upstream's per-resolution lookup at [`resolveDependencies.ts:1482`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/pkg-manager/resolve-dependencies/src/resolveDependencies.ts#L1482), adapted for pacquet's lockfile-driven flow.
- `BuildModules` gains a `patches: Option<&HashMap<PackageKey, ExtendedPatchInfo>>` field. The `calc_dep_state` call at the existing `build_modules.rs:326` placeholder now passes `patch_file_hash: patch.map(|p| p.hash.as_str())`, matching upstream's [`patchFileHash: depNode.patch?.hash`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/index.ts#L201).

## `AllowBuildPolicy` (#397 item 5)

- Drop `AllowBuildPolicy::from_manifest` (the `package.json` reader was a workaround until Config plumbing landed).
- Add `AllowBuildPolicy::from_config(&Config)` consuming the new yaml-sourced fields.
- `install_frozen_lockfile.rs:156` switches over.

## What's NOT in this PR

Tracked for slice C:

- **Build trigger update** (`requires_build || patch.is_some()`) and **patch application** to extracted package directories before postinstall hooks. These belong together — wiring the trigger in isolation produces an incoherent state (the cache key advertises a patch that isn't on disk).
- Decision on the patch applier (Rust crate vs `git apply` subprocess) will be raised when slice C opens.
- Diagnostics: `ERR_PNPM_PATCH_FILE_PATH_MISSING` / `PATCH_NOT_FOUND` / `INVALID_PATCH` / `PATCH_FAILED`.
- End-to-end test parity with upstream's [`simple-with-patch`](https://github.com/pnpm/pnpm/tree/b4f8f47ac2/pkg-manager/headless/test/fixtures/simple-with-patch) fixture.
2026-05-12 20:31:45 +02:00