Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion lib/create-config-gypi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand Down
14 changes: 14 additions & 0 deletions test/fixtures/nodedir-mismatched-host/include/node/config.gypi
Original file line number Diff line number Diff line change
@@ -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
}
}
46 changes: 46 additions & 0 deletions test/test-create-config-gypi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down