mirror of
https://github.com/wekan/wekan.git
synced 2026-11-05 15:45:01 -05:00
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:
1 parent
34924a5f3d
commit
34a33f7b14
5 files changed
+363
-46
No files matched your search
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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 ─
|
||||
|
||||
@@ -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 ─────────────────────
|
||||
|
||||
Reference in new issue
Block a user