index.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. 'use strict'
  2. exports = module.exports = lifecycle
  3. exports.makeEnv = makeEnv
  4. exports._incorrectWorkingDirectory = _incorrectWorkingDirectory
  5. // for testing
  6. const platform = process.env.__TESTING_FAKE_PLATFORM__ || process.platform
  7. const isWindows = platform === 'win32'
  8. const spawn = require('./lib/spawn')
  9. const path = require('path')
  10. const Stream = require('stream').Stream
  11. const fs = require('graceful-fs')
  12. const chain = require('slide').chain
  13. const uidNumber = require('uid-number')
  14. const umask = require('umask')
  15. const which = require('which')
  16. const byline = require('byline')
  17. const resolveFrom = require('resolve-from')
  18. const DEFAULT_NODE_GYP_PATH = resolveFrom(__dirname, 'node-gyp/bin/node-gyp')
  19. const hookStatCache = new Map()
  20. let PATH = isWindows ? 'Path' : 'PATH'
  21. exports._pathEnvName = PATH
  22. const delimiter = path.delimiter
  23. // windows calls its path 'Path' usually, but this is not guaranteed.
  24. // merge them all together in the order they appear in the object.
  25. const mergePath = env =>
  26. Object.keys(env).filter(p => /^path$/i.test(p) && env[p])
  27. .map(p => env[p].split(delimiter))
  28. .reduce((set, p) => set.concat(p.filter(p => !set.includes(p))), [])
  29. .join(delimiter)
  30. exports._mergePath = mergePath
  31. const setPathEnv = (env, path) => {
  32. // first ensure that the canonical value is set.
  33. env[PATH] = path
  34. // also set any other case values, because windows.
  35. Object.keys(env)
  36. .filter(p => p !== PATH && /^path$/i.test(p))
  37. .forEach(p => { env[p] = path })
  38. }
  39. exports._setPathEnv = setPathEnv
  40. function logid (pkg, stage) {
  41. return pkg._id + '~' + stage + ':'
  42. }
  43. function hookStat (dir, stage, cb) {
  44. const hook = path.join(dir, '.hooks', stage)
  45. const cachedStatError = hookStatCache.get(hook)
  46. if (cachedStatError === undefined) {
  47. return fs.stat(hook, function (statError) {
  48. hookStatCache.set(hook, statError)
  49. cb(statError)
  50. })
  51. }
  52. return setImmediate(() => cb(cachedStatError))
  53. }
  54. function lifecycle (pkg, stage, wd, opts) {
  55. return new Promise((resolve, reject) => {
  56. while (pkg && pkg._data) pkg = pkg._data
  57. if (!pkg) return reject(new Error('Invalid package data'))
  58. opts.log.info('lifecycle', logid(pkg, stage), pkg._id)
  59. if (!pkg.scripts) pkg.scripts = {}
  60. if (stage === 'prepublish' && opts.ignorePrepublish) {
  61. opts.log.info('lifecycle', logid(pkg, stage), 'ignored because ignore-prepublish is set to true', pkg._id)
  62. delete pkg.scripts.prepublish
  63. }
  64. hookStat(opts.dir, stage, function (statError) {
  65. // makeEnv is a slow operation. This guard clause prevents makeEnv being called
  66. // and avoids a ton of unnecessary work, and results in a major perf boost.
  67. if (!pkg.scripts[stage] && statError) return resolve()
  68. validWd(wd || path.resolve(opts.dir, pkg.name), function (er, wd) {
  69. if (er) return reject(er)
  70. if ((wd.indexOf(opts.dir) !== 0 || _incorrectWorkingDirectory(wd, pkg)) &&
  71. !opts.unsafePerm && pkg.scripts[stage]) {
  72. opts.log.warn('lifecycle', logid(pkg, stage), 'cannot run in wd', pkg._id, pkg.scripts[stage], `(wd=${wd})`)
  73. return resolve()
  74. }
  75. // set the env variables, then run scripts as a child process.
  76. var env = makeEnv(pkg, opts)
  77. env.npm_lifecycle_event = stage
  78. env.npm_node_execpath = env.NODE = env.NODE || process.execPath
  79. env.npm_execpath = require.main.filename
  80. env.INIT_CWD = process.cwd()
  81. env.npm_config_node_gyp = env.npm_config_node_gyp || DEFAULT_NODE_GYP_PATH
  82. // 'nobody' typically doesn't have permission to write to /tmp
  83. // even if it's never used, sh freaks out.
  84. if (!opts.unsafePerm) env.TMPDIR = wd
  85. lifecycle_(pkg, stage, wd, opts, env, (er) => {
  86. if (er) return reject(er)
  87. return resolve()
  88. })
  89. })
  90. })
  91. })
  92. }
  93. function _incorrectWorkingDirectory (wd, pkg) {
  94. return wd.lastIndexOf(pkg.name) !== wd.length - pkg.name.length
  95. }
  96. function lifecycle_ (pkg, stage, wd, opts, env, cb) {
  97. var pathArr = []
  98. var p = wd.split(/[\\/]node_modules[\\/]/)
  99. var acc = path.resolve(p.shift())
  100. p.forEach(function (pp) {
  101. pathArr.unshift(path.join(acc, 'node_modules', '.bin'))
  102. acc = path.join(acc, 'node_modules', pp)
  103. })
  104. pathArr.unshift(path.join(acc, 'node_modules', '.bin'))
  105. // we also unshift the bundled node-gyp-bin folder so that
  106. // the bundled one will be used for installing things.
  107. pathArr.unshift(path.join(__dirname, 'node-gyp-bin'))
  108. if (shouldPrependCurrentNodeDirToPATH(opts)) {
  109. // prefer current node interpreter in child scripts
  110. pathArr.push(path.dirname(process.execPath))
  111. }
  112. const existingPath = mergePath(env)
  113. if (existingPath) pathArr.push(existingPath)
  114. const envPath = pathArr.join(isWindows ? ';' : ':')
  115. setPathEnv(env, envPath)
  116. var packageLifecycle = pkg.scripts && pkg.scripts.hasOwnProperty(stage)
  117. if (opts.ignoreScripts) {
  118. opts.log.info('lifecycle', logid(pkg, stage), 'ignored because ignore-scripts is set to true', pkg._id)
  119. packageLifecycle = false
  120. } else if (packageLifecycle) {
  121. // define this here so it's available to all scripts.
  122. env.npm_lifecycle_script = pkg.scripts[stage]
  123. } else {
  124. opts.log.silly('lifecycle', logid(pkg, stage), 'no script for ' + stage + ', continuing')
  125. }
  126. function done (er) {
  127. if (er) {
  128. if (opts.force) {
  129. opts.log.info('lifecycle', logid(pkg, stage), 'forced, continuing', er)
  130. er = null
  131. } else if (opts.failOk) {
  132. opts.log.warn('lifecycle', logid(pkg, stage), 'continuing anyway', er.message)
  133. er = null
  134. }
  135. }
  136. cb(er)
  137. }
  138. chain(
  139. [
  140. packageLifecycle && [runPackageLifecycle, pkg, stage, env, wd, opts],
  141. [runHookLifecycle, pkg, stage, env, wd, opts]
  142. ],
  143. done
  144. )
  145. }
  146. function shouldPrependCurrentNodeDirToPATH (opts) {
  147. const cfgsetting = opts.scriptsPrependNodePath
  148. if (cfgsetting === false) return false
  149. if (cfgsetting === true) return true
  150. var isDifferentNodeInPath
  151. var foundExecPath
  152. try {
  153. foundExecPath = which.sync(path.basename(process.execPath), { pathExt: isWindows ? ';' : ':' })
  154. // Apply `fs.realpath()` here to avoid false positives when `node` is a symlinked executable.
  155. isDifferentNodeInPath = fs.realpathSync(process.execPath).toUpperCase() !==
  156. fs.realpathSync(foundExecPath).toUpperCase()
  157. } catch (e) {
  158. isDifferentNodeInPath = true
  159. }
  160. if (cfgsetting === 'warn-only') {
  161. if (isDifferentNodeInPath && !shouldPrependCurrentNodeDirToPATH.hasWarned) {
  162. if (foundExecPath) {
  163. opts.log.warn('lifecycle', 'The node binary used for scripts is', foundExecPath, 'but npm is using', process.execPath, 'itself. Use the `--scripts-prepend-node-path` option to include the path for the node binary npm was executed with.')
  164. } else {
  165. opts.log.warn('lifecycle', 'npm is using', process.execPath, 'but there is no node binary in the current PATH. Use the `--scripts-prepend-node-path` option to include the path for the node binary npm was executed with.')
  166. }
  167. shouldPrependCurrentNodeDirToPATH.hasWarned = true
  168. }
  169. return false
  170. }
  171. return isDifferentNodeInPath
  172. }
  173. function validWd (d, cb) {
  174. fs.stat(d, function (er, st) {
  175. if (er || !st.isDirectory()) {
  176. var p = path.dirname(d)
  177. if (p === d) {
  178. return cb(new Error('Could not find suitable wd'))
  179. }
  180. return validWd(p, cb)
  181. }
  182. return cb(null, d)
  183. })
  184. }
  185. function runPackageLifecycle (pkg, stage, env, wd, opts, cb) {
  186. // run package lifecycle scripts in the package root, or the nearest parent.
  187. var cmd = env.npm_lifecycle_script
  188. var note = '\n> ' + pkg._id + ' ' + stage + ' ' + wd +
  189. '\n> ' + cmd + '\n'
  190. runCmd(note, cmd, pkg, env, stage, wd, opts, cb)
  191. }
  192. var running = false
  193. var queue = []
  194. function dequeue () {
  195. running = false
  196. if (queue.length) {
  197. var r = queue.shift()
  198. runCmd.apply(null, r)
  199. }
  200. }
  201. function runCmd (note, cmd, pkg, env, stage, wd, opts, cb) {
  202. if (running) {
  203. queue.push([note, cmd, pkg, env, stage, wd, opts, cb])
  204. return
  205. }
  206. running = true
  207. opts.log.pause()
  208. var unsafe = opts.unsafePerm
  209. var user = unsafe ? null : opts.user
  210. var group = unsafe ? null : opts.group
  211. if (opts.log.level !== 'silent') {
  212. opts.log.clearProgress()
  213. console.log(note)
  214. opts.log.showProgress()
  215. }
  216. opts.log.verbose('lifecycle', logid(pkg, stage), 'unsafe-perm in lifecycle', unsafe)
  217. if (isWindows) {
  218. unsafe = true
  219. }
  220. if (unsafe) {
  221. runCmd_(cmd, pkg, env, wd, opts, stage, unsafe, 0, 0, cb)
  222. } else {
  223. uidNumber(user, group, function (er, uid, gid) {
  224. if (er) {
  225. er.code = 'EUIDLOOKUP'
  226. opts.log.resume()
  227. process.nextTick(dequeue)
  228. return cb(er)
  229. }
  230. runCmd_(cmd, pkg, env, wd, opts, stage, unsafe, uid, gid, cb)
  231. })
  232. }
  233. }
  234. const getSpawnArgs = ({ cmd, wd, opts, uid, gid, unsafe, env }) => {
  235. const conf = {
  236. cwd: wd,
  237. env: env,
  238. stdio: opts.stdio || [ 0, 1, 2 ]
  239. }
  240. if (!unsafe) {
  241. conf.uid = uid ^ 0
  242. conf.gid = gid ^ 0
  243. }
  244. const customShell = opts.scriptShell
  245. let sh = 'sh'
  246. let shFlag = '-c'
  247. if (customShell) {
  248. sh = customShell
  249. } else if (isWindows || opts._TESTING_FAKE_WINDOWS_) {
  250. sh = process.env.comspec || 'cmd'
  251. // '/d /s /c' is used only for cmd.exe.
  252. if (/^(?:.*\\)?cmd(?:\.exe)?$/i.test(sh)) {
  253. shFlag = '/d /s /c'
  254. conf.windowsVerbatimArguments = true
  255. }
  256. }
  257. return [sh, [shFlag, cmd], conf]
  258. }
  259. exports._getSpawnArgs = getSpawnArgs
  260. function runCmd_ (cmd, pkg, env, wd, opts, stage, unsafe, uid, gid, cb_) {
  261. function cb (er) {
  262. cb_.apply(null, arguments)
  263. opts.log.resume()
  264. process.nextTick(dequeue)
  265. }
  266. const [sh, args, conf] = getSpawnArgs({ cmd, wd, opts, uid, gid, unsafe, env })
  267. opts.log.verbose('lifecycle', logid(pkg, stage), 'PATH:', env[PATH])
  268. opts.log.verbose('lifecycle', logid(pkg, stage), 'CWD:', wd)
  269. opts.log.silly('lifecycle', logid(pkg, stage), 'Args:', args)
  270. var proc = spawn(sh, args, conf, opts.log)
  271. proc.on('error', procError)
  272. proc.on('close', function (code, signal) {
  273. opts.log.silly('lifecycle', logid(pkg, stage), 'Returned: code:', code, ' signal:', signal)
  274. if (signal) {
  275. process.kill(process.pid, signal)
  276. } else if (code) {
  277. var er = new Error('Exit status ' + code)
  278. er.errno = code
  279. }
  280. procError(er)
  281. })
  282. byline(proc.stdout).on('data', function (data) {
  283. opts.log.verbose('lifecycle', logid(pkg, stage), 'stdout', data.toString())
  284. })
  285. byline(proc.stderr).on('data', function (data) {
  286. opts.log.verbose('lifecycle', logid(pkg, stage), 'stderr', data.toString())
  287. })
  288. process.once('SIGTERM', procKill)
  289. process.once('SIGINT', procInterupt)
  290. function procError (er) {
  291. if (er) {
  292. opts.log.info('lifecycle', logid(pkg, stage), 'Failed to exec ' + stage + ' script')
  293. er.message = pkg._id + ' ' + stage + ': `' + cmd + '`\n' +
  294. er.message
  295. if (er.code !== 'EPERM') {
  296. er.code = 'ELIFECYCLE'
  297. }
  298. fs.stat(opts.dir, function (statError, d) {
  299. if (statError && statError.code === 'ENOENT' && opts.dir.split(path.sep).slice(-1)[0] === 'node_modules') {
  300. opts.log.warn('', 'Local package.json exists, but node_modules missing, did you mean to install?')
  301. }
  302. })
  303. er.pkgid = pkg._id
  304. er.stage = stage
  305. er.script = cmd
  306. er.pkgname = pkg.name
  307. }
  308. process.removeListener('SIGTERM', procKill)
  309. process.removeListener('SIGTERM', procInterupt)
  310. process.removeListener('SIGINT', procKill)
  311. process.removeListener('SIGINT', procInterupt)
  312. return cb(er)
  313. }
  314. function procKill () {
  315. proc.kill()
  316. }
  317. function procInterupt () {
  318. proc.kill('SIGINT')
  319. proc.on('exit', function () {
  320. process.exit()
  321. })
  322. process.once('SIGINT', procKill)
  323. }
  324. }
  325. function runHookLifecycle (pkg, stage, env, wd, opts, cb) {
  326. hookStat(opts.dir, stage, function (er) {
  327. if (er) return cb()
  328. var cmd = path.join(opts.dir, '.hooks', stage)
  329. var note = '\n> ' + pkg._id + ' ' + stage + ' ' + wd +
  330. '\n> ' + cmd
  331. runCmd(note, cmd, pkg, env, stage, wd, opts, cb)
  332. })
  333. }
  334. function makeEnv (data, opts, prefix, env) {
  335. prefix = prefix || 'npm_package_'
  336. if (!env) {
  337. env = {}
  338. for (var i in process.env) {
  339. if (!i.match(/^npm_/)) {
  340. env[i] = process.env[i]
  341. }
  342. }
  343. // express and others respect the NODE_ENV value.
  344. if (opts.production) env.NODE_ENV = 'production'
  345. } else if (!data.hasOwnProperty('_lifecycleEnv')) {
  346. Object.defineProperty(data, '_lifecycleEnv',
  347. {
  348. value: env,
  349. enumerable: false
  350. }
  351. )
  352. }
  353. if (opts.nodeOptions) env.NODE_OPTIONS = opts.nodeOptions
  354. for (i in data) {
  355. if (i.charAt(0) !== '_') {
  356. var envKey = (prefix + i).replace(/[^a-zA-Z0-9_]/g, '_')
  357. if (i === 'readme') {
  358. continue
  359. }
  360. if (data[i] && typeof data[i] === 'object') {
  361. try {
  362. // quick and dirty detection for cyclical structures
  363. JSON.stringify(data[i])
  364. makeEnv(data[i], opts, envKey + '_', env)
  365. } catch (ex) {
  366. // usually these are package objects.
  367. // just get the path and basic details.
  368. var d = data[i]
  369. makeEnv(
  370. { name: d.name, version: d.version, path: d.path },
  371. opts,
  372. envKey + '_',
  373. env
  374. )
  375. }
  376. } else {
  377. env[envKey] = String(data[i])
  378. env[envKey] = env[envKey].indexOf('\n') !== -1
  379. ? JSON.stringify(env[envKey])
  380. : env[envKey]
  381. }
  382. }
  383. }
  384. if (prefix !== 'npm_package_') return env
  385. prefix = 'npm_config_'
  386. var pkgConfig = {}
  387. var pkgVerConfig = {}
  388. var namePref = data.name + ':'
  389. var verPref = data.name + '@' + data.version + ':'
  390. Object.keys(opts.config).forEach(function (i) {
  391. // in some rare cases (e.g. working with nerf darts), there are segmented
  392. // "private" (underscore-prefixed) config names -- don't export
  393. if ((i.charAt(0) === '_' && i.indexOf('_' + namePref) !== 0) || i.match(/:_/)) {
  394. return
  395. }
  396. var value = opts.config[i]
  397. if (value instanceof Stream || Array.isArray(value) || typeof value === 'function') return
  398. if (i.match(/umask/)) value = umask.toString(value)
  399. if (!value) value = ''
  400. else if (typeof value === 'number') value = '' + value
  401. else if (typeof value !== 'string') value = JSON.stringify(value)
  402. if (typeof value !== 'string') {
  403. return
  404. }
  405. value = value.indexOf('\n') !== -1
  406. ? JSON.stringify(value)
  407. : value
  408. i = i.replace(/^_+/, '')
  409. var k
  410. if (i.indexOf(namePref) === 0) {
  411. k = i.substr(namePref.length).replace(/[^a-zA-Z0-9_]/g, '_')
  412. pkgConfig[k] = value
  413. } else if (i.indexOf(verPref) === 0) {
  414. k = i.substr(verPref.length).replace(/[^a-zA-Z0-9_]/g, '_')
  415. pkgVerConfig[k] = value
  416. }
  417. var envKey = (prefix + i).replace(/[^a-zA-Z0-9_]/g, '_')
  418. env[envKey] = value
  419. })
  420. prefix = 'npm_package_config_'
  421. ;[pkgConfig, pkgVerConfig].forEach(function (conf) {
  422. for (var i in conf) {
  423. var envKey = (prefix + i)
  424. env[envKey] = conf[i]
  425. }
  426. })
  427. return env
  428. }