node/deps/npm/node_modules/node-gyp/lib/install.js
Rod Vagg e52f963632 node-gyp: download header tarball for compile
Temporary fix only, node-gyp needs to be aware of whether it can even
download this file for the current runtime so that information needs to
come from the process itself.

Floating patch on npm.

PR-URL: https://github.com/nodejs/io.js/pull/2066
Reviewed-By: Johan Bergström <bugs@bergstroem.nu>
Reviewed-By: Ryan Graham <ryan@strongloop.com>
2015-08-04 11:56:15 -07:00

481 lines
16 KiB
JavaScript

module.exports = exports = install
exports.usage = 'Install node development files for the specified node version.'
/**
* Module dependencies.
*/
var fs = require('graceful-fs')
, osenv = require('osenv')
, tar = require('tar')
, rm = require('rimraf')
, path = require('path')
, crypto = require('crypto')
, zlib = require('zlib')
, log = require('npmlog')
, semver = require('semver')
, fstream = require('fstream')
, request = require('request')
, minimatch = require('minimatch')
, mkdir = require('mkdirp')
, win = process.platform == 'win32'
function install (gyp, argv, callback) {
// ensure no double-callbacks happen
function cb (err) {
if (cb.done) return
cb.done = true
if (err) {
log.warn('install', 'got an error, rolling back install')
// roll-back the install if anything went wrong
gyp.commands.remove([ version ], function (err2) {
callback(err)
})
} else {
callback(null, version)
}
}
var defaultUrl = getDefaultIojsUrl(process.version)
var distUrl = gyp.opts['dist-url'] || gyp.opts.disturl || defaultUrl
// Determine which node dev files version we are installing
var versionStr = argv[0] || gyp.opts.target || process.version
log.verbose('install', 'input version string %j', versionStr)
// parse the version to normalize and ensure it's valid
var version = semver.parse(versionStr)
if (!version) {
return callback(new Error('Invalid version number: ' + versionStr))
}
if (semver.lt(versionStr, '0.8.0')) {
return callback(new Error('Minimum target version is `0.8.0` or greater. Got: ' + versionStr))
}
// 0.x.y-pre versions are not published yet and cannot be installed. Bail.
if (version.prerelease[0] === 'pre') {
log.verbose('detected "pre" node version', versionStr)
if (gyp.opts.nodedir) {
log.verbose('--nodedir flag was passed; skipping install', gyp.opts.nodedir)
callback()
} else {
callback(new Error('"pre" versions of node cannot be installed, use the --nodedir flag instead'))
}
return
}
// flatten version into String
version = version.version
log.verbose('install', 'installing version: %s', version)
// distributions starting with 0.10.0 contain sha256 checksums
var checksumAlgo = semver.gte(version, '0.10.0') ? 'sha256' : 'sha1'
// the directory where the dev files will be installed
var devDir = path.resolve(gyp.devDir, version)
// If '--ensure' was passed, then don't *always* install the version;
// check if it is already installed, and only install when needed
if (gyp.opts.ensure) {
log.verbose('install', '--ensure was passed, so won\'t reinstall if already installed')
fs.stat(devDir, function (err, stat) {
if (err) {
if (err.code == 'ENOENT') {
log.verbose('install', 'version not already installed, continuing with install', version)
go()
} else if (err.code == 'EACCES') {
eaccesFallback()
} else {
cb(err)
}
return
}
log.verbose('install', 'version is already installed, need to check "installVersion"')
var installVersionFile = path.resolve(devDir, 'installVersion')
fs.readFile(installVersionFile, 'ascii', function (err, ver) {
if (err && err.code != 'ENOENT') {
return cb(err)
}
var installVersion = parseInt(ver, 10) || 0
log.verbose('got "installVersion"', installVersion)
log.verbose('needs "installVersion"', gyp.package.installVersion)
if (installVersion < gyp.package.installVersion) {
log.verbose('install', 'version is no good; reinstalling')
go()
} else {
log.verbose('install', 'version is good')
cb()
}
})
})
} else {
go()
}
function download (url) {
log.http('GET', url)
var req = null
var requestOpts = {
uri: url
, headers: {
'User-Agent': 'node-gyp v' + gyp.version + ' (node ' + process.version + ')'
}
}
// basic support for a proxy server
var proxyUrl = gyp.opts.proxy
|| process.env.http_proxy
|| process.env.HTTP_PROXY
|| process.env.npm_config_proxy
if (proxyUrl) {
if (/^https?:\/\//i.test(proxyUrl)) {
log.verbose('download', 'using proxy url: "%s"', proxyUrl)
requestOpts.proxy = proxyUrl
} else {
log.warn('download', 'ignoring invalid "proxy" config setting: "%s"', proxyUrl)
}
}
try {
// The "request" constructor can throw sometimes apparently :(
// See: https://github.com/TooTallNate/node-gyp/issues/114
req = request(requestOpts)
} catch (e) {
cb(e)
}
if (req) {
req.on('response', function (res) {
log.http(res.statusCode, url)
})
}
return req
}
function getContentSha(res, callback) {
var shasum = crypto.createHash(checksumAlgo)
res.on('data', function (chunk) {
shasum.update(chunk)
}).on('end', function () {
callback(null, shasum.digest('hex'))
})
}
function go () {
log.verbose('ensuring nodedir is created', devDir)
// first create the dir for the node dev files
mkdir(devDir, function (err, created) {
if (err) {
if (err.code == 'EACCES') {
eaccesFallback()
} else {
cb(err)
}
return
}
if (created) {
log.verbose('created nodedir', created)
}
// now download the node tarball
var tarPath = gyp.opts['tarball']
var tarballUrl = tarPath ? tarPath : distUrl + '/v' + version + '/iojs-v' + version + '-headers.tar.gz'
, badDownload = false
, extractCount = 0
, gunzip = zlib.createGunzip()
, extracter = tar.Extract({ path: devDir, strip: 1, filter: isValid })
var contentShasums = {}
var expectShasums = {}
// checks if a file to be extracted from the tarball is valid.
// only .h header files and the gyp files get extracted
function isValid () {
var name = this.path.substring(devDir.length + 1)
var isValid = valid(name)
if (name === '' && this.type === 'Directory') {
// the first directory entry is ok
return true
}
if (isValid) {
log.verbose('extracted file from tarball', name)
extractCount++
} else {
// invalid
log.silly('ignoring from tarball', name)
}
return isValid
}
gunzip.on('error', cb)
extracter.on('error', cb)
extracter.on('end', afterTarball)
// download the tarball, gunzip and extract!
if (tarPath) {
var input = fs.createReadStream(tarballUrl)
input.pipe(gunzip).pipe(extracter)
return
}
var req = download(tarballUrl)
if (!req) return
// something went wrong downloading the tarball?
req.on('error', function (err) {
if (err.code === 'ENOTFOUND') {
return cb(new Error('This is most likely not a problem with node-gyp or the package itself and\n' +
'is related to network connectivity. In most cases you are behind a proxy or have bad \n' +
'network settings.'))
}
badDownload = true
cb(err)
})
req.on('close', function () {
if (extractCount === 0) {
cb(new Error('Connection closed while downloading tarball file'))
}
})
req.on('response', function (res) {
if (res.statusCode !== 200) {
badDownload = true
cb(new Error(res.statusCode + ' response downloading ' + tarballUrl))
return
}
// content checksum
getContentSha(res, function (_, checksum) {
var filename = path.basename(tarballUrl).trim()
contentShasums[filename] = checksum
log.verbose('content checksum', filename, checksum)
})
// start unzipping and untaring
req.pipe(gunzip).pipe(extracter)
})
// invoked after the tarball has finished being extracted
function afterTarball () {
if (badDownload) return
if (extractCount === 0) {
return cb(new Error('There was a fatal problem while downloading/extracting the tarball'))
}
log.verbose('tarball', 'done parsing tarball')
var async = 0
if (win) {
// need to download iojs.lib
async++
downloadNodeLib(deref)
}
// write the "installVersion" file
async++
var installVersionPath = path.resolve(devDir, 'installVersion')
fs.writeFile(installVersionPath, gyp.package.installVersion + '\n', deref)
// Only download SHASUMS.txt if not using tarPath override
if (!tarPath) {
// download SHASUMS.txt
async++
downloadShasums(deref)
}
if (async === 0) {
// no async tasks required
cb()
}
function deref (err) {
if (err) return cb(err)
async--
if (!async) {
log.verbose('download contents checksum', JSON.stringify(contentShasums))
// check content shasums
for (var k in contentShasums) {
log.verbose('validating download checksum for ' + k, '(%s == %s)', contentShasums[k], expectShasums[k])
if (contentShasums[k] !== expectShasums[k]) {
cb(new Error(k + ' local checksum ' + contentShasums[k] + ' not match remote ' + expectShasums[k]))
return
}
}
cb()
}
}
}
function downloadShasums(done) {
var shasumsFile = (checksumAlgo === 'sha256') ? 'SHASUMS256.txt' : 'SHASUMS.txt'
log.verbose('check download content checksum, need to download `' + shasumsFile + '`...')
var shasumsPath = path.resolve(devDir, shasumsFile)
, shasumsUrl = distUrl + '/v' + version + '/' + shasumsFile
log.verbose('checksum url', shasumsUrl)
var req = download(shasumsUrl)
if (!req) return
req.on('error', done)
req.on('response', function (res) {
if (res.statusCode !== 200) {
done(new Error(res.statusCode + ' status code downloading checksum'))
return
}
var chunks = []
res.on('data', function (chunk) {
chunks.push(chunk)
})
res.on('end', function () {
var lines = Buffer.concat(chunks).toString().trim().split('\n')
lines.forEach(function (line) {
var items = line.trim().split(/\s+/)
if (items.length !== 2) return
// 0035d18e2dcf9aad669b1c7c07319e17abfe3762 ./node-v0.11.4.tar.gz
var name = items[1].replace(/^\.\//, '')
expectShasums[name] = items[0]
})
log.verbose('checksum data', JSON.stringify(expectShasums))
done()
})
})
}
function downloadNodeLib (done) {
log.verbose('on Windows; need to download `iojs.lib`...')
var dir32 = path.resolve(devDir, 'ia32')
, dir64 = path.resolve(devDir, 'x64')
, nodeLibPath32 = path.resolve(dir32, 'iojs.lib')
, nodeLibPath64 = path.resolve(dir64, 'iojs.lib')
, nodeLibUrl32 = distUrl + '/v' + version + '/win-x86/iojs.lib'
, nodeLibUrl64 = distUrl + '/v' + version + '/win-x64/iojs.lib'
log.verbose('32-bit iojs.lib dir', dir32)
log.verbose('64-bit iojs.lib dir', dir64)
log.verbose('`iojs.lib` 32-bit url', nodeLibUrl32)
log.verbose('`iojs.lib` 64-bit url', nodeLibUrl64)
var async = 2
mkdir(dir32, function (err) {
if (err) return done(err)
log.verbose('streaming 32-bit iojs.lib to:', nodeLibPath32)
var req = download(nodeLibUrl32)
if (!req) return
req.on('error', done)
req.on('response', function (res) {
if (res.statusCode !== 200) {
done(new Error(res.statusCode + ' status code downloading 32-bit iojs.lib'))
return
}
getContentSha(res, function (_, checksum) {
contentShasums['win-x86/iojs.lib'] = checksum
log.verbose('content checksum', 'win-x86/iojs.lib', checksum)
})
var ws = fs.createWriteStream(nodeLibPath32)
ws.on('error', cb)
req.pipe(ws)
})
req.on('end', function () {
--async || done()
})
})
mkdir(dir64, function (err) {
if (err) return done(err)
log.verbose('streaming 64-bit iojs.lib to:', nodeLibPath64)
var req = download(nodeLibUrl64)
if (!req) return
req.on('error', done)
req.on('response', function (res) {
if (res.statusCode !== 200) {
done(new Error(res.statusCode + ' status code downloading 64-bit iojs.lib'))
return
}
getContentSha(res, function (_, checksum) {
contentShasums['win-x64/iojs.lib'] = checksum
log.verbose('content checksum', 'win-x64/iojs.lib', checksum)
})
var ws = fs.createWriteStream(nodeLibPath64)
ws.on('error', cb)
req.pipe(ws)
})
req.on('end', function () {
--async || done()
})
})
} // downloadNodeLib()
}) // mkdir()
} // go()
/**
* Checks if a given filename is "valid" for this installation.
*/
function valid (file) {
// header files
return minimatch(file, '*.h', { matchBase: true }) ||
minimatch(file, '*.gypi', { matchBase: true })
}
/**
* The EACCES fallback is a workaround for npm's `sudo` behavior, where
* it drops the permissions before invoking any child processes (like
* node-gyp). So what happens is the "nobody" user doesn't have
* permission to create the dev dir. As a fallback, make the tmpdir() be
* the dev dir for this installation. This is not ideal, but at least
* the compilation will succeed...
*/
function eaccesFallback () {
var tmpdir = osenv.tmpdir()
gyp.devDir = path.resolve(tmpdir, '.node-gyp')
log.warn('EACCES', 'user "%s" does not have permission to access the dev dir "%s"', osenv.user(), devDir)
log.warn('EACCES', 'attempting to reinstall using temporary dev dir "%s"', gyp.devDir)
if (process.cwd() == tmpdir) {
log.verbose('tmpdir == cwd', 'automatically will remove dev files after to save disk space')
gyp.todo.push({ name: 'remove', args: argv })
}
gyp.commands.install(argv, cb)
}
}
// pick out 'nightly', 'next-nightly' or 'rc' from the version string if it's there
// adjust URL accordingly
function getDefaultIojsUrl(version) {
var versionMatch = version.match(/^v\d+\.\d+\.\d+-(?:(?:(nightly|next-nightly)\d{8}[0-9a-f]{10})|(?:(rc)\d+))$/)
var distType = versionMatch ? versionMatch[1] || versionMatch[2] : 'release'
var defaultUrl = `https://iojs.org/download/${distType}`
return defaultUrl
}
if (require.main === module) {
var assert = require('assert')
console.log('test v2.3.4 -> https://iojs.org/download/release')
assert(getDefaultIojsUrl('v2.3.4', 'https://iojs.org/download/release'))
console.log('test v2.3.4-nightly12345678aaaaaaaaaa -> https://iojs.org/download/nightly')
assert(getDefaultIojsUrl('v2.3.4-nightly12345678aaaaaaaaaa', 'https://iojs.org/download/nightly'))
console.log('test v2.3.4-next-nightly12345678aaaaaaaaaa -> https://iojs.org/download/release/next-nightly')
assert(getDefaultIojsUrl('v2.3.4-next-nightly12345678aaaaaaaaaa', 'https://iojs.org/download/next-nightly'))
console.log('test v2.3.4-rc100 -> https://iojs.org/download/rc')
assert(getDefaultIojsUrl('v2.3.4-rc100', 'https://iojs.org/download/rc'))
}