Initial commit
[yaffs-website] / node_modules / node-gyp / lib / install.js
1
2 module.exports = exports = install
3
4 module.exports.test = { download: download, readCAFile: readCAFile }
5
6 exports.usage = 'Install node development files for the specified node version.'
7
8 /**
9  * Module dependencies.
10  */
11
12 var fs = require('graceful-fs')
13   , osenv = require('osenv')
14   , tar = require('tar')
15   , rm = require('rimraf')
16   , path = require('path')
17   , crypto = require('crypto')
18   , zlib = require('zlib')
19   , log = require('npmlog')
20   , semver = require('semver')
21   , fstream = require('fstream')
22   , request = require('request')
23   , minimatch = require('minimatch')
24   , mkdir = require('mkdirp')
25   , processRelease = require('./process-release')
26   , win = process.platform == 'win32'
27
28 function install (gyp, argv, callback) {
29
30   var release = processRelease(argv, gyp, process.version, process.release)
31
32   // ensure no double-callbacks happen
33   function cb (err) {
34     if (cb.done) return
35     cb.done = true
36     if (err) {
37       log.warn('install', 'got an error, rolling back install')
38       // roll-back the install if anything went wrong
39       gyp.commands.remove([ release.versionDir ], function (err2) {
40         callback(err)
41       })
42     } else {
43       callback(null, release.version)
44     }
45   }
46
47   // Determine which node dev files version we are installing
48   log.verbose('install', 'input version string %j', release.version)
49
50   if (!release.semver) {
51     // could not parse the version string with semver
52     return callback(new Error('Invalid version number: ' + release.version))
53   }
54
55   if (semver.lt(release.version, '0.8.0')) {
56     return callback(new Error('Minimum target version is `0.8.0` or greater. Got: ' + release.version))
57   }
58
59   // 0.x.y-pre versions are not published yet and cannot be installed. Bail.
60   if (release.semver.prerelease[0] === 'pre') {
61     log.verbose('detected "pre" node version', release.version)
62     if (gyp.opts.nodedir) {
63       log.verbose('--nodedir flag was passed; skipping install', gyp.opts.nodedir)
64       callback()
65     } else {
66       callback(new Error('"pre" versions of node cannot be installed, use the --nodedir flag instead'))
67     }
68     return
69   }
70
71   // flatten version into String
72   log.verbose('install', 'installing version: %s', release.versionDir)
73
74   // the directory where the dev files will be installed
75   var devDir = path.resolve(gyp.devDir, release.versionDir)
76
77   // If '--ensure' was passed, then don't *always* install the version;
78   // check if it is already installed, and only install when needed
79   if (gyp.opts.ensure) {
80     log.verbose('install', '--ensure was passed, so won\'t reinstall if already installed')
81     fs.stat(devDir, function (err, stat) {
82       if (err) {
83         if (err.code == 'ENOENT') {
84           log.verbose('install', 'version not already installed, continuing with install', release.version)
85           go()
86         } else if (err.code == 'EACCES') {
87           eaccesFallback()
88         } else {
89           cb(err)
90         }
91         return
92       }
93       log.verbose('install', 'version is already installed, need to check "installVersion"')
94       var installVersionFile = path.resolve(devDir, 'installVersion')
95       fs.readFile(installVersionFile, 'ascii', function (err, ver) {
96         if (err && err.code != 'ENOENT') {
97           return cb(err)
98         }
99         var installVersion = parseInt(ver, 10) || 0
100         log.verbose('got "installVersion"', installVersion)
101         log.verbose('needs "installVersion"', gyp.package.installVersion)
102         if (installVersion < gyp.package.installVersion) {
103           log.verbose('install', 'version is no good; reinstalling')
104           go()
105         } else {
106           log.verbose('install', 'version is good')
107           cb()
108         }
109       })
110     })
111   } else {
112     go()
113   }
114
115   function getContentSha(res, callback) {
116     var shasum = crypto.createHash('sha256')
117     res.on('data', function (chunk) {
118       shasum.update(chunk)
119     }).on('end', function () {
120       callback(null, shasum.digest('hex'))
121     })
122   }
123
124   function go () {
125
126     log.verbose('ensuring nodedir is created', devDir)
127
128     // first create the dir for the node dev files
129     mkdir(devDir, function (err, created) {
130       if (err) {
131         if (err.code == 'EACCES') {
132           eaccesFallback()
133         } else {
134           cb(err)
135         }
136         return
137       }
138
139       if (created) {
140         log.verbose('created nodedir', created)
141       }
142
143       // now download the node tarball
144       var tarPath = gyp.opts.tarball
145       var badDownload = false
146         , extractCount = 0
147         , gunzip = zlib.createGunzip()
148         , extracter = tar.Extract({ path: devDir, strip: 1, filter: isValid })
149
150       var contentShasums = {}
151       var expectShasums = {}
152
153       // checks if a file to be extracted from the tarball is valid.
154       // only .h header files and the gyp files get extracted
155       function isValid () {
156         var name = this.path.substring(devDir.length + 1)
157         var isValid = valid(name)
158         if (name === '' && this.type === 'Directory') {
159           // the first directory entry is ok
160           return true
161         }
162         if (isValid) {
163           log.verbose('extracted file from tarball', name)
164           extractCount++
165         } else {
166           // invalid
167           log.silly('ignoring from tarball', name)
168         }
169         return isValid
170       }
171
172       gunzip.on('error', cb)
173       extracter.on('error', cb)
174       extracter.on('end', afterTarball)
175
176       // download the tarball, gunzip and extract!
177
178       if (tarPath) {
179         var input = fs.createReadStream(tarPath)
180         input.pipe(gunzip).pipe(extracter)
181         return
182       }
183
184       try {
185         var req = download(gyp, process.env, release.tarballUrl)
186       } catch (e) {
187         return cb(e)
188       }
189
190       // something went wrong downloading the tarball?
191       req.on('error', function (err) {
192         if (err.code === 'ENOTFOUND') {
193           return cb(new Error('This is most likely not a problem with node-gyp or the package itself and\n' +
194             'is related to network connectivity. In most cases you are behind a proxy or have bad \n' +
195             'network settings.'))
196         }
197         badDownload = true
198         cb(err)
199       })
200
201       req.on('close', function () {
202         if (extractCount === 0) {
203           cb(new Error('Connection closed while downloading tarball file'))
204         }
205       })
206
207       req.on('response', function (res) {
208         if (res.statusCode !== 200) {
209           badDownload = true
210           cb(new Error(res.statusCode + ' response downloading ' + release.tarballUrl))
211           return
212         }
213         // content checksum
214         getContentSha(res, function (_, checksum) {
215           var filename = path.basename(release.tarballUrl).trim()
216           contentShasums[filename] = checksum
217           log.verbose('content checksum', filename, checksum)
218         })
219
220         // start unzipping and untaring
221         req.pipe(gunzip).pipe(extracter)
222       })
223
224       // invoked after the tarball has finished being extracted
225       function afterTarball () {
226         if (badDownload) return
227         if (extractCount === 0) {
228           return cb(new Error('There was a fatal problem while downloading/extracting the tarball'))
229         }
230         log.verbose('tarball', 'done parsing tarball')
231         var async = 0
232
233         if (win) {
234           // need to download node.lib
235           async++
236           downloadNodeLib(deref)
237         }
238
239         // write the "installVersion" file
240         async++
241         var installVersionPath = path.resolve(devDir, 'installVersion')
242         fs.writeFile(installVersionPath, gyp.package.installVersion + '\n', deref)
243
244         // Only download SHASUMS.txt if not using tarPath override
245         if (!tarPath) {
246           // download SHASUMS.txt
247           async++
248           downloadShasums(deref)
249         }
250
251         if (async === 0) {
252           // no async tasks required
253           cb()
254         }
255
256         function deref (err) {
257           if (err) return cb(err)
258
259           async--
260           if (!async) {
261             log.verbose('download contents checksum', JSON.stringify(contentShasums))
262             // check content shasums
263             for (var k in contentShasums) {
264               log.verbose('validating download checksum for ' + k, '(%s == %s)', contentShasums[k], expectShasums[k])
265               if (contentShasums[k] !== expectShasums[k]) {
266                 cb(new Error(k + ' local checksum ' + contentShasums[k] + ' not match remote ' + expectShasums[k]))
267                 return
268               }
269             }
270             cb()
271           }
272         }
273       }
274
275       function downloadShasums(done) {
276         log.verbose('check download content checksum, need to download `SHASUMS256.txt`...')
277         var shasumsPath = path.resolve(devDir, 'SHASUMS256.txt')
278
279         log.verbose('checksum url', release.shasumsUrl)
280         try {
281           var req = download(gyp, process.env, release.shasumsUrl)
282         } catch (e) {
283           return cb(e)
284         }
285
286         req.on('error', done)
287         req.on('response', function (res) {
288           if (res.statusCode !== 200) {
289             done(new Error(res.statusCode + ' status code downloading checksum'))
290             return
291           }
292
293           var chunks = []
294           res.on('data', function (chunk) {
295             chunks.push(chunk)
296           })
297           res.on('end', function () {
298             var lines = Buffer.concat(chunks).toString().trim().split('\n')
299             lines.forEach(function (line) {
300               var items = line.trim().split(/\s+/)
301               if (items.length !== 2) return
302
303               // 0035d18e2dcf9aad669b1c7c07319e17abfe3762  ./node-v0.11.4.tar.gz
304               var name = items[1].replace(/^\.\//, '')
305               expectShasums[name] = items[0]
306             })
307
308             log.verbose('checksum data', JSON.stringify(expectShasums))
309             done()
310           })
311         })
312       }
313
314       function downloadNodeLib (done) {
315         log.verbose('on Windows; need to download `' + release.name + '.lib`...')
316         var dir32 = path.resolve(devDir, 'ia32')
317           , dir64 = path.resolve(devDir, 'x64')
318           , libPath32 = path.resolve(dir32, release.name + '.lib')
319           , libPath64 = path.resolve(dir64, release.name + '.lib')
320
321         log.verbose('32-bit ' + release.name + '.lib dir', dir32)
322         log.verbose('64-bit ' + release.name + '.lib dir', dir64)
323         log.verbose('`' + release.name + '.lib` 32-bit url', release.libUrl32)
324         log.verbose('`' + release.name + '.lib` 64-bit url', release.libUrl64)
325
326         var async = 2
327         mkdir(dir32, function (err) {
328           if (err) return done(err)
329           log.verbose('streaming 32-bit ' + release.name + '.lib to:', libPath32)
330
331           try {
332             var req = download(gyp, process.env, release.libUrl32, cb)
333           } catch (e) {
334             return cb(e)
335           }
336
337           req.on('error', done)
338           req.on('response', function (res) {
339             if (res.statusCode !== 200) {
340               done(new Error(res.statusCode + ' status code downloading 32-bit ' + release.name + '.lib'))
341               return
342             }
343
344             getContentSha(res, function (_, checksum) {
345               contentShasums[release.libPath32] = checksum
346               log.verbose('content checksum', release.libPath32, checksum)
347             })
348
349             var ws = fs.createWriteStream(libPath32)
350             ws.on('error', cb)
351             req.pipe(ws)
352           })
353           req.on('end', function () {
354             --async || done()
355           })
356         })
357         mkdir(dir64, function (err) {
358           if (err) return done(err)
359           log.verbose('streaming 64-bit ' + release.name + '.lib to:', libPath64)
360
361           try {
362             var req = download(gyp, process.env, release.libUrl64, cb)
363           } catch (e) {
364             return cb(e)
365           }
366
367           req.on('error', done)
368           req.on('response', function (res) {
369             if (res.statusCode !== 200) {
370               done(new Error(res.statusCode + ' status code downloading 64-bit ' + release.name + '.lib'))
371               return
372             }
373
374             getContentSha(res, function (_, checksum) {
375               contentShasums[release.libPath64] = checksum
376               log.verbose('content checksum', release.libPath64, checksum)
377             })
378
379             var ws = fs.createWriteStream(libPath64)
380             ws.on('error', cb)
381             req.pipe(ws)
382           })
383           req.on('end', function () {
384             --async || done()
385           })
386         })
387       } // downloadNodeLib()
388
389     }) // mkdir()
390
391   } // go()
392
393   /**
394    * Checks if a given filename is "valid" for this installation.
395    */
396
397   function valid (file) {
398     // header files
399     return minimatch(file, '*.h', { matchBase: true }) ||
400            minimatch(file, '*.gypi', { matchBase: true })
401   }
402
403   /**
404    * The EACCES fallback is a workaround for npm's `sudo` behavior, where
405    * it drops the permissions before invoking any child processes (like
406    * node-gyp). So what happens is the "nobody" user doesn't have
407    * permission to create the dev dir. As a fallback, make the tmpdir() be
408    * the dev dir for this installation. This is not ideal, but at least
409    * the compilation will succeed...
410    */
411
412   function eaccesFallback () {
413     var tmpdir = osenv.tmpdir()
414     gyp.devDir = path.resolve(tmpdir, '.node-gyp')
415     log.warn('EACCES', 'user "%s" does not have permission to access the dev dir "%s"', osenv.user(), devDir)
416     log.warn('EACCES', 'attempting to reinstall using temporary dev dir "%s"', gyp.devDir)
417     if (process.cwd() == tmpdir) {
418       log.verbose('tmpdir == cwd', 'automatically will remove dev files after to save disk space')
419       gyp.todo.push({ name: 'remove', args: argv })
420     }
421     gyp.commands.install(argv, cb)
422   }
423
424 }
425
426 function download (gyp, env, url) {
427   log.http('GET', url)
428
429   var requestOpts = {
430       uri: url
431     , headers: {
432         'User-Agent': 'node-gyp v' + gyp.version + ' (node ' + process.version + ')'
433       }
434   }
435
436   var cafile = gyp.opts.cafile
437   if (cafile) {
438     requestOpts.ca = readCAFile(cafile)
439   }
440
441   // basic support for a proxy server
442   var proxyUrl = gyp.opts.proxy
443               || env.http_proxy
444               || env.HTTP_PROXY
445               || env.npm_config_proxy
446   if (proxyUrl) {
447     if (/^https?:\/\//i.test(proxyUrl)) {
448       log.verbose('download', 'using proxy url: "%s"', proxyUrl)
449       requestOpts.proxy = proxyUrl
450     } else {
451       log.warn('download', 'ignoring invalid "proxy" config setting: "%s"', proxyUrl)
452     }
453   }
454
455   var req = request(requestOpts)
456   req.on('response', function (res) {
457     log.http(res.statusCode, url)
458   })
459
460   return req
461 }
462
463 function readCAFile (filename) {
464   // The CA file can contain multiple certificates so split on certificate
465   // boundaries.  [\S\s]*? is used to match everything including newlines.
466   var ca = fs.readFileSync(filename, 'utf8')
467   var re = /(-----BEGIN CERTIFICATE-----[\S\s]*?-----END CERTIFICATE-----)/g
468   return ca.match(re)
469 }