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