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)