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