Repository navigation
fix: respect gypfile:false when detecting install scripts - #10061
JamieMagee wants to merge 2 commits into
Conversation
fc8a3a2 to
9e4f5b9
Compare
|
Strict preflight now uses only incoming metadata and preserves known script bodies. Linked installs sync rebuilt metadata back to logical nodes, including stale |
manzoorwanijk
left a comment
There was a problem hiding this comment.
This looks good to me now.
| const includePrepare = hasNonRegistryShape(node) | ||
| const anyScript = scripts.preinstall || scripts.install || scripts.postinstall || | ||
| (includePrepare && scripts.prepare) | ||
| let isGyp = readFromDisk && !scripts.preinstall && !scripts.install && gypfile !== false && |
There was a problem hiding this comment.
The preflight callers (install, ci, libnpmexec) pass readFromDisk: false, so this line never probes node.path for binding.gyp. That is right for extracted packages, where node.path can hold an older same-name/version install. A directory dependency is linked in place and never extracted, so node.path is exactly what will be built.
Its package.json fields are already current in the tree (load-virtual re-reads link targets outside node_modules, and nodeFromSpec reads the directory). binding.gyp is the only signal preflight loses, and the lockfile can't stand in for it. A real lockfile entry for such a dependency is just "native": { "version": "1.0.0" } plus the link entry, with no hasInstallScript.
Repro (strict-allow-scripts, native/ has only package.json + binding.gyp, no allowScripts entry, no gypfile: false):
- base:
ESTRICTALLOWSCRIPTS, no link, no lockfile written - this head:
npm installlinks the package and writes the lockfile, then only warns (native@1.0.0 (install: node-gyp rebuild)).npm cihas already removednode_modules.
This isn't an execution bypass, because allowScripts still blocks the script at rebuild. It does break strict mode's reject-before-mutation contract.
| let isGyp = readFromDisk && !scripts.preinstall && !scripts.install && gypfile !== false && | |
| // A directory dependency is linked in place and never replaced, so unlike an | |
| // extracted package its binding.gyp is always the one that will be built. | |
| const probeDisk = readFromDisk || node.linksIn?.size > 0 | |
| let isGyp = probeDisk && !scripts.preinstall && !scripts.install && gypfile !== false && | |
| await isNodeGypPackage(node.path).catch(() => false) |
linksIn is non-empty only for link targets. Preflight runs on the regular ideal tree (the isolated tree is swapped in later, inside reify()), so store links never show up here. The readFromDisk guard on the metadata recovery below stays as is. The ?. is there because the unit tests use plain-object nodes.
Regression tests
workspaces/arborist/test/install-scripts.js, after "preflight does not recover an opt-out from an older installed package". This also keeps the new linksIn branch covered.
t.test('preflight probes directory dependencies for binding.gyp in place', async t => {
const getInstallScripts = require('../lib/install-scripts.js')
// A directory dependency is linked in place, so its binding.gyp is the one
// that will be built. An extracted package can still be replaced by reify.
const linksIn = new Set([{ isLink: true }])
const cases = [
{ name: 'native build', manifest: {}, linked: { install: 'node-gyp rebuild' } },
{ name: 'opt-out', manifest: { gypfile: false }, linked: {} },
]
for (const { name, manifest, linked } of cases) {
await t.test(name, async t => {
const path = t.testdir({
'package.json': JSON.stringify({ name: 'pkg', version: '1.0.0', ...manifest }),
'binding.gyp': '',
})
const node = { path, package: { name: 'pkg', version: '1.0.0', ...manifest } }
t.strictSame(await getInstallScripts({ ...node, linksIn }, { readFromDisk: false }),
linked, 'directory dependency')
t.strictSame(await getInstallScripts(node, { readFromDisk: false }),
{}, 'extracted package')
})
}
})test/lib/commands/install.js, before exec commands:
t.test('strict preflight reads directory dependencies in place', async t => {
for (const { name, manifest, rejects } of [
{ name: 'native build', manifest: {}, rejects: true },
{ name: 'gypfile:false', manifest: { gypfile: false }, rejects: false },
]) {
await t.test(name, async t => {
const { npm } = await loadMockNpm(t, {
config: { audit: false, 'strict-allow-scripts': true },
prefixDir: {
'package.json': JSON.stringify({
name: 'host',
version: '1.0.0',
dependencies: { native: 'file:./native' },
}),
native: {
'package.json': JSON.stringify({ name: 'native', version: '1.0.0', ...manifest }),
'binding.gyp': '',
},
},
})
const link = path.join(npm.prefix, 'node_modules/native')
const lockfile = path.join(npm.prefix, 'package-lock.json')
if (rejects) {
await t.rejects(npm.exec('install', []), {
code: 'ESTRICTALLOWSCRIPTS',
message: /native@1\.0\.0 \(install: node-gyp rebuild\)/,
})
t.notOk(fs.existsSync(link), 'does not link the dependency')
t.notOk(fs.existsSync(lockfile), 'does not write a lockfile')
} else {
await npm.exec('install', [])
t.ok(fs.lstatSync(link).isSymbolicLink(), 'links the dependency')
}
})
}
})ci and libnpmexec variants follow the same shape.
Arborist reads the installed package before inferring
node-gyp rebuild, so lockfile installs respectgypfile: falseand avoid false blocked-script warnings. The fix covers linked installs andscripts: null, with regression tests for stale script flags. Explicit scripts and the lockfile format are unchanged.References
Fixes #9837