Skip to content

Commit 607bac5

Browse files
committed
feat: install is completely promisified
Due to a posibility that callbacks are called twice on error, the install function is now internally completely promisified.
1 parent bda6424 commit 607bac5

File tree

3 files changed

+161
-162
lines changed

3 files changed

+161
-162
lines changed

lib/install.js

Lines changed: 83 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -4,60 +4,42 @@ const fs = require('graceful-fs')
44
const os = require('os')
55
const tar = require('tar')
66
const path = require('path')
7+
const util = require('util')
78
const stream = require('stream')
89
const crypto = require('crypto')
910
const log = require('npmlog')
1011
const semver = require('semver')
1112
const fetch = require('make-fetch-happen')
1213
const processRelease = require('./process-release')
1314
const win = process.platform === 'win32'
14-
const streamPipeline = require('util').promisify(stream.pipeline)
15+
const streamPipeline = util.promisify(stream.pipeline)
1516

1617
/**
1718
* @param {typeof import('graceful-fs')} fs
1819
*/
1920

20-
function install (fs, gyp, argv, callback) {
21+
async function install (fs, gyp, argv) {
2122
const release = processRelease(argv, gyp, process.version, process.release)
2223

23-
// ensure no double-callbacks happen
24-
function cb (err) {
25-
if (cb.done) {
26-
return
27-
}
28-
cb.done = true
29-
if (err) {
30-
log.warn('install', 'got an error, rolling back install')
31-
// roll-back the install if anything went wrong
32-
gyp.commands.remove([release.versionDir], function () {
33-
callback(err)
34-
})
35-
} else {
36-
callback(null, release.version)
37-
}
38-
}
39-
4024
// Determine which node dev files version we are installing
4125
log.verbose('install', 'input version string %j', release.version)
4226

4327
if (!release.semver) {
4428
// could not parse the version string with semver
45-
return callback(new Error('Invalid version number: ' + release.version))
29+
throw new Error('Invalid version number: ' + release.version)
4630
}
4731

4832
if (semver.lt(release.version, '0.8.0')) {
49-
return callback(new Error('Minimum target version is `0.8.0` or greater. Got: ' + release.version))
33+
throw new Error('Minimum target version is `0.8.0` or greater. Got: ' + release.version)
5034
}
5135

5236
// 0.x.y-pre versions are not published yet and cannot be installed. Bail.
5337
if (release.semver.prerelease[0] === 'pre') {
5438
log.verbose('detected "pre" node version', release.version)
55-
if (gyp.opts.nodedir) {
56-
log.verbose('--nodedir flag was passed; skipping install', gyp.opts.nodedir)
57-
callback()
58-
} else {
59-
callback(new Error('"pre" versions of node cannot be installed, use the --nodedir flag instead'))
39+
if (!gyp.opts.nodedir) {
40+
throw new Error('"pre" versions of node cannot be installed, use the --nodedir flag instead')
6041
}
42+
log.verbose('--nodedir flag was passed; skipping install', gyp.opts.nodedir)
6143
return
6244
}
6345

@@ -71,55 +53,48 @@ function install (fs, gyp, argv, callback) {
7153
// check if it is already installed, and only install when needed
7254
if (gyp.opts.ensure) {
7355
log.verbose('install', '--ensure was passed, so won\'t reinstall if already installed')
74-
fs.stat(devDir, function (err) {
75-
if (err) {
76-
if (err.code === 'ENOENT') {
77-
log.verbose('install', 'version not already installed, continuing with install', release.version)
78-
go().then(cb, cb)
79-
} else if (err.code === 'EACCES') {
80-
eaccesFallback(err)
81-
} else {
82-
cb(err)
56+
try {
57+
await fs.promises.stat(devDir)
58+
} catch (err) {
59+
if (err.code === 'ENOENT') {
60+
log.verbose('install', 'version not already installed, continuing with install', release.version)
61+
try {
62+
return await go()
63+
} catch (err) {
64+
return rollback(err)
8365
}
84-
return
66+
} else if (err.code === 'EACCES') {
67+
return eaccesFallback(err)
8568
}
86-
log.verbose('install', 'version is already installed, need to check "installVersion"')
87-
const installVersionFile = path.resolve(devDir, 'installVersion')
88-
fs.readFile(installVersionFile, 'ascii', (err, ver) => {
89-
if (err && err.code !== 'ENOENT') {
90-
return cb(err)
91-
}
92-
const installVersion = parseInt(ver, 10) || 0
93-
log.verbose('got "installVersion"', installVersion)
94-
log.verbose('needs "installVersion"', gyp.package.installVersion)
95-
if (installVersion < gyp.package.installVersion) {
96-
log.verbose('install', 'version is no good; reinstalling')
97-
go().then(cb, cb)
98-
} else {
99-
log.verbose('install', 'version is good')
100-
cb()
101-
}
102-
})
103-
})
104-
} else {
105-
go().then(cb, cb)
106-
}
107-
108-
class ShaSum extends stream.Transform {
109-
constructor (callback) {
110-
super()
111-
this._callback = callback
112-
this._digester = crypto.createHash('sha256')
69+
throw err
11370
}
114-
115-
_transform (chunk, _, callback) {
116-
this._digester.update(chunk)
117-
callback(null, chunk)
71+
log.verbose('install', 'version is already installed, need to check "installVersion"')
72+
const installVersionFile = path.resolve(devDir, 'installVersion')
73+
let installVersion = 0
74+
try {
75+
const ver = await fs.promises.readFile(installVersionFile, 'ascii')
76+
installVersion = parseInt(ver, 10) || 0
77+
} catch (err) {
78+
if (err.code !== 'ENOENT') {
79+
throw err
80+
}
11881
}
119-
120-
_flush (callback) {
121-
this._callback(null, this._digester.digest('hex'))
122-
callback()
82+
log.verbose('got "installVersion"', installVersion)
83+
log.verbose('needs "installVersion"', gyp.package.installVersion)
84+
if (installVersion < gyp.package.installVersion) {
85+
log.verbose('install', 'version is no good; reinstalling')
86+
try {
87+
return await go()
88+
} catch (err) {
89+
return rollback(err)
90+
}
91+
}
92+
log.verbose('install', 'version is good')
93+
} else {
94+
try {
95+
return await go()
96+
} catch (err) {
97+
return rollback(err)
12398
}
12499
}
125100

@@ -135,8 +110,7 @@ function install (fs, gyp, argv, callback) {
135110
}
136111
} catch (err) {
137112
if (err.code === 'EACCES') {
138-
eaccesFallback(err)
139-
return
113+
return eaccesFallback(err)
140114
}
141115

142116
throw err
@@ -173,7 +147,7 @@ function install (fs, gyp, argv, callback) {
173147
})
174148
} else {
175149
try {
176-
const res = await download(gyp, process.env, release.tarballUrl)
150+
const res = await download(gyp, release.tarballUrl)
177151

178152
if (res.status !== 200) {
179153
throw new Error(`${res.status} response downloading ${release.tarballUrl}`)
@@ -234,7 +208,7 @@ function install (fs, gyp, argv, callback) {
234208
log.verbose('check download content checksum, need to download `SHASUMS256.txt`...')
235209
log.verbose('checksum url', release.shasumsUrl)
236210

237-
const res = await download(gyp, process.env, release.shasumsUrl)
211+
const res = await download(gyp, release.shasumsUrl)
238212

239213
if (res.status !== 200) {
240214
throw new Error(`${res.status} status code downloading checksum`)
@@ -268,7 +242,7 @@ function install (fs, gyp, argv, callback) {
268242
await fs.promises.mkdir(dir, { recursive: true })
269243
log.verbose('streaming', name, 'to:', targetLibPath)
270244

271-
const res = await download(gyp, process.env, libUrl)
245+
const res = await download(gyp, libUrl)
272246

273247
if (res.status === 403 || res.status === 404) {
274248
if (arch === 'arm64') {
@@ -304,6 +278,13 @@ function install (fs, gyp, argv, callback) {
304278
return extname === '.h' || extname === '.gypi'
305279
}
306280

281+
async function rollback (err) {
282+
log.warn('install', 'got an error, rolling back install')
283+
// roll-back the install if anything went wrong
284+
await util.promisify(gyp.commands.remove)([release.versionDir])
285+
throw err
286+
}
287+
307288
/**
308289
* The EACCES fallback is a workaround for npm's `sudo` behavior, where
309290
* it drops the permissions before invoking any child processes (like
@@ -313,10 +294,10 @@ function install (fs, gyp, argv, callback) {
313294
* the compilation will succeed...
314295
*/
315296

316-
function eaccesFallback (err) {
297+
async function eaccesFallback (err) {
317298
const noretry = '--node_gyp_internal_noretry'
318299
if (argv.indexOf(noretry) !== -1) {
319-
return cb(err)
300+
throw err
320301
}
321302
const tmpdir = os.tmpdir()
322303
gyp.devDir = path.resolve(tmpdir, '.node-gyp')
@@ -331,11 +312,29 @@ function install (fs, gyp, argv, callback) {
331312
log.verbose('tmpdir == cwd', 'automatically will remove dev files after to save disk space')
332313
gyp.todo.push({ name: 'remove', args: argv })
333314
}
334-
gyp.commands.install([noretry].concat(argv), cb)
315+
return util.promisify(gyp.commands.install)([noretry].concat(argv))
316+
}
317+
}
318+
319+
class ShaSum extends stream.Transform {
320+
constructor (callback) {
321+
super()
322+
this._callback = callback
323+
this._digester = crypto.createHash('sha256')
324+
}
325+
326+
_transform (chunk, _, callback) {
327+
this._digester.update(chunk)
328+
callback(null, chunk)
329+
}
330+
331+
_flush (callback) {
332+
this._callback(null, this._digester.digest('hex'))
333+
callback()
335334
}
336335
}
337336

338-
async function download (gyp, env, url) {
337+
async function download (gyp, url) {
339338
log.http('GET', url)
340339

341340
const requestOpts = {
@@ -367,11 +366,11 @@ async function readCAFile (filename) {
367366
}
368367

369368
module.exports = function (gyp, argv, callback) {
370-
return install(fs, gyp, argv, callback)
369+
install(fs, gyp, argv).then(callback, callback)
371370
}
372371
module.exports.test = {
373-
download: download,
374-
install: install,
375-
readCAFile: readCAFile
372+
download,
373+
install,
374+
readCAFile
376375
}
377376
module.exports.usage = 'Install node development files for the specified node version.'

0 commit comments

Comments
 (0)