From 7b5eb4074587ad783f3efad7307a4e317a9a2a9b Mon Sep 17 00:00:00 2001 From: iFwu Date: Mon, 20 Apr 2026 19:14:19 +0800 Subject: [PATCH] fix: override host-specific variables when reading headers config.gypi The official Node release headers tarball is a single universal artifact shipped to all platforms, but its embedded config.gypi reflects the build host of the Node release-engineering machine (currently Linux x64 / GCC). When a consumer on a different host inherits it verbatim via --disturl or --nodedir (PR #2497), host-specific fields end up wrong: * host_arch is x64 even on arm64 hosts * clang is 0 even when the local toolchain is clang * llvm_version, xcode_version, arm_fpu, gas_version, shlib_suffix similarly reflect the build farm rather than the local host The most visible symptom is on macOS arm64, where clang=0 silently drops the `clang==1` branches in common.gypi (notably -std=gnu++20). C++ compilation then falls back to C++03 and node-addon-api/napi.h fails with "no template named initializer_list", "unknown type name constexpr", etc. Affected scope: anyone with disturl set in .npmrc or env (China mirrors such as npmmirror/taobao, self-hosted Nexus/JFrog with disturl rewriting, Electron projects via electron-rebuild). See electron/rebuild#1209 for the original report. Override only the host-specific variables from process.config after parsing the headers' config.gypi. PR #2497's intent is preserved: target build config (v8 features, bundled vs shared deps, node_module_version, etc.) still comes from the headers tarball; only host fields are corrected. Verified that the fix is a no-op on Linux x64 (all 7 fields already match between cache and process.config) and necessary on macOS arm64 (all 7 fields mismatched). Refs: #2497, electron/rebuild#1209 Signed-off-by: iFwu Made-with: Cursor --- lib/create-config-gypi.js | 36 ++++++++++++++- .../include/node/config.gypi | 14 ++++++ test/test-create-config-gypi.js | 46 +++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/nodedir-mismatched-host/include/node/config.gypi diff --git a/lib/create-config-gypi.js b/lib/create-config-gypi.js index 01a820e9f2..a5f8d4a6a5 100644 --- a/lib/create-config-gypi.js +++ b/lib/create-config-gypi.js @@ -15,6 +15,40 @@ function parseConfigGypi (config) { return JSON.parse(config) } +// Variables that describe the build host (the machine running node-gyp), not +// the target Node binary. The official Node release headers tarball is a +// single universal artifact whose embedded config.gypi reflects the build +// farm host (currently Linux x64 / GCC), so when a consumer on a different +// platform inherits it verbatim via --disturl/--nodedir, these fields end up +// wrong. The most visible symptom on macOS arm64 is `clang=0` silently +// dropping the `clang==1` branches in common.gypi (e.g. `-std=gnu++20`), +// breaking node-addon-api compilation. See PR #2497 for the original code +// path and electron/rebuild#1209 for the first user report. +const HOST_SPECIFIC_VARIABLES = [ + 'host_arch', + 'clang', + 'llvm_version', + 'xcode_version', + 'arm_fpu', + 'gas_version', + 'shlib_suffix' +] + +function overrideHostSpecificVariables (config) { + if (!config || !config.variables || !process.config || !process.config.variables) { + return config + } + for (const key of HOST_SPECIFIC_VARIABLES) { + const value = process.config.variables[key] + if (value !== undefined) { + config.variables[key] = value + } else { + delete config.variables[key] + } + } + return config +} + async function getBaseConfigGypi ({ gyp, nodeDir }) { // try reading $nodeDir/include/node/config.gypi first when: // 1. --dist-url or --nodedir is specified @@ -25,7 +59,7 @@ async function getBaseConfigGypi ({ gyp, nodeDir }) { try { const baseConfigGypiPath = path.resolve(nodeDir, 'include/node/config.gypi') const baseConfigGypi = await fs.readFile(baseConfigGypiPath) - return parseConfigGypi(baseConfigGypi.toString()) + return overrideHostSpecificVariables(parseConfigGypi(baseConfigGypi.toString())) } catch (err) { log.warn('read config.gypi', err.message) } diff --git a/test/fixtures/nodedir-mismatched-host/include/node/config.gypi b/test/fixtures/nodedir-mismatched-host/include/node/config.gypi new file mode 100644 index 0000000000..7dd7bcb1e5 --- /dev/null +++ b/test/fixtures/nodedir-mismatched-host/include/node/config.gypi @@ -0,0 +1,14 @@ +# Test fixture: mimics the official Node release headers tarball, whose +# embedded config.gypi is produced on a Linux x64 / GCC build farm and ships +# unchanged to all platforms. The host-specific fields here MUST be +# overridden by process.config when running on a different host. +{ + 'variables': { + 'host_arch': 'x64', + 'clang': 0, + 'llvm_version': '0.0', + 'gas_version': '2.38', + 'shlib_suffix': 'so.137', + 'build_with_electron': true + } +} diff --git a/test/test-create-config-gypi.js b/test/test-create-config-gypi.js index 3c77b87859..23e06bb424 100644 --- a/test/test-create-config-gypi.js +++ b/test/test-create-config-gypi.js @@ -52,6 +52,52 @@ describe('create-config-gypi', function () { assert.strictEqual(config.variables.build_with_electron, undefined) }) + it('config.gypi overrides host-specific vars from process.config when nodedir is set', async function () { + // The fixture mimics a Linux x64 / GCC build farm headers tarball. When + // running on a different host (e.g. macOS arm64 / clang), the host-specific + // fields must come from process.config, not from the headers tarball, + // otherwise binding.gyp / common.gypi `if (clang==1)` branches break + // (e.g. -std=gnu++20 is silently dropped, breaking node-addon-api). + const nodeDir = path.join(__dirname, 'fixtures', 'nodedir-mismatched-host') + + const prog = gyp() + prog.parseArgv(['_', '_', `--nodedir=${nodeDir}`]) + + const config = await getCurrentConfigGypi({ gyp: prog, nodeDir, vsInfo: {} }) + + // target build config from headers is still preserved (PR #2497 intent). + assert.strictEqual(config.variables.build_with_electron, true) + + // host-specific fields come from process.config. + assert.strictEqual(config.variables.host_arch, process.config.variables.host_arch) + assert.strictEqual(config.variables.clang, process.config.variables.clang) + assert.strictEqual(config.variables.llvm_version, process.config.variables.llvm_version) + + // fields that are present in headers but absent in process.config must be + // deleted (e.g. gas_version is Linux-only and meaningless on macOS). + if (process.config.variables.gas_version === undefined) { + assert.strictEqual('gas_version' in config.variables, false) + } + if (process.config.variables.xcode_version === undefined) { + assert.strictEqual('xcode_version' in config.variables, false) + } + }) + + it('config.gypi with --force-process-config bypasses host override too (back-compat)', async function () { + const nodeDir = path.join(__dirname, 'fixtures', 'nodedir-mismatched-host') + + const prog = gyp() + prog.parseArgv(['_', '_', '--force-process-config', `--nodedir=${nodeDir}`]) + + const config = await getCurrentConfigGypi({ gyp: prog, nodeDir, vsInfo: {} }) + + // --force-process-config still skips reading the headers entirely. + assert.strictEqual(config.variables.build_with_electron, undefined) + // And of course the host fields are from process.config (always were). + assert.strictEqual(config.variables.host_arch, process.config.variables.host_arch) + assert.strictEqual(config.variables.clang, process.config.variables.clang) + }) + it('config.gypi parsing', function () { const str = "# Some comments\n{'variables': {'multiline': 'A'\n'B'}}" const config = parseConfigGypi(str)