install.js 14 KB

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