Windows win32 and ARM64 bundles: compile bcrypt and argon2 with node-gyp itself.

v12.33's build-win32 and build-win-arm64 both failed their addon check:

  ...accounts-password/node_modules/argon2 has no arm64 Windows addon to load

The jobs relied on npm to compile the addons that have no prebuild for their
CPU (argon2 0.44 for win32-arm64; bcrypt and argon2 for win32-ia32) during
the programs/server install, through Meteor's npm-rebuild.js. The release
jobs use npm 12, which ignores npm_config_arch, npm_config_target_arch and
npm_config_nodedir ("Unknown env config", which it says will become an
error) and blocks install scripts outside allowScripts ("install scripts
blocked"). So nothing was compiled.

releases/compile-native-addons.mjs now runs node-gyp itself, using the
node-gyp that the install brings (pinned to 13.x, which finds Visual
Studio 2026), with --arch for the target CPU. win32 also passes the nodedir
that holds the fork's ia32 node.lib. It does this for every copy of each
named package that has a binding.gyp and no win32-<arch> prebuild, before
prune-build-only-modules removes node-gyp. It fails if node-gyp fails or
leaves no .node. Both jobs no longer set the npm_config_ variables.

Checked with the real node-gyp 13.0.2, argon2 0.44.0 and bcrypt 6.0.0
(with their win32 prebuilds removed, compiled for this machine): both
compiled, loaded, and hashed and verified a password.
tests/compileNativeAddons.test.cjs covers the plan, the node-gyp arguments
and every way the compile could quietly not happen.
tests/win32BundleBuild.test.cjs pins the step in both jobs and refuses
npm_config_arch/_target_arch/_nodedir in any workflow.
tests/releaseBundleCoverage.test.cjs used to require npm_config_arch: ia32,
and now requires the node-gyp cross-build, with the reason recorded in the
test.

Thanks to xet7 !
This commit is contained in:
Lauri Ojansivu committed 2026-10-11 23:34:51 +03:00
1 parent 34924a5f3d
commit 34a33f7b14
5 files changed
+363 -46

No files matched your search

+27 -25
View File
@@ -1714,9 +1714,9 @@ jobs:
# The runner is 64-bit and the bundle is 32-bit. There is NO 32-bit Windows
# Node 26 to install (that is the whole reason the fork exists), so setup-node
# runs the runner's normal x64 Node to DRIVE the build, and the native modules
# (bcrypt) are cross-compiled to ia32 by passing npm_config_arch=ia32 in the
# rebuild step below - node-gyp then fetches the ia32 headers and builds Win32
# binaries. The shipped node.exe is the fork's 32-bit node-win32.exe.
# (bcrypt, argon2) are cross-compiled to ia32 by compile-native-addons.mjs in
# the rebuild step below - node-gyp --arch=ia32 against a nodedir holding the
# fork's ia32 node.lib. The shipped node.exe is the fork's 32-bit node-win32.exe.
# Windows on ARM. Every binary this bundle needs is published: nodejs.org
# builds node-<version>-win-arm64.zip itself, wekan/FerretDB publishes
# ferretdb-win-arm64.exe and wekan/mongo-tools-patches publishes the tools
@@ -1843,21 +1843,20 @@ jobs:
- name: Rebuild native modules for Windows
if: steps.preflight.outputs.skip != 'true'
shell: bash
# The runner and its node are x64. Without these, node-gyp-build's
# install test loads argon2's win32-x64 prebuild in that x64 node,
# passes, and compiles nothing - so v12.32's win-arm64 zip shipped no
# arm64 argon2 at all (argon2 0.44 publishes no win32-arm64 prebuild),
# and accounts-password imports it with no fallback. With them, the
# test looks for win32-arm64, finds none, and node-gyp cross-compiles
# it with Visual Studio's ARM64 tools against nodejs.org's arm64 node.lib.
env:
npm_config_arch: arm64
npm_config_target_arch: arm64
# The runner and its node are x64. argon2 0.44 publishes no
# win32-arm64 prebuild, so it must be COMPILED for arm64, and
# accounts-password imports it with no fallback. npm 12 no longer does
# that during the install below: it ignores npm_config_arch ("Unknown
# env config") and blocks install scripts outside allowScripts, so
# v12.32 shipped no arm64 argon2 and v12.33 failed its addon check.
# compile-native-addons.mjs runs node-gyp itself, for arm64, before
# prune-build-only-modules removes node-gyp.
run: |
SRC="$PWD/src"
cd bundle/programs/server
rm -rf node_modules
bash "$SRC/releases/npm-retry.sh" npm install
node "$SRC/releases/compile-native-addons.mjs" ../.. arm64 bcrypt argon2
# ../.. is the bundle root: this step is inside programs/server.
node "$SRC/releases/prune-build-only-modules.mjs" ../..
node "$SRC/releases/bump-bundle-npm-deps.mjs" ../..
@@ -2136,9 +2135,9 @@ jobs:
# The runner is 64-bit and the bundle is 32-bit. There is NO 32-bit Windows
# Node 26 to install (that is the whole reason the fork exists), so setup-node
# runs the runner's normal x64 Node to DRIVE the build, and the native modules
# (bcrypt) are cross-compiled to ia32 by passing npm_config_arch=ia32 in the
# rebuild step below - node-gyp then fetches the ia32 headers and builds Win32
# binaries. The shipped node.exe is the fork's 32-bit node-win32.exe.
# (bcrypt, argon2) are cross-compiled to ia32 by compile-native-addons.mjs in
# the rebuild step below - node-gyp --arch=ia32 against a nodedir holding the
# fork's ia32 node.lib. The shipped node.exe is the fork's 32-bit node-win32.exe.
build-win32:
needs: [prepare, build-amd64, release]
runs-on: windows-latest
@@ -2153,7 +2152,7 @@ jobs:
# x64 Node (the runner's own): there is no 32-bit Windows Node 26 to
# install. It only DRIVES npm/node-gyp; the ia32 output comes from
# npm_config_arch=ia32 in the rebuild step.
# node-gyp --arch=ia32 (compile-native-addons.mjs) in the rebuild step.
- name: Audit telemetry source before release work
shell: bash
run: python3 src/releases/check-telemetry.py --source src
@@ -2255,16 +2254,14 @@ jobs:
- name: Rebuild native modules for Windows (32-bit)
if: steps.preflight.outputs.skip != 'true'
shell: bash
# The x64 runner Node drives node-gyp, and npm_config_arch=ia32 makes it
# cross-build 32-bit (Win32) native modules (bcrypt). But an ia32 addon
# The x64 runner Node drives node-gyp, and --arch=ia32 makes it
# cross-build 32-bit (Win32) native modules (bcrypt, argon2). An ia32 addon
# must LINK against an ia32 node.lib, and nodejs.org dropped 32-bit Windows
# in Node 23 - so node-gyp cannot fetch win-x86/node.lib. Build a nodedir
# it CAN use: node headers (arch-independent, from nodejs.org) plus the
# node-win32.lib that wekan/node-patches publishes beside its
# node-win32.exe, and point node-gyp at it with nodedir.
env:
npm_config_arch: ia32
npm_config_target_arch: ia32
NODE_FULL: ${{ steps.preflight.outputs.node_full }}
run: |
SRC="$PWD/src"
@@ -2292,11 +2289,17 @@ jobs:
bash "$SRC/releases/fetch.sh" -o "$ND/ia32/node.lib" \
"https://github.com/wekan/node-patches/releases/download/${NODE_FULL}/node-win32.lib"
cp "$ND/ia32/node.lib" "$ND/Release/node.lib"
export npm_config_nodedir="$(cygpath -w "$ND")"
echo "Cross-building ia32 native modules against nodedir $npm_config_nodedir (fork node.lib)."
NODEDIR="$(cygpath -w "$ND")"
echo "Cross-building ia32 native modules against nodedir $NODEDIR (fork node.lib)."
cd bundle/programs/server
rm -rf node_modules
bash "$SRC/releases/npm-retry.sh" npm install
# npm 12 ignores npm_config_arch/npm_config_nodedir and blocks install
# scripts outside allowScripts, so the install above compiles nothing
# (v12.33). bcrypt and argon2 publish no win32-ia32 prebuild: compile
# them here with node-gyp, for ia32 against this nodedir, before
# prune-build-only-modules removes node-gyp.
node "$SRC/releases/compile-native-addons.mjs" ../.. ia32 --nodedir "$NODEDIR" bcrypt argon2
# ../.. is the bundle root: this step is inside programs/server.
node "$SRC/releases/prune-build-only-modules.mjs" ../..
node "$SRC/releases/bump-bundle-npm-deps.mjs" ../..
@@ -2314,8 +2317,7 @@ jobs:
# packages load it through Npm.require(). This removes only what it can prove
# nothing requires, and refuses any policy entry the graph says IS reached.
node "$SRC/releases/prune-unreachable-npm.mjs" ../..
# bcrypt and argon2 publish no win32-ia32 prebuild, so this rebuild
# must COMPILE both; the next step reads the binaries to prove it did.
# The next step reads the compiled binaries to prove they are ia32.
- name: Add sharp for Windows 32-bit (win32), and check the native addons
if: steps.preflight.outputs.skip != 'true'
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env node
// Compile a bundle's native addons for a Windows CPU that has no prebuild.
//
// node releases/compile-native-addons.mjs <bundle dir> <ia32|arm64|x64> [--nodedir DIR] <package ...>
//
// Why. accounts-password imports bcrypt and argon2 with no fallback, and they
// publish no win32-ia32 prebuild, and argon2 0.44 none for win32-arm64. The
// Windows jobs relied on npm's lifecycle to compile them during the
// programs/server install (Meteor's npm-rebuild.js runs `npm rebuild` in each
// rebuild directory). That stopped working with npm 12, which the release jobs
// use: it ignores npm_config_arch / npm_config_target_arch / npm_config_nodedir
// ("Unknown env config") and blocks install scripts outside allowScripts
// ("install scripts blocked"). In v12.33 both build-win32 and build-win-arm64
// compiled nothing, and check-addon-arch.mjs refused the bundle:
//
// ...accounts-password/node_modules/argon2 has no arm64 Windows addon to load
//
// So the compile no longer depends on npm. This runs node-gyp itself - the one
// programs/server's install brought, pinned by bump-bundle-node-gyp.mjs to a
// version that finds Visual Studio 2026 - with the target CPU on the command
// line, for every copy of each named package that has a binding.gyp and no
// prebuild for win32-<arch>. node-gyp-build loads build/Release before
// prebuilds/, so the result is what Node opens. Run it after the programs/server
// install and BEFORE prune-build-only-modules.mjs, which removes node-gyp.
import { spawnSync } from 'node:child_process';
import { existsSync, readdirSync } from 'node:fs';
import { join, relative, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
export const ARCHES = ['ia32', 'x64', 'arm64'];
// Every node_modules/<name> under root, for a plain or scoped package name.
export function packageCopies(root, name) {
const seg = name.split('/');
const out = [];
(function walk(dir) {
let entries;
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const e of entries) {
if (!e.isDirectory()) continue;
const p = join(dir, e.name);
if (e.name === 'node_modules') {
const candidate = join(p, ...seg);
if (existsSync(join(candidate, 'package.json'))) out.push(candidate);
}
walk(p);
}
})(root);
return [...new Set(out)];
}
// node-gyp-build's rule: a prebuilds/<platform>-<arch>[+arch] directory with a
// .node in it is what it loads instead of compiling.
export function hasPrebuild(dir, arch) {
const base = join(dir, 'prebuilds');
if (!existsSync(base)) return false;
return readdirSync(base).some((d) => {
const [platform, archs = ''] = d.split('-');
if (platform !== 'win32' || !archs.split('+').includes(arch)) return false;
try {
return readdirSync(join(base, d)).some((f) => f.endsWith('.node'));
} catch {
return false;
}
});
}
export function plan(bundle, arch, packages) {
const server = join(bundle, 'programs', 'server');
const compile = [];
const skip = [];
const missing = [];
for (const name of packages) {
const copies = packageCopies(join(server, 'npm'), name);
if (copies.length === 0) missing.push(name);
for (const dir of copies) {
if (!existsSync(join(dir, 'binding.gyp'))) skip.push({ dir, why: 'no binding.gyp' });
else if (hasPrebuild(dir, arch)) skip.push({ dir, why: `prebuild for win32-${arch}` });
else compile.push(dir);
}
}
return { compile, skip, missing };
}
export function nodeGypPath(bundle) {
return join(bundle, 'programs', 'server', 'node_modules', 'node-gyp', 'bin', 'node-gyp.js');
}
export function compileNativeAddons(bundle, arch, packages, { nodedir = '', run = spawnSync, log = console.log } = {}) {
if (!ARCHES.includes(arch)) throw new Error(`unknown arch ${arch}; expected one of ${ARCHES.join(', ')}`);
const gyp = nodeGypPath(bundle);
const { compile, skip, missing } = plan(bundle, arch, packages);
if (missing.length) throw new Error(`not in the bundle: ${missing.join(', ')}`);
for (const s of skip) log(`skip ${relative(bundle, s.dir)} (${s.why})`);
if (compile.length && !existsSync(gyp)) {
throw new Error(`${relative(bundle, gyp)} is missing; run this before prune-build-only-modules.mjs`);
}
for (const dir of compile) {
const args = [gyp, 'rebuild', `--arch=${arch}`, `--directory=${dir}`];
if (nodedir) args.push(`--nodedir=${nodedir}`);
log(`compile ${relative(bundle, dir)} for win32-${arch}`);
const r = run(process.execPath, args, { stdio: 'inherit', cwd: dir, env: { ...process.env } });
if (r.error || r.status !== 0) {
throw new Error(`node-gyp failed for ${relative(bundle, dir)} (${r.error ? r.error.message : `exit ${r.status}`})`);
}
const out = join(dir, 'build', 'Release');
if (!existsSync(out) || !readdirSync(out).some((f) => f.endsWith('.node'))) {
throw new Error(`node-gyp left no .node in ${relative(bundle, out)}`);
}
}
return { compiled: compile.map((d) => relative(bundle, d)), skipped: skip.map((s) => relative(bundle, s.dir)) };
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
const args = process.argv.slice(2);
const ni = args.indexOf('--nodedir');
const nodedir = ni === -1 ? '' : args.splice(ni, 2)[1];
const [bundle, arch, ...packages] = args;
if (!bundle || !arch || packages.length === 0) {
console.error('usage: compile-native-addons.mjs <bundle dir> <ia32|arm64|x64> [--nodedir DIR] <package ...>');
process.exit(2);
}
try {
const { compiled } = compileNativeAddons(bundle, arch, packages, { nodedir });
console.log(`compile-native-addons: compiled ${compiled.length} addon(s) for win32-${arch}.`);
} catch (e) {
console.log(`::error::compile-native-addons: ${e.message}`);
process.exit(1);
}
}
+160
View File
@@ -0,0 +1,160 @@
'use strict';
(async () => {
// Plain-Node unit test for releases/compile-native-addons.mjs.
// Run: node tests/compileNativeAddons.test.cjs
//
// In v12.33 build-win32 and build-win-arm64 compiled no native addon: npm 12
// ignores npm_config_arch / npm_config_nodedir and blocks install scripts
// outside allowScripts, so the install the jobs relied on built nothing, and
// check-addon-arch.mjs refused both bundles. The script runs node-gyp itself.
// Here node-gyp is replaced by a function that records its arguments and
// writes what node-gyp would.
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { plan, hasPrebuild, compileNativeAddons, packageCopies } =
await import('../releases/compile-native-addons.mjs');
let passed = 0;
function test(name, fn) {
fn();
passed += 1;
console.log(' ok -', name);
}
const AP = 'programs/server/npm/node_modules/meteor/accounts-password/node_modules';
const GYP = 'programs/server/node_modules/node-gyp/bin/node-gyp.js';
function tree(files) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'compile-addons-'));
for (const [rel, content] of Object.entries(files)) {
const f = path.join(dir, rel);
fs.mkdirSync(path.dirname(f), { recursive: true });
fs.writeFileSync(f, content);
}
return dir;
}
function withTree(files, fn) {
const dir = tree(files);
try {
fn(dir);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}
// The accounts-password tree as the Windows jobs have it after the install.
const BUNDLE = {
[GYP]: '// node-gyp',
[`${AP}/bcrypt/package.json`]: '{"name":"bcrypt"}',
[`${AP}/bcrypt/binding.gyp`]: '{}',
[`${AP}/bcrypt/prebuilds/win32-x64/bcrypt.node`]: 'MZ',
[`${AP}/bcrypt/prebuilds/win32-arm64/bcrypt.node`]: 'MZ',
[`${AP}/argon2/package.json`]: '{"name":"argon2"}',
[`${AP}/argon2/binding.gyp`]: '{}',
[`${AP}/argon2/prebuilds/win32-x64/argon2.glibc.node`]: 'MZ',
[`${AP}/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node`]: 'ELF',
};
// A node-gyp that records what it was asked and leaves build/Release behind.
function fakeGyp({ status = 0, writes = true } = {}) {
const calls = [];
const run = (exe, args, opts) => {
calls.push({ exe, args, cwd: opts.cwd });
const dir = args.find((a) => a.startsWith('--directory=')).slice('--directory='.length);
if (writes) {
fs.mkdirSync(path.join(dir, 'build/Release'), { recursive: true });
fs.writeFileSync(path.join(dir, 'build/Release/addon.node'), 'MZ');
}
return { status };
};
return { run, calls };
}
const quiet = { log: () => {} };
const rel = (dir, p) => path.relative(dir, p).split(path.sep).join('/');
console.log('compileNativeAddons:');
test('only what has no prebuild for the target CPU is compiled', () => {
withTree(BUNDLE, (d) => {
const arm = plan(d, 'arm64', ['bcrypt', 'argon2']);
assert.deepStrictEqual(arm.compile.map((p) => rel(d, p)), [`${AP}/argon2`],
'arm64: argon2 0.44 has no win32-arm64 prebuild, bcrypt 6 has one');
const ia32 = plan(d, 'ia32', ['bcrypt', 'argon2']);
assert.deepStrictEqual(ia32.compile.map((p) => rel(d, p)).sort(), [`${AP}/argon2`, `${AP}/bcrypt`],
'ia32: neither publishes a win32-ia32 prebuild');
assert.deepStrictEqual(plan(d, 'x64', ['bcrypt', 'argon2']).compile, [], 'x64: both prebuilt');
});
});
test('prebuild directories are read the way node-gyp-build reads them', () => {
withTree({
'a/prebuilds/win32-x64+arm64/a.node': 'MZ',
'b/prebuilds/win32-arm64/README': 'no addon here',
'c/prebuilds/linux-arm64/c.node': 'ELF',
}, (d) => {
assert.strictEqual(hasPrebuild(path.join(d, 'a'), 'arm64'), true, 'multi-arch tuple');
assert.strictEqual(hasPrebuild(path.join(d, 'b'), 'arm64'), false, 'a directory with no .node');
assert.strictEqual(hasPrebuild(path.join(d, 'c'), 'arm64'), false, 'another platform');
});
});
test('node-gyp is asked for the target CPU, in the package, with the nodedir', () => {
withTree(BUNDLE, (d) => {
const gyp = fakeGyp();
const r = compileNativeAddons(d, 'ia32', ['bcrypt', 'argon2'], { nodedir: 'D:\\a\\_temp\\node-ia32', run: gyp.run, ...quiet });
assert.strictEqual(r.compiled.length, 2);
for (const call of gyp.calls) {
assert.strictEqual(call.exe, process.execPath, 'node-gyp.js runs under this node, no shell');
assert.ok(call.args[0].endsWith(path.join('node-gyp', 'bin', 'node-gyp.js')));
assert.deepStrictEqual(call.args.slice(1, 3), ['rebuild', '--arch=ia32']);
assert.ok(call.args.includes(`--directory=${call.cwd}`));
assert.ok(call.args.includes('--nodedir=D:\\a\\_temp\\node-ia32'));
}
});
withTree(BUNDLE, (d) => {
const gyp = fakeGyp();
compileNativeAddons(d, 'arm64', ['bcrypt', 'argon2'], { run: gyp.run, ...quiet });
assert.strictEqual(gyp.calls.length, 1);
assert.ok(!gyp.calls[0].args.some((a) => a.startsWith('--nodedir')), 'arm64 uses nodejs.org\'s own arm64 node.lib');
});
});
// Negative: every way the compile can quietly not happen is an error.
test('a failed compile, no output, no node-gyp or no package is an error', () => {
withTree(BUNDLE, (d) => {
assert.throws(() => compileNativeAddons(d, 'arm64', ['argon2'], { run: fakeGyp({ status: 1 }).run, ...quiet }),
/node-gyp failed for .*argon2 \(exit 1\)/);
});
withTree(BUNDLE, (d) => {
assert.throws(() => compileNativeAddons(d, 'arm64', ['argon2'], { run: fakeGyp({ writes: false }).run, ...quiet }),
/left no \.node/);
});
const noGyp = { ...BUNDLE };
delete noGyp[GYP];
withTree(noGyp, (d) => {
assert.throws(() => compileNativeAddons(d, 'arm64', ['argon2'], { run: fakeGyp().run, ...quiet }),
/before prune-build-only-modules/);
});
withTree(BUNDLE, (d) => {
assert.throws(() => compileNativeAddons(d, 'arm64', ['argon3'], { run: fakeGyp().run, ...quiet }),
/not in the bundle: argon3/);
assert.throws(() => compileNativeAddons(d, 'mips', ['argon2'], quiet), /unknown arch/);
});
});
test('scoped packages and nested copies are all found', () => {
withTree({
'programs/server/npm/node_modules/@scope/pkg/package.json': '{}',
'programs/server/npm/node_modules/x/node_modules/@scope/pkg/package.json': '{}',
}, (d) => {
assert.strictEqual(packageCopies(path.join(d, 'programs/server/npm'), '@scope/pkg').length, 2);
});
});
console.log(`\ncompileNativeAddons: all ${passed} tests passed`);
})().catch((err) => {
console.error(err);
process.exit(1);
});
+11 -4
View File
@@ -88,12 +88,19 @@ test('win32 cross-builds ia32 native modules (no 32-bit Windows Node exists to i
// There is NO 32-bit Windows Node 26 to install - setup-node architecture: x86
// fails with "Unable to find Node version '24' for platform win32 and
// architecture x86". So the x64 runner Node drives node-gyp, and the modules
// (bcrypt) are cross-compiled to ia32 via npm_config_arch=ia32, matching the
// fork's 32-bit node.exe the bundle ships.
// (bcrypt, argon2) are cross-compiled to ia32, matching the fork's 32-bit
// node.exe the bundle ships.
//
// This used to require npm_config_arch: ia32. That was the right pin until the
// release jobs moved to npm 12, which ignores npm_config_arch ("Unknown env
// config") and blocks install scripts outside allowScripts - so v12.33's
// build-win32 compiled nothing. The cross-build now calls node-gyp itself
// with --arch=ia32 (releases/compile-native-addons.mjs), and that is what this
// pins; tests/win32BundleBuild.test.cjs pins where in the step it runs.
assert.ok(!/architecture:\s*x86/.test(body),
'build-win32 must NOT ask setup-node for architecture: x86 - no 32-bit Windows Node 26 exists');
assert.ok(/npm_config_arch:\s*ia32/.test(body),
'build-win32 must set npm_config_arch: ia32 so node-gyp cross-builds 32-bit native modules');
assert.ok(/compile-native-addons\.mjs" \.\.\/\.\. ia32 --nodedir/.test(body),
'build-win32 must cross-build its 32-bit native modules with node-gyp --arch=ia32');
});
// ── release-notes waits for every bundle so the provenance table is complete ─
+30 -17
View File
@@ -75,7 +75,7 @@ test('build-win32 gives tar no drive-letter path', () => {
'tar extracts by relative name inside the nodedir');
assert.ok(/test -f "\$ND\/include\/node\/node\.h"/.test(win32.body),
'and the headers are checked to have arrived');
assert.ok(/export npm_config_nodedir="\$\(cygpath -w "\$ND"\)"/.test(win32.body),
assert.ok(/NODEDIR="\$\(cygpath -w "\$ND"\)"/.test(win32.body),
'node-gyp gets the Windows form of the nodedir');
});
@@ -116,23 +116,36 @@ test('each Windows bundle job adds its own sharp and checks its addons', () => {
}
});
// v12.32's win-arm64 zip had no arm64 argon2: the x64 runner's node passed
// node-gyp-build's install test with argon2's win32-x64 prebuild, so nothing
// was compiled. A job whose CPU is not the runner's must say so to npm.
test('a cross-built Windows job tells npm its target CPU', () => {
const rebuildEnv = (name) => {
// v12.32's win-arm64 zip had no arm64 argon2, and in v12.33 both cross-built
// jobs compiled nothing: npm 12 ignores npm_config_arch / npm_config_nodedir
// ("Unknown env config") and blocks install scripts outside allowScripts. A job
// whose CPU is not the runner's compiles with node-gyp directly, after the
// programs/server install brought node-gyp in and before it is pruned.
const CROSS = { 'build-win-arm64': 'arm64', 'build-win32': 'ia32' };
test('each cross-built Windows job compiles its addons with node-gyp itself', () => {
for (const [name, arch] of Object.entries(CROSS)) {
const job = windowsJobs.find((j) => j.name === name);
const i = job.body.indexOf('- name: Rebuild native modules for Windows');
const step = job.body.slice(i, job.body.indexOf('\n - name:', i + 1));
return {
arch: (/npm_config_arch:\s*(\S+)/.exec(step) || [])[1],
target: (/npm_config_target_arch:\s*(\S+)/.exec(step) || [])[1],
};
};
assert.deepStrictEqual(rebuildEnv('build-win-arm64'), { arch: 'arm64', target: 'arm64' });
assert.deepStrictEqual(rebuildEnv('build-win32'), { arch: 'ia32', target: 'ia32' });
// win64 is the runner's own CPU, and must not be told another one.
assert.deepStrictEqual(rebuildEnv('build-win64'), { arch: undefined, target: undefined });
const at = (s) => job.body.indexOf(s);
const compile = at(`releases/compile-native-addons.mjs" ../.. ${arch}`);
assert.ok(compile !== -1, `${name} runs compile-native-addons.mjs for ${arch}`);
assert.ok(/compile-native-addons\.mjs"[^\n]* bcrypt argon2/.test(job.body), `${name}: for bcrypt and argon2`);
assert.ok(at('npm-retry.sh" npm install') < compile, `${name}: after the install that brings node-gyp`);
assert.ok(compile < at('prune-build-only-modules.mjs'), `${name}: before node-gyp is pruned`);
}
assert.ok(/compile-native-addons\.mjs" \.\.\/\.\. ia32 --nodedir "\$NODEDIR"/.test(
windowsJobs.find((j) => j.name === 'build-win32').body),
'win32 links against the fork\'s ia32 node.lib through --nodedir');
});
// Negative, everywhere: no workflow hands npm a config npm 12 does not know.
// It ignores them today, says it will refuse them in its next major version,
// and relying on them is what compiled nothing in v12.33.
test('no workflow relies on npm_config_arch, _target_arch or _nodedir', () => {
for (const { f, text } of workflows) {
const body = code(text);
assert.ok(!/^\s*npm_config_(?:arch|target_arch|nodedir)\s*:/m.test(body), `${f} sets an npm_config_ arch/nodedir env`);
assert.ok(!/export\s+npm_config_(?:arch|target_arch|nodedir)=/.test(body), `${f} exports an npm_config_ arch/nodedir`);
}
});
// ── check-addon-arch.mjs itself, on real PE/ELF headers ─────────────────────