Skip to content

Commit eae161f

Browse files
authored
fix: fall back gracefully when build symlink creation fails (#3339)
Fixes: #3317 Signed-off-by: Kirill Saied <sayed.kirill@gmail.com>
1 parent 42367da commit eae161f

2 files changed

Lines changed: 115 additions & 6 deletions

File tree

‎lib/build.js‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,17 +195,29 @@ async function build (gyp, argv) {
195195

196196
if (!win) {
197197
// Add build-time dependency symlinks (such as Python) to PATH
198-
buildBinsDir = path.resolve('build', 'node_gyp_bins')
199-
process.env.PATH = `${buildBinsDir}:${process.env.PATH}`
200-
await fs.mkdir(buildBinsDir, { recursive: true })
201-
const symlinkDestination = path.join(buildBinsDir, 'python3')
198+
const candidateBuildBinsDir = path.resolve('build', 'node_gyp_bins')
199+
await fs.mkdir(candidateBuildBinsDir, { recursive: true })
200+
const symlinkDestination = path.join(candidateBuildBinsDir, 'python3')
202201
try {
203202
await fs.unlink(symlinkDestination)
204203
} catch (err) {
205204
if (err.code !== 'ENOENT') throw err
206205
}
207-
await fs.symlink(python, symlinkDestination)
208-
log.verbose('bin symlinks', `created symlink to "${python}" in "${buildBinsDir}" and added to PATH`)
206+
try {
207+
await fs.symlink(python, symlinkDestination)
208+
process.env.PATH = `${candidateBuildBinsDir}:${process.env.PATH}`
209+
buildBinsDir = candidateBuildBinsDir
210+
log.verbose('bin symlinks', `created symlink to "${python}" in "${candidateBuildBinsDir}" and added to PATH`)
211+
} catch (err) {
212+
if (!['EPERM', 'EACCES', 'ENOSYS', 'ENOTSUP'].includes(err.code)) {
213+
throw err
214+
}
215+
log.warn('bin symlinks',
216+
`failed to create symlink to "${python}" in "${candidateBuildBinsDir}" ` +
217+
`(${err.message}); continuing without it. ` +
218+
'The build may fail if it relies on the "python3" command being on PATH.')
219+
await fs.rm(candidateBuildBinsDir, { recursive: true, force: true, maxRetries: 3 })
220+
}
209221
}
210222

211223
const proc = gyp.spawn(command, argv)

‎test/test-build-symlink.js‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
'use strict'
2+
3+
const { describe, it, beforeEach, afterEach } = require('mocha')
4+
const assert = require('assert')
5+
const os = require('os')
6+
const path = require('path')
7+
const { EventEmitter } = require('events')
8+
const gracefulFs = require('graceful-fs')
9+
const fs = gracefulFs.promises
10+
const build = require('../lib/build')
11+
12+
const fakeGyp = {
13+
opts: { make: process.execPath },
14+
spawn () {
15+
const proc = new EventEmitter()
16+
setImmediate(() => proc.emit('exit', 0, null))
17+
return proc
18+
}
19+
}
20+
21+
function stubSymlink (code) {
22+
gracefulFs.promises.symlink = async () => {
23+
const err = new Error(`${code}: simulated symlink failure`)
24+
err.code = code
25+
throw err
26+
}
27+
}
28+
29+
describe('build symlink fallback', function () {
30+
let projectDir, buildDir
31+
const orig = {}
32+
beforeEach(async function () {
33+
if (process.platform === 'win32') {
34+
return this.skip('symlink logic only runs on non-Windows')
35+
}
36+
projectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'node-gyp-build-'))
37+
buildDir = path.join(projectDir, 'build')
38+
await fs.mkdir(buildDir, { recursive: true })
39+
await fs.writeFile(path.join(buildDir, 'Makefile'), '')
40+
const config = {
41+
target_defaults: { default_configuration: 'Release' },
42+
variables: { target_arch: 'x64', nodedir: projectDir, python: 'python3' }
43+
}
44+
await fs.writeFile(path.join(buildDir, 'config.gypi'), JSON.stringify(config))
45+
46+
orig.symlink = gracefulFs.promises.symlink
47+
orig.cwd = process.cwd()
48+
orig.path = process.env.PATH
49+
50+
process.chdir(projectDir)
51+
})
52+
afterEach(async function () {
53+
if (process.platform === 'win32') {
54+
return
55+
}
56+
gracefulFs.promises.symlink = orig.symlink
57+
process.chdir(orig.cwd)
58+
process.env.PATH = orig.path
59+
await fs.rm(projectDir, { recursive: true, force: true })
60+
})
61+
it('continues and warns when symlink creation fails', async function () {
62+
stubSymlink('EPERM')
63+
const warnings = []
64+
const onLog = (level, prefix) => {
65+
if (level === 'warn') {
66+
warnings.push(prefix)
67+
}
68+
}
69+
process.on('log', onLog)
70+
try {
71+
await build(fakeGyp, [])
72+
assert.ok(
73+
!gracefulFs.existsSync(path.join(buildDir, 'node_gyp_bins')),
74+
'node_gyp_bins should be removed after symlink failure'
75+
)
76+
assert.ok(
77+
!process.env.PATH.includes('node_gyp_bins'),
78+
'PATH should not contain node_gyp_bins when symlink fails'
79+
)
80+
assert.ok(warnings.includes('bin symlinks'), 'should warn about the failed symlink')
81+
} finally {
82+
process.removeListener('log', onLog)
83+
}
84+
})
85+
it('fails if symlink creation failed for unknown reason', async function () {
86+
stubSymlink('UNKNOWN')
87+
await assert.rejects(
88+
build(fakeGyp, []),
89+
/UNKNOWN/,
90+
'build should rethrow errors that are not symlink-support failures'
91+
)
92+
assert.ok(
93+
!process.env.PATH.includes('node_gyp_bins'),
94+
'PATH should not contain node_gyp_bins when symlink fails'
95+
)
96+
})
97+
})

0 commit comments

Comments
 (0)