A dependency's `requiresBuild` is read off its published files, which do
not carry the patch yet. A patch that adds `preinstall`, `install`, or
`postinstall` — or a `binding.gyp`, which the lifecycle runner turns into
an implicit `node-gyp rebuild` — therefore left the package looking
build-free: pnpm 12 never ran it and never offered it in
`pnpm approve-builds`, while pnpm 11 ran it without asking for approval
at all.
pnpm 12 previews the patch in memory (`pnpm_patching::preview_patch`)
while it builds `requires_build_map`, so the allow-build gate, the build
graph, `pendingBuilds`, and the side-effects cache key all describe the
package that ends up on disk. Deciding it there rather than after
`apply_patch_to_dir` is what makes the fix reach existing users: the key
derived from the pre-patch answer matches the entry a broken version
already wrote, and that cache hit would suppress the build forever on any
machine that had installed once. The preview answers for the package the
apply would leave: it chains repeated records for one file, drops a path a
later record deletes, resolves header paths the way `apply_one_file` does,
and reports the manifest verbatim so the caller's BOM-tolerant parser sees
what it would on disk.
pnpm 11 recomputes from the patched directory, where its filesystem-based
applier can reach it, through the new `dirRequiresBuild`, which reads the
same two triggers off a directory that `pkg_requires_build` reads in the
Rust stack. The recompute
runs even under `ignoreScripts`, because pnpm 11 must also stop writing a
side-effects entry for a package whose build is still owed: the entry the
patch alone produced would replay on the install that follows an approval,
and the scripts would never get their chance.
`manifest_requires_build` now reads a script's value rather than its key,
matching pnpm v11's `pkgRequiresBuild`. A manifest carrying an empty
`postinstall` runs nothing, and gating it would have asked the user to
approve a build that does not exist.
The preview is answered once per patch rather than once per snapshot: peer
variants of a package share both the extracted manifest and the patch, and
each preview reads and parses two files on the install path.
The preview also answers for a manifest a patch deletes and writes again,
tracks written paths as a set so a delete costs a lookup rather than a scan,
and `dirRequiresBuild` reports no build for a directory it cannot inspect,
as `pkg_requires_build` already did.
A header that spells the manifest in another case resolves through the
filesystem rather than by guessing the platform, so the preview reads the
manifest exactly where the applier writes it.
The preview is skipped for a package whose published manifest already
declares a build: a patch cannot subtract that, so reading and parsing the
patch would change nothing.
The two stacks agree on what a user sees. They still derive the
side-effects cache key differently for a patched package that runs
scripts, so such a package rebuilds on every pnpm 11 install, as it does
today.
Closes https://github.com/pnpm/pnpm/issues/14648