diff options
Diffstat (limited to 'deps/npm/lib/install.js')
-rw-r--r-- | deps/npm/lib/install.js | 608 |
1 files changed, 608 insertions, 0 deletions
diff --git a/deps/npm/lib/install.js b/deps/npm/lib/install.js new file mode 100644 index 0000000000..4d71b9a512 --- /dev/null +++ b/deps/npm/lib/install.js @@ -0,0 +1,608 @@ + +// npm install <pkg> <pkg> <pkg> +// +// See doc/install.md for more description + +// Managing "family" lists... +// every time we dive into a deeper node_modules folder, the "family" +// list that gets passed along uses the previous "family" list as +// it's __proto__. Any "resolved precise dependency" things that aren't +// already on this object get added, and then that's passed to the next +// generation of installation. + +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 dependencies in ./package.json if no argument supplied" + +install.completion = function (opts, cb) { + // install can complete to a folder with a package.json, or any package. + // if it has a slash, then it's gotta be a folder + // if it starts with https?://, then just give up, because it's a url + // for now, not yet implemented. + var registry = require("./utils/npm-registry-client/index.js") + registry.get("/-/short", function (er, pkgs) { + if (er) return cb() + if (!opts.partialWord) return cb(null, pkgs) + + var name = opts.partialWord.split("@").shift() + pkgs = pkgs.filter(function (p) { + return p.indexOf(name) === 0 + }) + + if (pkgs.length !== 1 && opts.partialWord === name) { + return cb(null, pkgs) + } + + registry.get(pkgs[0], function (er, d) { + if (er) return cb() + return cb(null, Object.keys(d["dist-tags"] || {}) + .concat(Object.keys(d.versions || {})) + .map(function (t) { + return pkgs[0] + "@" + t + })) + }) + }) +} + +var npm = require("./npm.js") + , semver = require("semver") + , readJson = require("./utils/read-json.js") + , log = require("./utils/log.js") + , path = require("path") + , fs = require("graceful-fs") + , cache = require("./cache.js") + , asyncMap = require("slide").asyncMap + , chain = require("slide").chain + , relativize = require("./utils/relativize.js") + , output + , url = require("url") + , mkdir = require("./utils/mkdir-p.js") + , lifecycle = require("./utils/lifecycle.js") + +function install (args, cb_) { + + function cb (er, installed) { + if (er) return cb_(er) + + output = output || require("./utils/output.js") + + var tree = treeify(installed) + , pretty = prettify(tree, installed) + + output.write(pretty, function (er) { + if (er) return cb_(er) + save(where, installed, tree, pretty, cb_) + }) + } + + // the /path/to/node_modules/.. + var where = path.resolve(npm.dir, "..") + + // internal api: install(where, what, cb) + if (arguments.length === 3) { + where = args + args = [].concat(cb_) // pass in [] to do default dep-install + cb_ = arguments[2] + log.verbose([where, args], "install(where, what)") + } + + if (!npm.config.get("global")) { + args = args.filter(function (a) { + return path.resolve(a) !== where + }) + } + + mkdir(where, function (er) { + if (er) return cb(er) + // install dependencies locally by default, + // or install current folder globally + if (!args.length) { + if (npm.config.get("global")) args = ["."] + else return readJson( path.resolve(where, "package.json") + , { dev: !npm.config.get("production") } + , function (er, data) { + if (er) return log.er(cb, "Couldn't read dependencies.")(er) + var deps = Object.keys(data.dependencies || {}) + log.verbose([where, deps], "where, deps") + var family = {} + , ancestors = {} + family[data.name] = ancestors[data.name] = data.version + installManyTop(deps.map(function (dep) { + var target = data.dependencies[dep] + , parsed = url.parse(target.replace(/^git\+/, "git")) + if (!parsed.protocol) { + target = dep + "@" + target + } + return target + }), where, family, ancestors, false, cb) + }) + } + + // initial "family" is the name:version of the root, if it's got + // a pacakge.json file. + readJson(path.resolve(where, "package.json"), function (er, data) { + if (er) data = null + var family = {} + , ancestors = {} + if (data) family[data.name] = ancestors[data.name] = data.version + var fn = npm.config.get("global") ? installMany : installManyTop + fn(args, where, family, ancestors, true, cb) + }) + }) +} + +// if the -S|--save option is specified, then write installed packages +// as dependencies to a package.json file. +// This is experimental. +function save (where, installed, tree, pretty, cb) { + if (!npm.config.get("save") || npm.config.get("global")) { + return cb(null, installed, tree, pretty) + } + // each item in the tree is a top-level thing that should be saved + // to the package.json file. + // The relevant tree shape is { <folder>: {what:<pkg>} } + var saveTarget = path.resolve(where, "package.json") + , things = Object.keys(tree).map(function (k) { + //log.warn(k, "k") + return tree[k].what.split("@") + }).reduce(function (set, k) { + var rangeDescriptor = semver.gte(k[1], "0.1.0") ? "~" : "" + set[k[0]] = rangeDescriptor + k[1] + return set + }, {}) + + //log.warn(things, "things") + + // don't use readJson, because we don't want to do all the other + // tricky npm-specific stuff that's in there. + fs.readFile(saveTarget, function (er, data) { + // ignore errors here, just don't save it. + try { + data = JSON.parse(data.toString("utf8")) + } catch (ex) { + er = ex + } + if (er) return cb(null, installed, tree, pretty) + + var deps = npm.config.get("dev") ? "devDependencies" : "dependencies" + deps = data[deps] = data[deps] || {} + + Object.keys(things).forEach(function (t) { + deps[t] = things[t] + }) + data = JSON.stringify(data, null, 2) + "\n" + fs.writeFile(saveTarget, data, function (er) { + cb(er, installed, tree, pretty) + }) + }) +} + + +// Outputting *all* the installed modules is a bit confusing, +// because the length of the path does not make it clear +// that the submodules are not immediately require()able. +// TODO: Show the complete tree, ls-style. +function prettify (tree, installed) { + if (npm.config.get("parseable")) return parseable(installed) + return Object.keys(tree).map(function (p) { + p = tree[p] + var c = "" + if (p.children && p.children.length) { + pref = "\n" + var l = p.children.pop() + c = p.children.map(function (c) { + var gc = c.children && c.children.length + ? " (" + c.children.map(function (gc) { + return gc.what + }).join(" ") + ")" + : "" + return "\n├── " + c.what + gc + }).join("") + "\n└── " + l.what + } + return [p.what, p.where, c].join(" ") + + }).join("\n") +} + +function parseable (installed) { + var long = npm.config.get("long") + , cwd = process.cwd() + return installed.map(function (item) { + return path.resolve(cwd, item[1]) + + ( long ? ":" + item[0] : "" ) + }).join("\n") +} + +function treeify (installed) { + // each item is [what, where, parent, parentDir] + // If no parent, then report it. + // otherwise, tack it into the parent's children list. + // If the parent isn't a top-level then ignore it. + var whatWhere = installed.reduce(function (l, r) { + var parentDir = r[3] + , parent = r[2] + , where = r[1] + , what = r[0] + l[where] = { parentDir: parentDir + , parent: parent + , children: [] + , where: where + , what: what } + return l + }, {}) + + //log.warn(whatWhere, "whatWhere") + return Object.keys(whatWhere).reduce(function (l, r) { + var ww = whatWhere[r] + //log.warn([r, ww], "r, ww") + if (!ww.parent) { + l[r] = ww + } else { + var p = whatWhere[ww.parentDir] + if (p) p.children.push(ww) + else l[r] = ww + } + return l + }, {}) +} + + +// just like installMany, but also add the existing packages in +// where/node_modules to the family object. +function installManyTop (what, where, family, ancestors, explicit, cb_) { + + function cb (er, d) { + if (explicit || er) return cb_(er, d) + // since this wasn't an explicit install, let's build the top + // folder, so that `npm install` also runs the lifecycle scripts. + npm.commands.build([where], false, true, function (er) { + return cb_(er, d) + }) + } + + if (explicit) return next() + + readJson(path.join(where, "package.json"), function (er, data) { + if (er) return next(er) + lifecycle(data, "preinstall", where, next) + }) + + function next (er) { + if (er) return cb(er) + installManyTop_(what, where, family, ancestors, explicit, cb) + } +} + +function installManyTop_ (what, where, family, ancestors, explicit, cb) { + var nm = path.resolve(where, "node_modules") + , names = explicit + ? what.map(function (w) { return w.split(/@/).shift() }) + : [] + + fs.readdir(nm, function (er, pkgs) { + if (er) return installMany(what, where, family, ancestors, explicit, cb) + pkgs = pkgs.filter(function (p) { + return !p.match(/^[\._-]/) + && (!explicit || names.indexOf(p) === -1) + }) + asyncMap(pkgs.map(function (p) { + return path.resolve(nm, p, "package.json") + }), function (jsonfile, cb) { + readJson(jsonfile, function (er, data) { + if (er) return cb(null, []) + return cb(null, [[data.name, data.version]]) + }) + }, function (er, packages) { + // add all the existing packages to the family list. + // however, do not add to the ancestors list. + packages.forEach(function (p) { + family[p[0]] = p[1] + }) + return installMany(what, where, family, ancestors, explicit, cb) + }) + }) +} + +function installMany (what, where, family, ancestors, explicit, cb) { + // 'npm install foo' should install the version of foo + // that satisfies the dep in the current folder. + // This will typically return immediately, since we already read + // this file family, and it'll be cached. + readJson(path.resolve(where, "package.json"), function (er, data) { + if (er) data = {} + + d = data.dependencies || {} + var parent = data._id + + log.verbose(what, "into "+where) + // what is a list of things. + // resolve each one. + asyncMap( what + , targetResolver(where, family, ancestors, explicit, d) + , function (er, targets) { + if (er) return cb(er) + // each target will be a data object corresponding + // to a package, folder, or whatever that is in the cache now. + var newPrev = Object.create(family) + , newAnc = Object.create(ancestors) + + newAnc[data.name] = data.version + targets.forEach(function (t) { + newPrev[t.name] = t.version + }) + log.silly(targets, "resolved") + targets.filter(function (t) { return t }).forEach(function (t) { + log.info(t._id, "into "+where) + }) + asyncMap(targets, function (target, cb) { + log(target._id, "installOne") + installOne(target, where, newPrev, newAnc, parent, cb) + }, cb) + }) + }) +} + +function targetResolver (where, family, ancestors, explicit, deps) { + var alreadyInstalledManually = explicit ? [] : null + , nm = path.resolve(where, "node_modules") + + if (!explicit) fs.readdir(nm, function (er, inst) { + if (er) return alreadyInstalledManually = [] + asyncMap(inst, function (pkg, cb) { + readJson(path.resolve(nm, pkg, "package.json"), function (er, d) { + if (er) return cb(null, []) + if (semver.satisfies(d.version, deps[d.name] || "*")) { + return cb(null, d.name) + } + return cb(null, []) + }) + }, function (er, inst) { + // this is the list of things that are valid and should be ignored. + alreadyInstalledManually = inst + }) + }) + + var to = 0 + return function resolver (what, cb) { + if (!alreadyInstalledManually) return setTimeout(function () { + resolver(what, cb) + }, to++) + // now we know what's been installed here manually, + // or tampered with in some way that npm doesn't want to overwrite. + if (alreadyInstalledManually.indexOf(what.split("@").shift()) !== -1) { + log.verbose("skipping "+what, "already installed in "+where) + return cb(null, []) + } + + if (family[what] && semver.satisfies(family[what], deps[what] || "")) { + return cb(null, []) + } + + if (deps[what]) { + what = what + "@" + deps[what] + } + log.verbose(what, "cache add") + cache.add(what, function (er, data) { + if (!er && data && family[data.name] === data.version) { + return cb(null, []) + } + return cb(er, data) + }) + } +} + +// we've already decided to install this. if anything's in the way, +// then uninstall it first. +function installOne (target, where, family, ancestors, parent, cb) { + // the --link flag makes this a "link" command if it's at the + // the top level. + if (where === npm.prefix && npm.config.get("link") + && !npm.config.get("global")) { + return localLink(target, where, family, ancestors, parent, cb) + } + installOne_(target, where, family, ancestors, parent, cb) +} + +function localLink (target, where, family, ancestors, parent, cb) { + log.verbose(target._id, "try to link") + var jsonFile = path.resolve( npm.dir, target.name + , "package.json" ) + + readJson(jsonFile, function (er, data) { + if (er || data._id === target._id) { + if (er) { + install( path.resolve(npm.globalDir, "..") + , target._id + , function (er) { + if (er) return cb(er, []) + thenLink() + }) + } else thenLink() + + function thenLink () { + npm.commands.link([target.name], function (er, d) { + log.silly([er, d], "back from link") + cb(er, [resultList(target, where, parent)]) + }) + } + + } else { + log.verbose(target._id, "install locally (no link)") + installOne_(target, where, family, ancestors, parent, cb) + } + }) +} + +function resultList (target, where, parent) { + var nm = path.resolve(where, "node_modules") + , targetFolder = path.resolve(nm, target.name) + , prettyWhere = relativize(where, process.cwd() + "/x") + + if (prettyWhere === ".") prettyWhere = null + + if (!npm.config.get("global")) { + // print out the folder relative to where we are right now. + // relativize isn't really made for dirs, so you need this hack + targetFolder = relativize(targetFolder, process.cwd()+"/x") + } + + return [ target._id + , targetFolder + , prettyWhere && parent + , parent && prettyWhere ] +} + +function installOne_ (target, where, family, ancestors, parent, cb) { + var nm = path.resolve(where, "node_modules") + , targetFolder = path.resolve(nm, target.name) + , prettyWhere = relativize(where, process.cwd() + "/x") + + if (prettyWhere === ".") prettyWhere = null + + chain + ( [ [checkEngine, target] + , [checkCycle, target, ancestors] + , [checkGit, targetFolder] + , [write, target, targetFolder, family, ancestors] ] + , function (er, d) { + log.verbose(target._id, "installOne cb") + if (er) return cb(er) + + d.push(resultList(target, where, parent)) + cb(er, d) + } + ) +} + +function checkEngine (target, cb) { + var npmv = npm.version + , force = npm.config.get("force") + , nodev = force ? null : npm.config.get("node-version") + , eng = target.engines + if (!eng) return cb() + if (nodev && eng.node && !semver.satisfies(nodev, eng.node) + || eng.npm && !semver.satisfies(npmv, eng.npm)) { + var er = new Error("Unsupported") + er.errno = npm.ENOTSUP + er.required = eng + er.pkgid = target._id + return cb(er) + } + return cb() +} + + +function checkCycle (target, ancestors, cb) { + // there are some very rare and pathological edge-cases where + // a cycle can cause npm to try to install a never-ending tree + // of stuff. + // Simplest: + // + // A -> B -> A' -> B' -> A -> B -> A' -> B' -> A -> ... + // + // Solution: Simply flat-out refuse to install any name@version + // that is already in the prototype tree of the ancestors object. + // A more correct, but more complex, solution would be to symlink + // the deeper thing into the new location. + // Will do that if anyone whines about this irl. + + var p = Object.getPrototypeOf(ancestors) + , name = target.name + , version = target.version + while (p && p !== Object.prototype && p[name] !== version) { + p = Object.getPrototypeOf(p) + } + if (p[name] !== version) return cb() + + var er = new Error("Unresolvable cycle detected") + var tree = [target._id, JSON.parse(JSON.stringify(ancestors))] + , t = Object.getPrototypeOf(ancestors) + while (t && t !== Object.prototype) { + if (t === p) t.THIS_IS_P = true + tree.push(JSON.parse(JSON.stringify(t))) + t = Object.getPrototypeOf(t) + } + log.verbose(tree, "unresolvable dependency tree") + er.pkgid = target._id + er.errno = npm.ECYCLE + return cb(er) +} + +function checkGit (folder, cb) { + // if it's a git repo then don't touch it! + fs.lstat(folder, function (er, s) { + if (er || !s.isDirectory()) return cb() + else checkGit_(folder, cb) + }) +} + +function checkGit_ (folder, cb) { + fs.stat(path.resolve(folder, ".git"), function (er, s) { + if (!er && s.isDirectory()) { + var e = new Error("Appears to be a git repo or submodule.") + e.path = folder + e.errno = npm.EISGIT + return cb(e) + } + cb() + }) +} + +function write (target, targetFolder, family, ancestors, cb_) { + var up = npm.config.get("unsafe-perm") + , user = up ? null : npm.config.get("user") + , group = up ? null : npm.config.get("group") + + function cb (er, data) { + // cache.unpack returns the data object, and all we care about + // is the list of installed packages from that last thing. + if (!er) return cb_(er, data) + log.error(er, "error installing "+target._id) + if (false === npm.config.get("rollback")) return cb_(er) + npm.commands.unbuild([targetFolder], function (er2) { + if (er2) log.error(er2, "error rolling back "+target._id) + return cb_(er, data) + }) + } + + chain + ( [ [ npm.commands.unbuild, [targetFolder] ] + , [ cache.unpack, target.name, target.version, targetFolder + , null, null, user, group ] + , [ lifecycle, target, "preinstall", targetFolder ] ] + + // nest the chain so that we can throw away the results returned + // up until this point, since we really don't care about it. + , function (er) { + if (er) return cb(er) + var deps = Object.keys(target.dependencies || {}) + installMany(deps.filter(function (d) { + // prefer to not install things that are satisfied by + // something in the "family" list. + return !semver.satisfies(family[d], target.dependencies[d]) + }).map(function (d) { + var t = target.dependencies[d] + , parsed = url.parse(t.replace(/^git\+/, "git")) + if (!parsed.protocol) { + t = d + "@" + t + } + return t + }), targetFolder, family, ancestors, false, function (er, d) { + //log.warn(d, "write installMany cb") + log.verbose(targetFolder, "about to build") + if (er) return cb(er) + npm.commands.build( [targetFolder] + , npm.config.get("global") + , true + , function (er) { return cb(er, d) }) + }) + } ) +} |