diff --git a/lib/build.js b/lib/build.js index 00a0abe691..e6ec08def8 100644 --- a/lib/build.js +++ b/lib/build.js @@ -195,17 +195,29 @@ async function build (gyp, argv) { if (!win) { // Add build-time dependency symlinks (such as Python) to PATH - buildBinsDir = path.resolve('build', 'node_gyp_bins') - process.env.PATH = `${buildBinsDir}:${process.env.PATH}` - await fs.mkdir(buildBinsDir, { recursive: true }) - const symlinkDestination = path.join(buildBinsDir, 'python3') + const candidateBuildBinsDir = path.resolve('build', 'node_gyp_bins') + await fs.mkdir(candidateBuildBinsDir, { recursive: true }) + const symlinkDestination = path.join(candidateBuildBinsDir, 'python3') try { await fs.unlink(symlinkDestination) } catch (err) { if (err.code !== 'ENOENT') throw err } - await fs.symlink(python, symlinkDestination) - log.verbose('bin symlinks', `created symlink to "${python}" in "${buildBinsDir}" and added to PATH`) + try { + await fs.symlink(python, symlinkDestination) + process.env.PATH = `${candidateBuildBinsDir}:${process.env.PATH}` + buildBinsDir = candidateBuildBinsDir + log.verbose('bin symlinks', `created symlink to "${python}" in "${candidateBuildBinsDir}" and added to PATH`) + } catch (err) { + if (!['EPERM', 'EACCES', 'ENOSYS', 'ENOTSUP'].includes(err.code)) { + throw err + } + log.warn('bin symlinks', + `failed to create symlink to "${python}" in "${candidateBuildBinsDir}" ` + + `(${err.message}); continuing without it. ` + + 'The build may fail if it relies on the "python3" command being on PATH.') + await fs.rm(candidateBuildBinsDir, { recursive: true, force: true, maxRetries: 3 }) + } } const proc = gyp.spawn(command, argv) diff --git a/test/test-build-symlink.js b/test/test-build-symlink.js new file mode 100644 index 0000000000..aa840cf5a9 --- /dev/null +++ b/test/test-build-symlink.js @@ -0,0 +1,97 @@ +'use strict' + +const { describe, it, beforeEach, afterEach } = require('mocha') +const assert = require('assert') +const os = require('os') +const path = require('path') +const { EventEmitter } = require('events') +const gracefulFs = require('graceful-fs') +const fs = gracefulFs.promises +const build = require('../lib/build') + +const fakeGyp = { + opts: { make: process.execPath }, + spawn () { + const proc = new EventEmitter() + setImmediate(() => proc.emit('exit', 0, null)) + return proc + } +} + +function stubSymlink (code) { + gracefulFs.promises.symlink = async () => { + const err = new Error(`${code}: simulated symlink failure`) + err.code = code + throw err + } +} + +describe('build symlink fallback', function () { + let projectDir, buildDir + const orig = {} + beforeEach(async function () { + if (process.platform === 'win32') { + return this.skip('symlink logic only runs on non-Windows') + } + projectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'node-gyp-build-')) + buildDir = path.join(projectDir, 'build') + await fs.mkdir(buildDir, { recursive: true }) + await fs.writeFile(path.join(buildDir, 'Makefile'), '') + const config = { + target_defaults: { default_configuration: 'Release' }, + variables: { target_arch: 'x64', nodedir: projectDir, python: 'python3' } + } + await fs.writeFile(path.join(buildDir, 'config.gypi'), JSON.stringify(config)) + + orig.symlink = gracefulFs.promises.symlink + orig.cwd = process.cwd() + orig.path = process.env.PATH + + process.chdir(projectDir) + }) + afterEach(async function () { + if (process.platform === 'win32') { + return + } + gracefulFs.promises.symlink = orig.symlink + process.chdir(orig.cwd) + process.env.PATH = orig.path + await fs.rm(projectDir, { recursive: true, force: true }) + }) + it('continues and warns when symlink creation fails', async function () { + stubSymlink('EPERM') + const warnings = [] + const onLog = (level, prefix) => { + if (level === 'warn') { + warnings.push(prefix) + } + } + process.on('log', onLog) + try { + await build(fakeGyp, []) + assert.ok( + !gracefulFs.existsSync(path.join(buildDir, 'node_gyp_bins')), + 'node_gyp_bins should be removed after symlink failure' + ) + assert.ok( + !process.env.PATH.includes('node_gyp_bins'), + 'PATH should not contain node_gyp_bins when symlink fails' + ) + assert.ok(warnings.includes('bin symlinks'), 'should warn about the failed symlink') + } finally { + process.removeListener('log', onLog) + } + }) + it('fails if symlink creation failed for unknown reason', async function () { + stubSymlink('UNKNOWN') + await assert.rejects( + build(fakeGyp, []), + /UNKNOWN/, + 'build should rethrow errors that are not symlink-support failures' + ) + assert.ok( + !process.env.PATH.includes('node_gyp_bins'), + 'PATH should not contain node_gyp_bins when symlink fails' + ) + }) +})