diff --git a/lib/configure.js b/lib/configure.js index ee672cfbf2..361244a37b 100644 --- a/lib/configure.js +++ b/lib/configure.js @@ -311,9 +311,37 @@ async function configure (gyp, argv) { process.env.PYTHONPATH = pypath.join(win ? ';' : ':') await new Promise((resolve, reject) => { - const cp = gyp.spawn(python, argv) + // Capture gyp's stderr (still forwarded to the terminal) so we can + // recognise known failure modes and print actionable hints. + const cp = gyp.spawn(python, argv, { stdio: ['inherit', 'inherit', 'pipe'] }) + let gypStderr = '' + if (cp.stderr) { + cp.stderr.on('data', (chunk) => { + gypStderr += chunk + process.stderr.write(chunk) + }) + } cp.on('exit', (code) => { if (code !== 0) { + // Only hint when the undefined variable comes from the Node/NDK + // header toolchain (android_ndk_* variables, or any variable while + // loading common.gypi), not when a binding.gyp itself references an + // undefined variable — that is a project bug and this hint would + // send the user down the wrong path. + const ndkHint = process.platform === 'android' && + isNdkUndefinedVariableHint(gypStderr) + if (ndkHint) { + log.error('gyp', [ + 'The Node headers downloaded for this target reference NDK', + 'variables, but no NDK is installed. On Android (e.g. Termux)', + 'you usually want to compile against the system toolchain instead:', + 'make sure node-gyp uses the locally installed headers', + '($PREFIX/include/node) rather than the official ones downloaded', + 'from nodejs.org, e.g. by rebuilding Node with', + '--use-prefix-to-find-headers or by passing --nodedir pointing at', + 'your local headers.' + ].join('\n')) + } reject(new Error('`gyp` failed with exit code: ' + code)) } else { // we're done @@ -324,5 +352,20 @@ async function configure (gyp, argv) { } } +/** + * Detect the "Undefined variable" gyp failure that comes from the Node/NDK + * header toolchain (android_ndk_* variables, or any undefined variable while + * loading common.gypi) rather than from the project's own binding.gyp. + * Exported for tests. + * @param {string} gypStderr - captured gyp stderr (also forwarded to the + * terminal byte-for-byte by the caller). + * @returns {boolean} + */ +function isNdkUndefinedVariableHint (gypStderr) { + return /Undefined variable android_ndk_/.test(gypStderr) || + /Undefined variable [A-Za-z0-9_]+ in .*common\.gypi/.test(gypStderr) +} + module.exports = configure +module.exports.isNdkUndefinedVariableHint = isNdkUndefinedVariableHint module.exports.usage = 'Generates ' + (win ? 'MSVC project files' : 'a Makefile') + ' for the current module' diff --git a/test/test-ndk-undefined-variable-hint.js b/test/test-ndk-undefined-variable-hint.js new file mode 100644 index 0000000000..c4f69e39d6 --- /dev/null +++ b/test/test-ndk-undefined-variable-hint.js @@ -0,0 +1,38 @@ +'use strict' + +const { describe, it } = require('mocha') +const assert = require('assert') +const configure = require('../lib/configure') + +const isHint = configure.isNdkUndefinedVariableHint + +describe('isNdkUndefinedVariableHint', function () { + it('detects the android_ndk_path failure from the official headers', function () { + const stderr = 'gyp: Undefined variable android_ndk_path in binding.gyp while trying to load binding.gyp\n' + assert.strictEqual(isHint(stderr), true) + }) + + it('detects other android_ndk_* variables', function () { + const stderr = 'gyp: Undefined variable android_ndk_include_dir in binding.gyp while trying to load binding.gyp\n' + assert.strictEqual(isHint(stderr), true) + }) + + it('detects an undefined variable while loading common.gypi', function () { + const stderr = 'gyp: Undefined variable some_var in /usr/include/node/common.gypi while trying to load binding.gyp\n' + assert.strictEqual(isHint(stderr), true) + }) + + it('ignores an undefined variable in the project binding.gyp', function () { + const stderr = 'gyp: Undefined variable my_custom_flag in binding.gyp while trying to load binding.gyp\n' + assert.strictEqual(isHint(stderr), false) + }) + + it('ignores unrelated gyp errors', function () { + const stderr = "gyp: 'x' doesn't look like a valid filename\n" + assert.strictEqual(isHint(stderr), false) + }) + + it('ignores empty stderr', function () { + assert.strictEqual(isHint(''), false) + }) +})