1'use strict' 2 3const { createWriteStream, promises: fs } = require('graceful-fs') 4const os = require('os') 5const { backOff } = require('exponential-backoff') 6const tar = require('tar') 7const path = require('path') 8const { Transform, promises: { pipeline } } = require('stream') 9const crypto = require('crypto') 10const log = require('./log') 11const semver = require('semver') 12const { download } = require('./download') 13const processRelease = require('./process-release') 14 15const win = process.platform === 'win32' 16 17async function install (gyp, argv) { 18 log.stdout() 19 const release = processRelease(argv, gyp, process.version, process.release) 20 // Detecting target_arch based on logic from create-cnfig-gyp.js. Used on Windows only. 21 const arch = win ? (gyp.opts.target_arch || gyp.opts.arch || process.arch || 'ia32') : '' 22 // Used to prevent downloading tarball if only new node.lib is required on Windows. 23 let shouldDownloadTarball = true 24 25 // Determine which node dev files version we are installing 26 log.verbose('install', 'input version string %j', release.version) 27 28 if (!release.semver) { 29 // could not parse the version string with semver 30 throw new Error('Invalid version number: ' + release.version) 31 } 32 33 if (semver.lt(release.version, '0.8.0')) { 34 throw new Error('Minimum target version is `0.8.0` or greater. Got: ' + release.version) 35 } 36 37 // 0.x.y-pre versions are not published yet and cannot be installed. Bail. 38 if (release.semver.prerelease[0] === 'pre') { 39 log.verbose('detected "pre" node version', release.version) 40 if (!gyp.opts.nodedir) { 41 throw new Error('"pre" versions of node cannot be installed, use the --nodedir flag instead') 42 } 43 log.verbose('--nodedir flag was passed; skipping install', gyp.opts.nodedir) 44 return 45 } 46 47 // flatten version into String 48 log.verbose('install', 'installing version: %s', release.versionDir) 49 50 // the directory where the dev files will be installed 51 const devDir = path.resolve(gyp.devDir, release.versionDir) 52 53 // If '--ensure' was passed, then don't *always* install the version; 54 // check if it is already installed, and only install when needed 55 if (gyp.opts.ensure) { 56 log.verbose('install', '--ensure was passed, so won\'t reinstall if already installed') 57 try { 58 await fs.stat(devDir) 59 } catch (err) { 60 if (err.code === 'ENOENT') { 61 log.verbose('install', 'version not already installed, continuing with install', release.version) 62 try { 63 return await go() 64 } catch (err) { 65 return rollback(err) 66 } 67 } else if (err.code === 'EACCES') { 68 return eaccesFallback(err) 69 } 70 throw err 71 } 72 log.verbose('install', 'version is already installed, need to check "installVersion"') 73 const installVersionFile = path.resolve(devDir, 'installVersion') 74 let installVersion = 0 75 try { 76 const ver = await fs.readFile(installVersionFile, 'ascii') 77 installVersion = parseInt(ver, 10) || 0 78 } catch (err) { 79 if (err.code !== 'ENOENT') { 80 throw err 81 } 82 } 83 log.verbose('got "installVersion"', installVersion) 84 log.verbose('needs "installVersion"', gyp.package.installVersion) 85 if (installVersion < gyp.package.installVersion) { 86 log.verbose('install', 'version is no good; reinstalling') 87 try { 88 return await go() 89 } catch (err) { 90 return rollback(err) 91 } 92 } 93 log.verbose('install', 'version is good') 94 if (win) { 95 log.verbose('on Windows; need to check node.lib') 96 const nodeLibPath = path.resolve(devDir, arch, 'node.lib') 97 try { 98 await fs.stat(nodeLibPath) 99 } catch (err) { 100 if (err.code === 'ENOENT') { 101 log.verbose('install', `version not already installed for ${arch}, continuing with install`, release.version) 102 try { 103 shouldDownloadTarball = false 104 return await go() 105 } catch (err) { 106 return rollback(err) 107 } 108 } else if (err.code === 'EACCES') { 109 return eaccesFallback(err) 110 } 111 throw err 112 } 113 } 114 } else { 115 try { 116 return await go() 117 } catch (err) { 118 return rollback(err) 119 } 120 } 121 122 async function copyDirectory (src, dest) { 123 try { 124 await fs.stat(src) 125 } catch { 126 throw new Error(`Missing source directory for copy: ${src}`) 127 } 128 await fs.mkdir(dest, { recursive: true }) 129 const entries = await fs.readdir(src, { withFileTypes: true }) 130 for (const entry of entries) { 131 if (entry.isDirectory()) { 132 await copyDirectory(path.join(src, entry.name), path.join(dest, entry.name)) 133 } else if (entry.isFile()) { 134 // with parallel installs, copying files may cause file errors on 135 // Windows so use an exponential backoff to resolve collisions 136 await backOff(async () => { 137 try { 138 await fs.copyFile(path.join(src, entry.name), path.join(dest, entry.name)) 139 } catch (err) { 140 // if ensure, check if file already exists and that's good enough 141 if (gyp.opts.ensure && err.code === 'EBUSY') { 142 try { 143 await fs.stat(path.join(dest, entry.name)) 144 return 145 } catch {} 146 } 147 throw err 148 } 149 }) 150 } else { 151 throw new Error('Unexpected file directory entry type') 152 } 153 } 154 } 155 156 async function go () { 157 log.verbose('ensuring devDir is created', devDir) 158 159 // first create the dir for the node dev files 160 try { 161 const created = await fs.mkdir(devDir, { recursive: true }) 162 163 if (created) { 164 log.verbose('created devDir', created) 165 } 166 } catch (err) { 167 if (err.code === 'EACCES') { 168 return eaccesFallback(err) 169 } 170 171 throw err 172 } 173 174 // now download the node tarball 175 const tarPath = gyp.opts.tarball 176 let extractErrors = false 177 let extractCount = 0 178 const contentShasums = {} 179 const expectShasums = {} 180 181 // checks if a file to be extracted from the tarball is valid. 182 // only .h header files and the gyp files get extracted 183 function isValid (path) { 184 const isValid = valid(path) 185 if (isValid) { 186 log.verbose('extracted file from tarball', path) 187 extractCount++ 188 } else { 189 // invalid 190 log.silly('ignoring from tarball', path) 191 } 192 return isValid 193 } 194 195 function onwarn (code, message) { 196 extractErrors = true 197 log.error('error while extracting tarball', code, message) 198 } 199 200 // download the tarball and extract! 201 // Ommited on Windows if only new node.lib is required 202 203 // on Windows there can be file errors from tar if parallel installs 204 // are happening (not uncommon with multiple native modules) so 205 // extract the tarball to a temp directory first and then copy over 206 const tarExtractDir = win ? await fs.mkdtemp(path.join(os.tmpdir(), 'node-gyp-tmp-')) : devDir 207 208 try { 209 if (shouldDownloadTarball) { 210 if (tarPath) { 211 await tar.extract({ 212 file: tarPath, 213 strip: 1, 214 filter: isValid, 215 onwarn, 216 cwd: tarExtractDir 217 }) 218 } else { 219 try { 220 const res = await download(gyp, release.tarballUrl) 221 222 if (res.status !== 200) { 223 throw new Error(`${res.status} response downloading ${release.tarballUrl}`) 224 } 225 226 await pipeline( 227 res.body, 228 // content checksum 229 new ShaSum((_, checksum) => { 230 const filename = path.basename(release.tarballUrl).trim() 231 contentShasums[filename] = checksum 232 log.verbose('content checksum', filename, checksum) 233 }), 234 tar.extract({ 235 strip: 1, 236 cwd: tarExtractDir, 237 filter: isValid, 238 onwarn 239 }) 240 ) 241 } catch (err) { 242 // something went wrong downloading the tarball? 243 if (err.code === 'ENOTFOUND') { 244 throw new Error('This is most likely not a problem with node-gyp or the package itself and\n' + 245 'is related to network connectivity. In most cases you are behind a proxy or have bad \n' + 246 'network settings.') 247 } 248 throw err 249 } 250 } 251 252 // invoked after the tarball has finished being extracted 253 if (extractErrors || extractCount === 0) { 254 throw new Error('There was a fatal problem while downloading/extracting the tarball') 255 } 256 257 log.verbose('tarball', 'done parsing tarball') 258 } 259 260 const installVersionPath = path.resolve(tarExtractDir, 'installVersion') 261 await Promise.all([ 262 // need to download node.lib 263 ...(win ? [downloadNodeLib()] : []), 264 // write the "installVersion" file 265 fs.writeFile(installVersionPath, gyp.package.installVersion + '\n'), 266 // Only download SHASUMS.txt if we downloaded something in need of SHA verification 267 ...(!tarPath || win ? [downloadShasums()] : []) 268 ]) 269 270 log.verbose('download contents checksum', JSON.stringify(contentShasums)) 271 // check content shasums 272 for (const k in contentShasums) { 273 log.verbose('validating download checksum for ' + k, '(%s == %s)', contentShasums[k], expectShasums[k]) 274 if (contentShasums[k] !== expectShasums[k]) { 275 throw new Error(k + ' local checksum ' + contentShasums[k] + ' not match remote ' + expectShasums[k]) 276 } 277 } 278 279 // copy over the files from the temp tarball extract directory to devDir 280 if (tarExtractDir !== devDir) { 281 await copyDirectory(tarExtractDir, devDir) 282 } 283 } finally { 284 if (tarExtractDir !== devDir) { 285 try { 286 // try to cleanup temp dir 287 await fs.rm(tarExtractDir, { recursive: true }) 288 } catch { 289 log.warn('failed to clean up temp tarball extract directory') 290 } 291 } 292 } 293 294 async function downloadShasums () { 295 log.verbose('check download content checksum, need to download `SHASUMS256.txt`...') 296 log.verbose('checksum url', release.shasumsUrl) 297 298 const res = await download(gyp, release.shasumsUrl) 299 300 if (res.status !== 200) { 301 throw new Error(`${res.status} status code downloading checksum`) 302 } 303 304 for (const line of (await res.text()).trim().split('\n')) { 305 const items = line.trim().split(/\s+/) 306 if (items.length !== 2) { 307 return 308 } 309 310 // 0035d18e2dcf9aad669b1c7c07319e17abfe3762 ./node-v0.11.4.tar.gz 311 const name = items[1].replace(/^\.\//, '') 312 expectShasums[name] = items[0] 313 } 314 315 log.verbose('checksum data', JSON.stringify(expectShasums)) 316 } 317 318 async function downloadNodeLib () { 319 log.verbose('on Windows; need to download `' + release.name + '.lib`...') 320 const dir = path.resolve(tarExtractDir, arch) 321 const targetLibPath = path.resolve(dir, release.name + '.lib') 322 const { libUrl, libPath } = release[arch] 323 const name = `${arch} ${release.name}.lib` 324 log.verbose(name, 'dir', dir) 325 log.verbose(name, 'url', libUrl) 326 327 await fs.mkdir(dir, { recursive: true }) 328 log.verbose('streaming', name, 'to:', targetLibPath) 329 330 const res = await download(gyp, libUrl) 331 332 // Since only required node.lib is downloaded throw error if it is not fetched 333 if (res.status !== 200) { 334 throw new Error(`${res.status} status code downloading ${name}`) 335 } 336 337 return pipeline( 338 res.body, 339 new ShaSum((_, checksum) => { 340 contentShasums[libPath] = checksum 341 log.verbose('content checksum', libPath, checksum) 342 }), 343 createWriteStream(targetLibPath) 344 ) 345 } // downloadNodeLib() 346 } // go() 347 348 /** 349 * Checks if a given filename is "valid" for this installation. 350 */ 351 352 function valid (file) { 353 // header files 354 const extname = path.extname(file) 355 return extname === '.h' || extname === '.gypi' 356 } 357 358 async function rollback (err) { 359 log.warn('install', 'got an error, rolling back install') 360 // roll-back the install if anything went wrong 361 await gyp.commands.remove([release.versionDir]) 362 throw err 363 } 364 365 /** 366 * The EACCES fallback is a workaround for npm's `sudo` behavior, where 367 * it drops the permissions before invoking any child processes (like 368 * node-gyp). So what happens is the "nobody" user doesn't have 369 * permission to create the dev dir. As a fallback, make the tmpdir() be 370 * the dev dir for this installation. This is not ideal, but at least 371 * the compilation will succeed... 372 */ 373 374 async function eaccesFallback (err) { 375 const noretry = '--node_gyp_internal_noretry' 376 if (argv.indexOf(noretry) !== -1) { 377 throw err 378 } 379 const tmpdir = os.tmpdir() 380 gyp.devDir = path.resolve(tmpdir, '.node-gyp') 381 let userString = '' 382 try { 383 // os.userInfo can fail on some systems, it's not critical here 384 userString = ` ("${os.userInfo().username}")` 385 } catch (e) {} 386 log.warn('EACCES', 'current user%s does not have permission to access the dev dir "%s"', userString, devDir) 387 log.warn('EACCES', 'attempting to reinstall using temporary dev dir "%s"', gyp.devDir) 388 if (process.cwd() === tmpdir) { 389 log.verbose('tmpdir == cwd', 'automatically will remove dev files after to save disk space') 390 gyp.todo.push({ name: 'remove', args: argv }) 391 } 392 return gyp.commands.install([noretry].concat(argv)) 393 } 394} 395 396class ShaSum extends Transform { 397 constructor (callback) { 398 super() 399 this._callback = callback 400 this._digester = crypto.createHash('sha256') 401 } 402 403 _transform (chunk, _, callback) { 404 this._digester.update(chunk) 405 callback(null, chunk) 406 } 407 408 _flush (callback) { 409 this._callback(null, this._digester.digest('hex')) 410 callback() 411 } 412} 413 414module.exports = install 415module.exports.usage = 'Install node development files for the specified node version.' 416