Files
npm/lib/install.js
isaacs ae6a9ceb7c If latest not supported, use the highest version
This lets you do stuff like `npm install foo` to get the latest version
supported by your version of node, even if that's not actually
foo@latest.
2010-12-03 11:30:25 -08:00

295 lines
9.8 KiB
JavaScript

// npm install <pkg> <pkg> <pkg>
// npm install <pkg@version> <pkg@"1.0.0 - 1.99.99"> <pkg[@latest]> <pkg@tagname>
// 1. fetch the data for that package/tag into the cache
// 2. if it has any dependents, which are not yet installed,
// then add those to the list, and fetch their data.
// 3. when all the pkgs are fetched to the cache, and we have a set
// of packages that are either installed or fetched which
// will satisfy everyone's dependencies, then untar into the
// target directories for each of them.
// 4. build each of the packages that aren't already installed
module.exports = install
install.usage = "npm install <tarball file>"
+ "\nnpm install <tarball url>"
+ "\nnpm install <folder>"
+ "\nnpm install <pkg>"
+ "\nnpm install <pkg>@<tag>"
+ "\nnpm install <pkg>@<version>"
+ "\nnpm install <pkg>@<version range>"
+ "\n\nCan specify one or more: npm install ./foo.tgz bar@stable /some/folder"
+ "\nInstalls '.' if no argument supplied"
install.completion = function (args, index, cb) {
var remotePkgs = require("./utils/completion/remote-packages")
remotePkgs(args, index, true, true, true, cb)
}
var registry = require("./utils/registry")
, npm = require("../npm")
, readInstalled = require("./utils/read-installed")
, installedPackages
, semver = require("./utils/semver")
, url = require("url")
, fetch = require("./utils/fetch")
, mkdir = require("./utils/mkdir-p")
, readJson = require("./utils/read-json")
, log = require("./utils/log")
, path = require("path")
, fs = require("./utils/graceful-fs")
, cache = require("./cache")
, asyncMap = require("./utils/async-map")
function install (pkglist, cb) {
log.verbose(pkglist, "install")
if (pkglist.length === 0) pkglist = ["."]
// it's helpful to know what we have already
if (!installedPackages) return readInstalled([], function (er, data) {
if (er) return cb(er)
installedPackages = data || {}
install(pkglist, cb)
})
log.verbose(pkglist, "install pkglist")
var mustInstall = npm.config.get("must-install") ? pkglist.slice(0) : []
// three lists: "pkglist", "next", and "reg"
// asyncMap over the "left" list: for each "it"
// find out what it is
// if it's version/range installed or on "reg" list, continue.
// if it's a url, fetch to cache and add the name/version to "next"
// if it's a tag, fetch the json, add the version to "next"
// if it's a version(range) not installed, then fetch the json,
// add url to "next"
// if it's a specific version in cache, then unpack, add to "reg"
// list, add its deps to "next"
// if the "next" list is not empty, then pkglist=next,next=[], and repeat.
// if it is, then build all the "reg" folders.
var reg = Object.create(installedPackages)
, seen = {}
log.verbose(mustInstall, "must install")
asyncMap(pkglist, function (pkg, cb) {
install_(pkg, reg, seen, mustInstall.indexOf(pkg) !== -1, pkglist, cb)
}, function (er) {
if (er) return cb(er)
buildAll(reg, cb)
})
}
// call the cb with the "next" thing(s) to look up for this one, or nothing
function install_ (pkg, reg, seen, mustHave, pkglist, cb) {
log.verbose(pkg, "install_")
if (seen[pkg]) return cb() // repeat, skip it
seen[pkg] = true
// it's a local thing or a url if it has a / in it.
if (pkg.indexOf("/") !== -1 || pkg === ".") {
log.silly(pkg, "install local")
return cache.add(pkg, finisher(pkg, reg, pkglist, cb))
}
// now we know it's not a URL or file,
// so handle it like a tag, version, or range.
pkg = pkg.split("@")
var name = pkg[0]
, defTag = npm.config.get("tag")
, ver = pkg.slice(1).join("@").trim() || defTag
, range = semver.validRange(ver)
, exact = semver.valid(ver)
, tag = !exact && !range && range !== "" && ver
log.verbose([pkg, mustHave], "must install?")
pkg = pkg.join("@")
seen[name+"@"+ver] = true
// if there is a satisfying version already, then simply move on.
if (!tag && findSatisfying(pkg, name, range, mustHave, reg)) {
return cb()
}
// at this point, assume that it has to be installed.
if (exact) {
log.verbose("exact", pkg)
// just pull the data out of the cache to ensure it's there
return cache.read(name, exact, finisher(pkg, reg, pkglist, cb))
}
getData(name, function (er, data) {
if (er) return cb(er)
log.silly(data, pkg)
if (tag) {
log.verbose(tag, pkg+" tag")
var tags = data["dist-tags"]
if (!tags[tag]) cb(new Error(
"Tag not found: "+data.name+"@"+tag
+"\nValid install targets for "+data.name+": "
+installTargets(data)))
install_(data.name+"@"+tags[tag], reg, seen, mustHave, pkglist, cb)
} else {
log(pkg, "range")
// prefer the default tag version.
var defTag = npm.config.get("tag")
, satis
defTag = defTag && data["dist-tags"] && data["dist-tags"][defTag]
if (semver.satisfies(defTag, range)) satis = defTag
else satis = semver.maxSatisfying(Object.keys(data.versions), range)
if (!satis) return cb(new Error(
"No satisfying version found for '"+data.name+"'@'"+range+"'"
+"\nValid install targets for "+data.name+": "
+installTargets(data)))
install_(data.name+"@"+satis, reg, seen, mustHave, pkglist, cb)
}
})
}
function installTargets (data) {
return Object.keys(data["dist-tags"])
.concat(Object.keys(data.versions))
.map(JSON.stringify)
.join(", ") || "(none)"
}
function getData (name, cb) {
var data = npm.get(name)
if (data) return cb(null, data)
registry.get(name, function (er, data) {
if (er) return cb(er)
if (!data._id) data._id = name
for (var ver in data.versions) {
try {
readJson.processJson(data.versions[ver])
} catch (ex) {
log(ver, "ignoring invalid version")
delete data.versions[ver]
}
}
filterNodeVersion(data)
npm.set(data)
cb(null, data)
})
}
// see if there is a satisfying version already
function findSatisfying (pkg, name, range, mustHave, reg) {
if (mustHave) return null
return semver.maxSatisfying
( Object.keys(reg[name] || {})
.concat(Object.keys(Object.getPrototypeOf(reg[name] || {})))
, range
)
}
function finisher (pkg, reg, pkglist, cb) { return function (er, data) {
if (er) return log.er(cb, "Error installing "+pkg)(er)
if (!data._nodeSupported) cb(
data.name+"@"+data.version+" not compatible with your version of node\n"+
"Requires: node@"+data.engines.node+"\n"+
"You have: node@"+process.version)
if (!reg.hasOwnProperty(data.name)) {
reg[data.name] = Object.create(reg[data.name] || Object.prototype)
}
if (!reg[data.name].hasOwnProperty(data.version)) {
reg[data.name][data.version] = {}
}
reg[data.name][data.version] = data
// also add the dependencies.
if (!pkglist) pkglist = []
getDeps(data, function (er, deps) {
if (!Array.isArray(pkglist)) pkglist = [pkglist]
pkglist.push.apply(pkglist, Object.keys(deps).map(function (dep) {
var v = deps[dep].trim()
, d = dep.trim()
return v ? d + "@" + v : d
}))
cb()
})
}}
function getDeps (data, cb) {
var deps = data.dependencies || {}
, devDeps = data.devDependencies || {}
if (npm.config.get("dev")) {
Object.keys(devDeps).forEach(function (d) { deps[d] = devDeps[d] })
}
// now see if any of them are already bundled.
// if so, omit them from the list.
var bundle = path.join( npm.cache, data.name, data.version
, "package", "node_modules" )
fs.readdir(bundle, function (er, bundles) {
bundles = bundles || []
bundles.forEach(function (b) { delete deps[b] })
cb(null, deps)
})
}
function filterNodeVersion (data) {
var supported = []
Object.keys(data.versions).forEach(function (v) {
if (!data.versions[v]._nodeSupported) {
log.verbose(data.versions[v]._id, "not supported on node@"+process.version)
delete data.versions[v]
} else supported.push(data.versions[v]._id)
})
if (!supported.length) {
log.warn(data._id, "Not supported on node@"+process.version)
} else log.verbose(supported, "Supported versions")
// might have deleted the special "latest" tag. if so, replace it.
var v = data["dist-tags"].latest
if (v && !data.versions[v] && supported.length) {
data["dist-tags"].latest = Object.keys(data.versions)
.sort(semver.compare).pop()
log("not supported by node@"+process.version,
"latest = "+data.name+"@"+v)
}
}
function buildAll (installed, cb) {
var list = []
Object.keys(installed).forEach(function (i) {
Object.keys(installed[i]).forEach(function (v) {
list.push([i, v])
})
})
log.verbose(list, "install list")
cb = rollbackFailure(list, cb)
var buildList = []
asyncMap(list, function (i, cb) {
var target = path.join(npm.dir, i[0], i[1])
cache.unpack(i[0], i[1], target, cb)
buildList.push(target)
}, function (er) {
if (er) return cb(er)
log.verbose(list.join("\n"), "unpacked, building")
if (buildList.length) npm.commands.build(buildList, cb)
else {
log.info("Nothing to do", "install")
cb()
}
})
}
function rollbackFailure (installList, cb) { return function (er) {
if (!er) return log.verbose(installList.map(function (i) {
return i.join("@")
}).join("\n"), "installed", cb)
// error happened, roll back
installList = installList.map(function (p) {
return p.join('@')
})
npm.ROLLBACK = true
log.error(er, "install failed")
log("rollback", "install failed")
log(installList, "uninstall")
return npm.commands.uninstall
( installList
, function (er_) {
if (er_) log.error(er_, "rollback failed")
else log("rolled back", "install failed")
cb(er)
}
)
}}