index.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. 'use strict'
  2. const path = require('path')
  3. const fs = require('graceful-fs')
  4. const BB = require('bluebird')
  5. const gentleFs = require('gentle-fs')
  6. const linkIfExists = BB.promisify(gentleFs.linkIfExists)
  7. const gentleFsBinLink = BB.promisify(gentleFs.binLink)
  8. const open = BB.promisify(fs.open)
  9. const close = BB.promisify(fs.close)
  10. const read = BB.promisify(fs.read, {multiArgs: true})
  11. const chmod = BB.promisify(fs.chmod)
  12. const readFile = BB.promisify(fs.readFile)
  13. const writeFileAtomic = BB.promisify(require('write-file-atomic'))
  14. const normalize = require('npm-normalize-package-bin')
  15. module.exports = BB.promisify(binLinks)
  16. function binLinks (pkg, folder, global, opts, cb) {
  17. pkg = normalize(pkg)
  18. folder = path.resolve(folder)
  19. // if it's global, and folder is in {prefix}/node_modules,
  20. // then bins are in {prefix}/bin
  21. // otherwise, then bins are in folder/../.bin
  22. var parent = pkg.name && pkg.name[0] === '@' ? path.dirname(path.dirname(folder)) : path.dirname(folder)
  23. var gnm = global && opts.globalDir
  24. var gtop = parent === gnm
  25. opts.log.info('linkStuff', opts.pkgId)
  26. opts.log.silly('linkStuff', opts.pkgId, 'has', parent, 'as its parent node_modules')
  27. if (global) opts.log.silly('linkStuff', opts.pkgId, 'is part of a global install')
  28. if (gnm) opts.log.silly('linkStuff', opts.pkgId, 'is installed into a global node_modules')
  29. if (gtop) opts.log.silly('linkStuff', opts.pkgId, 'is installed into the top-level global node_modules')
  30. return BB.join(
  31. linkBins(pkg, folder, parent, gtop, opts),
  32. linkMans(pkg, folder, parent, gtop, opts)
  33. ).asCallback(cb)
  34. }
  35. function isHashbangFile (file) {
  36. return open(file, 'r').then(fileHandle => {
  37. return read(fileHandle, Buffer.alloc(2), 0, 2, 0).spread((_, buf) => {
  38. if (!hasHashbang(buf)) return []
  39. return read(fileHandle, Buffer.alloc(2048), 0, 2048, 0)
  40. }).spread((_, buf) => buf && hasCR(buf), /* istanbul ignore next */ () => false)
  41. .finally(() => close(fileHandle))
  42. }).catch(/* istanbul ignore next */ () => false)
  43. }
  44. function hasHashbang (buf) {
  45. const str = buf.toString()
  46. return str.slice(0, 2) === '#!'
  47. }
  48. function hasCR (buf) {
  49. return /^#![^\n]+\r\n/.test(buf)
  50. }
  51. function dos2Unix (file) {
  52. return readFile(file, 'utf8').then(content => {
  53. return writeFileAtomic(file, content.replace(/^(#![^\n]+)\r\n/, '$1\n'))
  54. })
  55. }
  56. function getLinkOpts (opts, gently) {
  57. return Object.assign({}, opts, { gently: gently })
  58. }
  59. function linkBins (pkg, folder, parent, gtop, opts) {
  60. if (!pkg.bin || (!gtop && path.basename(parent) !== 'node_modules')) {
  61. return
  62. }
  63. var linkOpts = getLinkOpts(opts, gtop && folder)
  64. var execMode = parseInt('0777', 8) & (~opts.umask)
  65. var binRoot = gtop ? opts.globalBin
  66. : path.resolve(parent, '.bin')
  67. opts.log.verbose('linkBins', [pkg.bin, binRoot, gtop])
  68. return BB.map(Object.keys(pkg.bin), bin => {
  69. var dest = path.resolve(binRoot, bin)
  70. var src = path.resolve(folder, pkg.bin[bin])
  71. /* istanbul ignore if - that unpossible */
  72. if (src.indexOf(folder) !== 0) {
  73. throw new Error('invalid bin entry for package ' +
  74. pkg._id + '. key=' + bin + ', value=' + pkg.bin[bin])
  75. }
  76. return linkBin(src, dest, linkOpts).then(() => {
  77. // bins should always be executable.
  78. // XXX skip chmod on windows?
  79. return chmod(src, execMode)
  80. }).then(() => {
  81. return isHashbangFile(src)
  82. }).then(isHashbang => {
  83. if (!isHashbang) return
  84. opts.log.silly('linkBins', 'Converting line endings of hashbang file:', src)
  85. return dos2Unix(src)
  86. }).then(() => {
  87. if (!gtop) return
  88. var dest = path.resolve(binRoot, bin)
  89. var out = opts.parseable
  90. ? dest + '::' + src + ':BINFILE'
  91. : dest + ' -> ' + src
  92. if (!opts.json && !opts.parseable) {
  93. opts.log.clearProgress()
  94. console.log(out)
  95. opts.log.showProgress()
  96. }
  97. }).catch(err => {
  98. /* istanbul ignore next */
  99. if (err.code === 'ENOENT' && opts.ignoreScripts) return
  100. throw err
  101. })
  102. })
  103. }
  104. function linkBin (from, to, opts) {
  105. // do not clobber global bins
  106. if (opts.globalBin && to.indexOf(opts.globalBin) === 0) {
  107. opts.clobberLinkGently = true
  108. }
  109. return gentleFsBinLink(from, to, opts)
  110. }
  111. function linkMans (pkg, folder, parent, gtop, opts) {
  112. if (!pkg.man || !gtop || process.platform === 'win32') return
  113. var manRoot = path.resolve(opts.prefix, 'share', 'man')
  114. opts.log.verbose('linkMans', 'man files are', pkg.man, 'in', manRoot)
  115. // make sure that the mans are unique.
  116. // otherwise, if there are dupes, it'll fail with EEXIST
  117. var set = pkg.man.reduce(function (acc, man) {
  118. if (typeof man !== 'string') {
  119. return acc
  120. }
  121. const cleanMan = path.join('/', man).replace(/\\|:/g, '/').substr(1)
  122. acc[path.basename(man)] = cleanMan
  123. return acc
  124. }, {})
  125. var manpages = pkg.man.filter(function (man) {
  126. if (typeof man !== 'string') {
  127. return false
  128. }
  129. const cleanMan = path.join('/', man).replace(/\\|:/g, '/').substr(1)
  130. return set[path.basename(man)] === cleanMan
  131. })
  132. return BB.map(manpages, man => {
  133. opts.log.silly('linkMans', 'preparing to link', man)
  134. var parseMan = man.match(/(.*\.([0-9]+)(\.gz)?)$/)
  135. if (!parseMan) {
  136. throw new Error(
  137. man + ' is not a valid name for a man file. ' +
  138. 'Man files must end with a number, ' +
  139. 'and optionally a .gz suffix if they are compressed.'
  140. )
  141. }
  142. var stem = parseMan[1]
  143. var sxn = parseMan[2]
  144. var bn = path.basename(stem)
  145. var manSrc = path.resolve(folder, man)
  146. /* istanbul ignore if - that unpossible */
  147. if (manSrc.indexOf(folder) !== 0) {
  148. throw new Error('invalid man entry for package ' +
  149. pkg._id + '. man=' + manSrc)
  150. }
  151. var manDest = path.join(manRoot, 'man' + sxn, bn)
  152. // man pages should always be clobbering gently, because they are
  153. // only installed for top-level global packages, so never destroy
  154. // a link if it doesn't point into the folder we're linking
  155. opts.clobberLinkGently = true
  156. return linkIfExists(manSrc, manDest, getLinkOpts(opts, gtop && folder))
  157. })
  158. }