deps: make node-gyp work with io.js
[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         if (err.code === 'ENOTFOUND') {
234           return cb(new Error('This is most likely not a problem with node-gyp or the package itself and\n' +
235             'is related to network connectivity. In most cases you are behind a proxy or have bad \n' +
236             'network settings.'))
237         }
238         badDownload = true
239         cb(err)
240       })
241
242       req.on('close', function () {
243         if (extractCount === 0) {
244           cb(new Error('Connection closed while downloading tarball file'))
245         }
246       })
247
248       req.on('response', function (res) {
249         if (res.statusCode !== 200) {
250           badDownload = true
251           cb(new Error(res.statusCode + ' response dowloading ' + tarballUrl))
252           return
253         }
254         // content checksum
255         getContentSha(res, function (_, checksum) {
256           var filename = path.basename(tarballUrl).trim()
257           contentShasums[filename] = checksum
258           log.verbose('content checksum', filename, checksum)
259         })
260
261         // start unzipping and untaring
262         req.pipe(gunzip).pipe(extracter)
263       })
264
265       // invoked after the tarball has finished being extracted
266       function afterTarball () {
267         if (badDownload) return
268         if (extractCount === 0) {
269           return cb(new Error('There was a fatal problem while downloading/extracting the tarball'))
270         }
271         log.verbose('tarball', 'done parsing tarball')
272         var async = 0
273
274         if (win) {
275           // need to download iojs.lib
276           async++
277           downloadNodeLib(deref)
278         }
279
280         // write the "installVersion" file
281         async++
282         var installVersionPath = path.resolve(devDir, 'installVersion')
283         fs.writeFile(installVersionPath, gyp.package.installVersion + '\n', deref)
284
285         // Only download SHASUMS.txt if not using tarPath override
286         if (!tarPath) {
287           // download SHASUMS.txt
288           async++
289           downloadShasums(deref)
290         }
291
292         if (async === 0) {
293           // no async tasks required
294           cb()
295         }
296
297         function deref (err) {
298           if (err) return cb(err)
299
300           async--
301           if (!async) {
302             log.verbose('download contents checksum', JSON.stringify(contentShasums))
303             // check content shasums
304             for (var k in contentShasums) {
305               log.verbose('validating download checksum for ' + k, '(%s == %s)', contentShasums[k], expectShasums[k])
306               if (contentShasums[k] !== expectShasums[k]) {
307                 cb(new Error(k + ' local checksum ' + contentShasums[k] + ' not match remote ' + expectShasums[k]))
308                 return
309               }
310             }
311             cb()
312           }
313         }
314       }
315
316       function downloadShasums(done) {
317         var shasumsFile = (checksumAlgo === 'sha256') ? 'SHASUMS256.txt' : 'SHASUMS.txt'
318         log.verbose('check download content checksum, need to download `' + shasumsFile + '`...')
319         var shasumsPath = path.resolve(devDir, shasumsFile)
320           , shasumsUrl = distUrl + '/v' + version + '/' + shasumsFile
321
322         log.verbose('checksum url', shasumsUrl)
323         var req = download(shasumsUrl)
324         if (!req) return
325         req.on('error', done)
326         req.on('response', function (res) {
327           if (res.statusCode !== 200) {
328             done(new Error(res.statusCode + ' status code downloading checksum'))
329             return
330           }
331
332           var chunks = []
333           res.on('data', function (chunk) {
334             chunks.push(chunk)
335           })
336           res.on('end', function () {
337             var lines = Buffer.concat(chunks).toString().trim().split('\n')
338             lines.forEach(function (line) {
339               var items = line.trim().split(/\s+/)
340               if (items.length !== 2) return
341
342               // 0035d18e2dcf9aad669b1c7c07319e17abfe3762  ./node-v0.11.4.tar.gz
343               var name = items[1].replace(/^\.\//, '')
344               expectShasums[name] = items[0]
345             })
346
347             log.verbose('checksum data', JSON.stringify(expectShasums))
348             done()
349           })
350         })
351       }
352
353       function downloadNodeLib (done) {
354         log.verbose('on Windows; need to download `iojs.lib`...')
355         var dir32 = path.resolve(devDir, 'ia32')
356           , dir64 = path.resolve(devDir, 'x64')
357           , nodeLibPath32 = path.resolve(dir32, 'iojs.lib')
358           , nodeLibPath64 = path.resolve(dir64, 'iojs.lib')
359           , nodeLibUrl32 = distUrl + '/v' + version + '/win-x86/iojs.lib'
360           , nodeLibUrl64 = distUrl + '/v' + version + '/win-x64/iojs.lib'
361
362         log.verbose('32-bit iojs.lib dir', dir32)
363         log.verbose('64-bit iojs.lib dir', dir64)
364         log.verbose('`iojs.lib` 32-bit url', nodeLibUrl32)
365         log.verbose('`iojs.lib` 64-bit url', nodeLibUrl64)
366
367         var async = 2
368         mkdir(dir32, function (err) {
369           if (err) return done(err)
370           log.verbose('streaming 32-bit iojs.lib to:', nodeLibPath32)
371
372           var req = download(nodeLibUrl32)
373           if (!req) return
374           req.on('error', done)
375           req.on('response', function (res) {
376             if (res.statusCode !== 200) {
377               done(new Error(res.statusCode + ' status code downloading 32-bit iojs.lib'))
378               return
379             }
380
381             getContentSha(res, function (_, checksum) {
382               contentShasums['win-x86/iojs.lib'] = checksum
383               log.verbose('content checksum', 'win-x86/iojs.lib', checksum)
384             })
385
386             var ws = fs.createWriteStream(nodeLibPath32)
387             ws.on('error', cb)
388             req.pipe(ws)
389           })
390           req.on('end', function () {
391             --async || done()
392           })
393         })
394         mkdir(dir64, function (err) {
395           if (err) return done(err)
396           log.verbose('streaming 64-bit iojs.lib to:', nodeLibPath64)
397
398           var req = download(nodeLibUrl64)
399           if (!req) return
400           req.on('error', done)
401           req.on('response', function (res) {
402             if (res.statusCode !== 200) {
403               done(new Error(res.statusCode + ' status code downloading 64-bit iojs.lib'))
404               return
405             }
406
407             getContentSha(res, function (_, checksum) {
408               contentShasums['win-x64/iojs.lib'] = checksum
409               log.verbose('content checksum', 'win-x64/iojs.lib', checksum)
410             })
411
412             var ws = fs.createWriteStream(nodeLibPath64)
413             ws.on('error', cb)
414             req.pipe(ws)
415           })
416           req.on('end', function () {
417             --async || done()
418           })
419         })
420       } // downloadNodeLib()
421
422     }) // mkdir()
423
424   } // go()
425
426   /**
427    * Checks if a given filename is "valid" for this installation.
428    */
429
430   function valid (file) {
431     // header files
432     return minimatch(file, '*.h', { matchBase: true }) ||
433            minimatch(file, '*.gypi', { matchBase: true })
434   }
435
436   /**
437    * The EACCES fallback is a workaround for npm's `sudo` behavior, where
438    * it drops the permissions before invoking any child processes (like
439    * node-gyp). So what happens is the "nobody" user doesn't have
440    * permission to create the dev dir. As a fallback, make the tmpdir() be
441    * the dev dir for this installation. This is not ideal, but at least
442    * the compilation will succeed...
443    */
444
445   function eaccesFallback () {
446     var tmpdir = osenv.tmpdir()
447     gyp.devDir = path.resolve(tmpdir, '.node-gyp')
448     log.warn('EACCES', 'user "%s" does not have permission to access the dev dir "%s"', osenv.user(), devDir)
449     log.warn('EACCES', 'attempting to reinstall using temporary dev dir "%s"', gyp.devDir)
450     if (process.cwd() == tmpdir) {
451       log.verbose('tmpdir == cwd', 'automatically will remove dev files after to save disk space')
452       gyp.todo.push({ name: 'remove', args: argv })
453     }
454     gyp.commands.install(argv, cb)
455   }
456
457 }