Skip to content

Commit 7b5eb40

Browse files
committed
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 <[email protected]> Made-with: Cursor
1 parent 19da158 commit 7b5eb40

3 files changed

Lines changed: 95 additions & 1 deletion

File tree

lib/create-config-gypi.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,40 @@ function parseConfigGypi (config) {
1515
return JSON.parse(config)
1616
}
1717

18+
// Variables that describe the build host (the machine running node-gyp), not
19+
// the target Node binary. The official Node release headers tarball is a
20+
// single universal artifact whose embedded config.gypi reflects the build
21+
// farm host (currently Linux x64 / GCC), so when a consumer on a different
22+
// platform inherits it verbatim via --disturl/--nodedir, these fields end up
23+
// wrong. The most visible symptom on macOS arm64 is `clang=0` silently
24+
// dropping the `clang==1` branches in common.gypi (e.g. `-std=gnu++20`),
25+
// breaking node-addon-api compilation. See PR #2497 for the original code
26+
// path and electron/rebuild#1209 for the first user report.
27+
const HOST_SPECIFIC_VARIABLES = [
28+
'host_arch',
29+
'clang',
30+
'llvm_version',
31+
'xcode_version',
32+
'arm_fpu',
33+
'gas_version',
34+
'shlib_suffix'
35+
]
36+
37+
function overrideHostSpecificVariables (config) {
38+
if (!config || !config.variables || !process.config || !process.config.variables) {
39+
return config
40+
}
41+
for (const key of HOST_SPECIFIC_VARIABLES) {
42+
const value = process.config.variables[key]
43+
if (value !== undefined) {
44+
config.variables[key] = value
45+
} else {
46+
delete config.variables[key]
47+
}
48+
}
49+
return config
50+
}
51+
1852
async function getBaseConfigGypi ({ gyp, nodeDir }) {
1953
// try reading $nodeDir/include/node/config.gypi first when:
2054
// 1. --dist-url or --nodedir is specified
@@ -25,7 +59,7 @@ async function getBaseConfigGypi ({ gyp, nodeDir }) {
2559
try {
2660
const baseConfigGypiPath = path.resolve(nodeDir, 'include/node/config.gypi')
2761
const baseConfigGypi = await fs.readFile(baseConfigGypiPath)
28-
return parseConfigGypi(baseConfigGypi.toString())
62+
return overrideHostSpecificVariables(parseConfigGypi(baseConfigGypi.toString()))
2963
} catch (err) {
3064
log.warn('read config.gypi', err.message)
3165
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Test fixture: mimics the official Node release headers tarball, whose
2+
# embedded config.gypi is produced on a Linux x64 / GCC build farm and ships
3+
# unchanged to all platforms. The host-specific fields here MUST be
4+
# overridden by process.config when running on a different host.
5+
{
6+
'variables': {
7+
'host_arch': 'x64',
8+
'clang': 0,
9+
'llvm_version': '0.0',
10+
'gas_version': '2.38',
11+
'shlib_suffix': 'so.137',
12+
'build_with_electron': true
13+
}
14+
}

test/test-create-config-gypi.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,52 @@ describe('create-config-gypi', function () {
5252
assert.strictEqual(config.variables.build_with_electron, undefined)
5353
})
5454

55+
it('config.gypi overrides host-specific vars from process.config when nodedir is set', async function () {
56+
// The fixture mimics a Linux x64 / GCC build farm headers tarball. When
57+
// running on a different host (e.g. macOS arm64 / clang), the host-specific
58+
// fields must come from process.config, not from the headers tarball,
59+
// otherwise binding.gyp / common.gypi `if (clang==1)` branches break
60+
// (e.g. -std=gnu++20 is silently dropped, breaking node-addon-api).
61+
const nodeDir = path.join(__dirname, 'fixtures', 'nodedir-mismatched-host')
62+
63+
const prog = gyp()
64+
prog.parseArgv(['_', '_', `--nodedir=${nodeDir}`])
65+
66+
const config = await getCurrentConfigGypi({ gyp: prog, nodeDir, vsInfo: {} })
67+
68+
// target build config from headers is still preserved (PR #2497 intent).
69+
assert.strictEqual(config.variables.build_with_electron, true)
70+
71+
// host-specific fields come from process.config.
72+
assert.strictEqual(config.variables.host_arch, process.config.variables.host_arch)
73+
assert.strictEqual(config.variables.clang, process.config.variables.clang)
74+
assert.strictEqual(config.variables.llvm_version, process.config.variables.llvm_version)
75+
76+
// fields that are present in headers but absent in process.config must be
77+
// deleted (e.g. gas_version is Linux-only and meaningless on macOS).
78+
if (process.config.variables.gas_version === undefined) {
79+
assert.strictEqual('gas_version' in config.variables, false)
80+
}
81+
if (process.config.variables.xcode_version === undefined) {
82+
assert.strictEqual('xcode_version' in config.variables, false)
83+
}
84+
})
85+
86+
it('config.gypi with --force-process-config bypasses host override too (back-compat)', async function () {
87+
const nodeDir = path.join(__dirname, 'fixtures', 'nodedir-mismatched-host')
88+
89+
const prog = gyp()
90+
prog.parseArgv(['_', '_', '--force-process-config', `--nodedir=${nodeDir}`])
91+
92+
const config = await getCurrentConfigGypi({ gyp: prog, nodeDir, vsInfo: {} })
93+
94+
// --force-process-config still skips reading the headers entirely.
95+
assert.strictEqual(config.variables.build_with_electron, undefined)
96+
// And of course the host fields are from process.config (always were).
97+
assert.strictEqual(config.variables.host_arch, process.config.variables.host_arch)
98+
assert.strictEqual(config.variables.clang, process.config.variables.clang)
99+
})
100+
55101
it('config.gypi parsing', function () {
56102
const str = "# Some comments\n{'variables': {'multiline': 'A'\n'B'}}"
57103
const config = parseConfigGypi(str)

0 commit comments

Comments
 (0)