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