1 | 'use strict'
|
2 |
|
3 | const BB = require('bluebird')
|
4 |
|
5 | const binLink = require('bin-links')
|
6 | const buildLogicalTree = require('npm-logical-tree')
|
7 | const extract = require('./lib/extract.js')
|
8 | const figgyPudding = require('figgy-pudding')
|
9 | const fs = require('graceful-fs')
|
10 | const getPrefix = require('find-npm-prefix')
|
11 | const lifecycle = require('npm-lifecycle')
|
12 | const lockVerify = require('lock-verify')
|
13 | const mkdirp = BB.promisify(require('mkdirp'))
|
14 | const npa = require('npm-package-arg')
|
15 | const path = require('path')
|
16 | const readPkgJson = BB.promisify(require('read-package-json'))
|
17 | const rimraf = BB.promisify(require('rimraf'))
|
18 |
|
19 | const readFileAsync = BB.promisify(fs.readFile)
|
20 | const statAsync = BB.promisify(fs.stat)
|
21 | const symlinkAsync = BB.promisify(fs.symlink)
|
22 | const writeFileAsync = BB.promisify(fs.writeFile)
|
23 |
|
24 | const LifecycleOpts = figgyPudding({
|
25 | config: {},
|
26 | 'script-shell': {},
|
27 | scriptShell: 'script-shell',
|
28 | 'ignore-scripts': {},
|
29 | ignoreScripts: 'ignore-scripts',
|
30 | 'ignore-prepublish': {},
|
31 | ignorePrepublish: 'ignore-prepublish',
|
32 | 'scripts-prepend-node-path': {},
|
33 | scriptsPrependNodePath: 'scripts-prepend-node-path',
|
34 | 'unsafe-perm': {},
|
35 | unsafePerm: 'unsafe-perm',
|
36 | prefix: {},
|
37 | dir: 'prefix',
|
38 | failOk: { default: false }
|
39 | }, { other () { return true } })
|
40 |
|
41 | class Installer {
|
42 | constructor (opts) {
|
43 | this.opts = opts
|
44 |
|
45 |
|
46 | this.startTime = Date.now()
|
47 | this.runTime = 0
|
48 | this.timings = { scripts: 0 }
|
49 | this.pkgCount = 0
|
50 |
|
51 |
|
52 | this.log = this.opts.log || require('./lib/silentlog.js')
|
53 | this.pkg = null
|
54 | this.tree = null
|
55 | this.failedDeps = new Set()
|
56 | }
|
57 |
|
58 | timedStage (name) {
|
59 | const start = Date.now()
|
60 | return BB.resolve(this[name].apply(this, [].slice.call(arguments, 1)))
|
61 | .tap(() => {
|
62 | this.timings[name] = Date.now() - start
|
63 | this.log.info(name, `Done in ${this.timings[name] / 1000}s`)
|
64 | })
|
65 | }
|
66 |
|
67 | run () {
|
68 | return this.timedStage('prepare')
|
69 | .then(() => this.timedStage('extractTree', this.tree))
|
70 | .then(() => this.timedStage('updateJson', this.tree))
|
71 | .then(pkgJsons => this.timedStage('buildTree', this.tree, pkgJsons))
|
72 | .then(() => this.timedStage('garbageCollect', this.tree))
|
73 | .then(() => this.timedStage('runScript', 'prepublish', this.pkg, this.prefix))
|
74 | .then(() => this.timedStage('runScript', 'prepare', this.pkg, this.prefix))
|
75 | .then(() => this.timedStage('teardown'))
|
76 | .then(() => {
|
77 | this.runTime = Date.now() - this.startTime
|
78 | this.log.info(
|
79 | 'run-scripts',
|
80 | `total script time: ${this.timings.scripts / 1000}s`
|
81 | )
|
82 | this.log.info(
|
83 | 'run-time',
|
84 | `total run time: ${this.runTime / 1000}s`
|
85 | )
|
86 | })
|
87 | .catch(err => {
|
88 | this.timedStage('teardown')
|
89 | if (err.message.match(/aggregate error/)) {
|
90 | throw err[0]
|
91 | } else {
|
92 | throw err
|
93 | }
|
94 | })
|
95 | .then(() => this)
|
96 | }
|
97 |
|
98 | prepare () {
|
99 | this.log.info('prepare', 'initializing installer')
|
100 | this.log.level = this.opts.loglevel
|
101 | this.log.verbose('prepare', 'starting workers')
|
102 | extract.startWorkers()
|
103 |
|
104 | return (
|
105 | this.opts.prefix && this.opts.global
|
106 | ? BB.resolve(this.opts.prefix)
|
107 |
|
108 |
|
109 | : process.argv.some(arg => arg.match(/^\s*--prefix\s*/i))
|
110 | ? BB.resolve(this.opts.prefix)
|
111 | : getPrefix(process.cwd())
|
112 | )
|
113 | .then(prefix => {
|
114 | this.prefix = prefix
|
115 | this.log.verbose('prepare', 'installation prefix: ' + prefix)
|
116 | return BB.join(
|
117 | readJson(prefix, 'package.json'),
|
118 | readJson(prefix, 'package-lock.json', true),
|
119 | readJson(prefix, 'npm-shrinkwrap.json', true),
|
120 | (pkg, lock, shrink) => {
|
121 | if (shrink) {
|
122 | this.log.verbose('prepare', 'using npm-shrinkwrap.json')
|
123 | } else if (lock) {
|
124 | this.log.verbose('prepare', 'using package-lock.json')
|
125 | }
|
126 | pkg._shrinkwrap = shrink || lock
|
127 | this.pkg = pkg
|
128 | }
|
129 | )
|
130 | })
|
131 | .then(() => statAsync(
|
132 | path.join(this.prefix, 'node_modules')
|
133 | ).catch(err => { if (err.code !== 'ENOENT') { throw err } }))
|
134 | .then(stat => {
|
135 | stat && this.log.warn(
|
136 | 'prepare', 'removing existing node_modules/ before installation'
|
137 | )
|
138 | return BB.join(
|
139 | this.checkLock(),
|
140 | stat && rimraf(path.join(this.prefix, 'node_modules/*'))
|
141 | )
|
142 | }).then(() => {
|
143 |
|
144 | this.tree = buildLogicalTree(this.pkg, this.pkg._shrinkwrap)
|
145 | this.log.silly('tree', this.tree)
|
146 | this.expectedTotal = 0
|
147 | this.tree.forEach((dep, next) => {
|
148 | this.expectedTotal++
|
149 | next()
|
150 | })
|
151 | })
|
152 | }
|
153 |
|
154 | teardown () {
|
155 | this.log.verbose('teardown', 'shutting down workers.')
|
156 | return extract.stopWorkers()
|
157 | }
|
158 |
|
159 | checkLock () {
|
160 | this.log.verbose('checkLock', 'verifying package-lock data')
|
161 | const pkg = this.pkg
|
162 | const prefix = this.prefix
|
163 | if (!pkg._shrinkwrap || !pkg._shrinkwrap.lockfileVersion) {
|
164 | return BB.reject(
|
165 | new Error(`cipm can only install packages with an existing package-lock.json or npm-shrinkwrap.json with lockfileVersion >= 1. Run an install with npm@5 or later to generate it, then try again.`)
|
166 | )
|
167 | }
|
168 | return lockVerify(prefix).then(result => {
|
169 | if (result.status) {
|
170 | result.warnings.forEach(w => this.log.warn('lockfile', w))
|
171 | } else {
|
172 | throw new Error(
|
173 | 'cipm can only install packages when your package.json and package-lock.json or ' +
|
174 | 'npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` ' +
|
175 | 'before continuing.\n\n' +
|
176 | result.warnings.map(w => 'Warning: ' + w).join('\n') + '\n' +
|
177 | result.errors.join('\n') + '\n'
|
178 | )
|
179 | }
|
180 | }).catch(err => {
|
181 | throw err
|
182 | })
|
183 | }
|
184 |
|
185 | extractTree (tree) {
|
186 | this.log.verbose('extractTree', 'extracting dependencies to node_modules/')
|
187 | const cg = this.log.newItem('extractTree', this.expectedTotal)
|
188 | return tree.forEachAsync((dep, next) => {
|
189 | if (!this.checkDepEnv(dep)) { return }
|
190 | const depPath = dep.path(this.prefix)
|
191 | const spec = npa.resolve(dep.name, dep.version, this.prefix)
|
192 | if (dep.isRoot) {
|
193 | return next()
|
194 | } else if (spec.type === 'directory') {
|
195 | const relative = path.relative(path.dirname(depPath), spec.fetchSpec)
|
196 | this.log.silly('extractTree', `${dep.name}@${spec.fetchSpec} -> ${depPath} (symlink)`)
|
197 | return mkdirp(path.dirname(depPath))
|
198 | .then(() => symlinkAsync(relative, depPath, 'junction'))
|
199 | .catch(
|
200 | () => rimraf(depPath)
|
201 | .then(() => symlinkAsync(relative, depPath, 'junction'))
|
202 | ).then(() => next())
|
203 | .then(() => {
|
204 | this.pkgCount++
|
205 | cg.completeWork(1)
|
206 | })
|
207 | } else {
|
208 | this.log.silly('extractTree', `${dep.name}@${dep.version} -> ${depPath}`)
|
209 | return (
|
210 | dep.bundled
|
211 | ? statAsync(path.join(depPath, 'package.json')).catch(err => {
|
212 | if (err.code !== 'ENOENT') { throw err }
|
213 | })
|
214 | : BB.resolve(false)
|
215 | )
|
216 | .then(wasBundled => {
|
217 |
|
218 | if (wasBundled) {
|
219 | cg.completeWork(1)
|
220 | return next()
|
221 | } else {
|
222 | return BB.resolve(extract.child(
|
223 | dep.name, dep, depPath, this.opts
|
224 | ))
|
225 | .then(() => cg.completeWork(1))
|
226 | .then(() => { this.pkgCount++ })
|
227 | .then(next)
|
228 | }
|
229 | })
|
230 | }
|
231 | }, {concurrency: 50, Promise: BB})
|
232 | .then(() => cg.finish())
|
233 | }
|
234 |
|
235 | checkDepEnv (dep) {
|
236 | const includeDev = (
|
237 |
|
238 | this.opts.dev ||
|
239 | (
|
240 | !/^prod(uction)?$/.test(this.opts.only) &&
|
241 | !this.opts.production
|
242 | ) ||
|
243 | /^dev(elopment)?$/.test(this.opts.only) ||
|
244 | /^dev(elopment)?$/.test(this.opts.also)
|
245 | )
|
246 | const includeProd = !/^dev(elopment)?$/.test(this.opts.only)
|
247 | const includeOptional = includeProd && this.opts.optional
|
248 | return (dep.dev && includeDev) ||
|
249 | (dep.optional && includeOptional) ||
|
250 | (!dep.dev && !dep.optional && includeProd)
|
251 | }
|
252 |
|
253 | updateJson (tree) {
|
254 | this.log.verbose('updateJson', 'updating json deps to include _from')
|
255 | const pkgJsons = new Map()
|
256 | return tree.forEachAsync((dep, next) => {
|
257 | if (!this.checkDepEnv(dep)) { return }
|
258 | const spec = npa.resolve(dep.name, dep.version)
|
259 | const depPath = dep.path(this.prefix)
|
260 | return next()
|
261 | .then(() => readJson(depPath, 'package.json'))
|
262 | .then(pkg => (spec.registry || spec.type === 'directory')
|
263 | ? pkg
|
264 | : this.updateFromField(dep, pkg).then(() => pkg)
|
265 | )
|
266 | .then(pkg => (pkg.scripts && pkg.scripts.install)
|
267 | ? pkg
|
268 | : this.updateInstallScript(dep, pkg).then(() => pkg)
|
269 | )
|
270 | .tap(pkg => { pkgJsons.set(dep, pkg) })
|
271 | }, {concurrency: 100, Promise: BB})
|
272 | .then(() => pkgJsons)
|
273 | }
|
274 |
|
275 | buildTree (tree, pkgJsons) {
|
276 | this.log.verbose('buildTree', 'finalizing tree and running scripts')
|
277 | return tree.forEachAsync((dep, next) => {
|
278 | if (!this.checkDepEnv(dep)) { return }
|
279 | const spec = npa.resolve(dep.name, dep.version)
|
280 | const depPath = dep.path(this.prefix)
|
281 | const pkg = pkgJsons.get(dep)
|
282 | this.log.silly('buildTree', `linking ${spec}`)
|
283 | return this.runScript('preinstall', pkg, depPath)
|
284 | .then(next)
|
285 |
|
286 | .then(() => {
|
287 | if (
|
288 | dep.isRoot ||
|
289 | !(pkg.bin || pkg.man || (pkg.directories && pkg.directories.bin))
|
290 | ) {
|
291 |
|
292 |
|
293 | return
|
294 | }
|
295 | return readPkgJson(path.join(depPath, 'package.json'))
|
296 | .then(pkg => binLink(pkg, depPath, false, {
|
297 | force: this.opts.force,
|
298 | ignoreScripts: this.opts['ignore-scripts'],
|
299 | log: Object.assign({}, this.log, { info: () => {} }),
|
300 | name: pkg.name,
|
301 | pkgId: pkg.name + '@' + pkg.version,
|
302 | prefix: this.prefix,
|
303 | prefixes: [this.prefix],
|
304 | umask: this.opts.umask
|
305 | }), e => {
|
306 | this.log.verbose('buildTree', `error linking ${spec}: ${e.message} ${e.stack}`)
|
307 | })
|
308 | })
|
309 | .then(() => this.runScript('install', pkg, depPath))
|
310 | .then(() => this.runScript('postinstall', pkg, depPath))
|
311 | .then(() => this)
|
312 | .catch(e => {
|
313 | if (dep.optional) {
|
314 | this.failedDeps.add(dep)
|
315 | } else {
|
316 | throw e
|
317 | }
|
318 | })
|
319 | }, {concurrency: 1, Promise: BB})
|
320 | }
|
321 |
|
322 | updateFromField (dep, pkg) {
|
323 | const depPath = dep.path(this.prefix)
|
324 | const depPkgPath = path.join(depPath, 'package.json')
|
325 | const parent = dep.requiredBy.values().next().value
|
326 | return readJson(parent.path(this.prefix), 'package.json')
|
327 | .then(ppkg =>
|
328 | (ppkg.dependencies && ppkg.dependencies[dep.name]) ||
|
329 | (ppkg.devDependencies && ppkg.devDependencies[dep.name]) ||
|
330 | (ppkg.optionalDependencies && ppkg.optionalDependencies[dep.name])
|
331 | )
|
332 | .then(from => npa.resolve(dep.name, from))
|
333 | .then(from => { pkg._from = from.toString() })
|
334 | .then(() => writeFileAsync(depPkgPath, JSON.stringify(pkg, null, 2)))
|
335 | .then(() => pkg)
|
336 | }
|
337 |
|
338 | updateInstallScript (dep, pkg) {
|
339 | const depPath = dep.path(this.prefix)
|
340 | return statAsync(path.join(depPath, 'binding.gyp'))
|
341 | .catch(err => { if (err.code !== 'ENOENT') { throw err } })
|
342 | .then(stat => {
|
343 | if (stat) {
|
344 | if (!pkg.scripts) {
|
345 | pkg.scripts = {}
|
346 | }
|
347 | pkg.scripts.install = 'node-gyp rebuild'
|
348 | }
|
349 | })
|
350 | .then(() => pkg)
|
351 | }
|
352 |
|
353 |
|
354 | garbageCollect (tree) {
|
355 | if (!this.failedDeps.size) { return }
|
356 | return sweep(
|
357 | tree,
|
358 | this.prefix,
|
359 | mark(tree, this.failedDeps)
|
360 | )
|
361 | .then(purged => {
|
362 | this.purgedDeps = purged
|
363 | this.pkgCount -= purged.size
|
364 | })
|
365 | }
|
366 |
|
367 | runScript (stage, pkg, pkgPath) {
|
368 | const start = Date.now()
|
369 | if (!this.opts['ignore-scripts']) {
|
370 |
|
371 | pkg._id = pkg.name + '@' + pkg.version
|
372 | return BB.resolve(lifecycle(
|
373 | pkg, stage, pkgPath, LifecycleOpts(this.opts).concat({
|
374 |
|
375 |
|
376 | config: Object.assign({}, this.opts, {
|
377 | log: null,
|
378 | dirPacker: null
|
379 | }),
|
380 | dir: this.prefix
|
381 | }))
|
382 | ).tap(() => { this.timings.scripts += Date.now() - start })
|
383 | }
|
384 | return BB.resolve()
|
385 | }
|
386 | }
|
387 | module.exports = Installer
|
388 |
|
389 | function mark (tree, failed) {
|
390 | const liveDeps = new Set()
|
391 | tree.forEach((dep, next) => {
|
392 | if (!failed.has(dep)) {
|
393 | liveDeps.add(dep)
|
394 | next()
|
395 | }
|
396 | })
|
397 | return liveDeps
|
398 | }
|
399 |
|
400 | function sweep (tree, prefix, liveDeps) {
|
401 | const purged = new Set()
|
402 | return tree.forEachAsync((dep, next) => {
|
403 | return next().then(() => {
|
404 | if (
|
405 | !dep.isRoot &&
|
406 | !liveDeps.has(dep) &&
|
407 | !purged.has(dep)
|
408 | ) {
|
409 | purged.add(dep)
|
410 | return rimraf(dep.path(prefix))
|
411 | }
|
412 | })
|
413 | }, {concurrency: 50, Promise: BB}).then(() => purged)
|
414 | }
|
415 |
|
416 | function stripBOM (str) {
|
417 | return str.replace(/^\uFEFF/, '')
|
418 | }
|
419 |
|
420 | module.exports._readJson = readJson
|
421 | function readJson (jsonPath, name, ignoreMissing) {
|
422 | return readFileAsync(path.join(jsonPath, name), 'utf8')
|
423 | .then(str => JSON.parse(stripBOM(str)))
|
424 | .catch({code: 'ENOENT'}, err => {
|
425 | if (!ignoreMissing) {
|
426 | throw err
|
427 | }
|
428 | })
|
429 | }
|