// npm install // // See doc/install.md for more description // Managing contexts... // there's a lot of state associated with an "install" operation, including // packages that are already installed, parent packages, current shrinkwrap, and // so on. We maintain this state in a "context" object that gets passed around. // every time we dive into a deeper node_modules folder, the "family" list that // gets passed along uses the previous "family" list as its __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 " + "\nnpm install " + "\nnpm install " + "\nnpm install " + "\nnpm install @" + "\nnpm install @" + "\nnpm install @" + "\n\nCan specify one or more: npm install ./foo.tgz bar@stable /some/folder" + "\nIf no argument is supplied and ./npm-shrinkwrap.json is " + "\npresent, installs dependencies specified in the shrinkwrap." + "\nOtherwise, installs dependencies from ./package.json." 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("mkdirp") , lifecycle = require("./utils/lifecycle.js") , archy = require("archy") 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).trim() if (pretty) output.write(pretty, afterWrite) else afterWrite() function afterWrite (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, made) { 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 readDependencies( null , where , { 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 context = { family: {} , ancestors: {} , explicit: false , parent: data , wrap: null } context.family[data.name] = context.ancestors[data.name] = data.version installManyTop(deps.map(function (dep) { var target = data.dependencies[dep] , parsed = url.parse(target.replace(/^git\+/, "git")) target = dep + "@" + target return target }), where, context, cb) }) } // initial "family" is the name:version of the root, if it's got // a package.json file. readJson(path.resolve(where, "package.json"), function (er, data) { if (er) data = null var context = { family: {} , ancestors: {} , explicit: true , parent: data , wrap: null } if (data) { context.family[data.name] = context.ancestors[data.name] = data.version } var fn = npm.config.get("global") ? installMany : installManyTop fn(args, where, context, cb) }) }) } // reads dependencies for the package at "where". There are several cases, // depending on our current state and the package's configuration: // // 1. If "context" is specified, then we examine the context to see if there's a // shrinkwrap there. In that case, dependencies are read from the shrinkwrap. // 2. Otherwise, if an npm-shrinkwrap.json file is present, dependencies are // read from there. // 3. Otherwise, dependencies come from package.json. // // Regardless of which case we fall into, "cb" is invoked with a first argument // describing the full package (as though readJson had been used) but with // "dependencies" read as described above. The second argument to "cb" is the // shrinkwrap to use in processing this package's dependencies, which may be // "wrap" (in case 1) or a new shrinkwrap (in case 2). function readDependencies (context, where, opts, cb) { var wrap = context ? context.wrap : null readJson( path.resolve(where, "package.json") , opts , function (er, data) { if (er) return cb(er) if (wrap) { log.verbose([where, wrap], "readDependencies: using existing wrap") var rv = {} Object.keys(data).forEach(function (key) { rv[key] = data[key] }) rv.dependencies = {} Object.keys(wrap).forEach(function (key) { log.verbose([key, wrap[key]], "from wrap") var w = wrap[key] rv.dependencies[key] = w.from || w.version }) log.verbose([rv.dependencies], "readDependencies: returned deps") return cb(null, rv, wrap) } var wrapfile = path.resolve(where, "npm-shrinkwrap.json") fs.readFile(wrapfile, "utf8", function (er, wrapjson) { if (er) { log.verbose("readDependencies: using package.json deps") return cb(null, data, null) } try { var newwrap = JSON.parse(wrapjson) } catch (ex) { return cb(ex) } log.info(wrapfile, "using shrinkwrap file") var rv = {} Object.keys(data).forEach(function (key) { rv[key] = data[key] }) rv.dependencies = {} Object.keys(newwrap.dependencies || {}).forEach(function (key) { var w = newwrap.dependencies[key] rv.dependencies[key] = w.from || w.version }) log.verbose([rv.dependencies], "readDependencies: returned deps") return cb(null, rv, newwrap.dependencies) }) }) } // 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("save-dev") && !npm.config.get("save-optional") || 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 { : {what:} } var saveTarget = path.resolve(where, "package.json") , things = Object.keys(tree).map(function (k) { // if "what" was a url, then save that instead. var t = tree[k] , u = url.parse(t.from) , w = t.what.split("@") if (u && u.protocol) w[1] = t.from return w }).reduce(function (set, k) { var rangeDescriptor = semver.valid(k[1]) && semver.gte(k[1], "0.1.0") ? "~" : "" set[k[0]] = rangeDescriptor + k[1] return set }, {}) // 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("save-optional") ? "optionalDependencies" : npm.config.get("save-dev") ? "devDependencies" : "dependencies" data[deps] = data[deps] || {} Object.keys(things).forEach(function (t) { data[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, but only if --long is provided function prettify (tree, installed) { if (npm.config.get("json")) { function red (set, kv) { set[kv[0]] = kv[1] return set } tree = Object.keys(tree).map(function (p) { if (!tree[p]) return null var what = tree[p].what.split("@") , name = what.shift() , version = what.join("@") , o = { name: name, version: version, from: tree[p].from } o.dependencies = tree[p].children.map(function P (dep) { var what = dep.what.split("@") , name = what.shift() , version = what.join("@") , o = { version: version, from: dep.from } o.dependencies = dep.children.map(P).reduce(red, {}) return [name, o] }).reduce(red, {}) return o }) return JSON.stringify(tree, null, 2) } if (npm.config.get("parseable")) return parseable(installed) return Object.keys(tree).map(function (p) { return archy({ label: tree[p].what + " " + p , nodes: (tree[p].children || []).map(function P (c) { if (npm.config.get("long")) { return { label: c.what, nodes: c.children.map(P) } } var g = c.children.map(function (g) { return g.what }).join(", ") if (g) g = " (" + g + ")" return c.what + g }) }) }).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] , from = r[4] l[where] = { parentDir: parentDir , parent: parent , children: [] , where: where , what: what , from: from } 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, context, cb_) { function cb (er, d) { if (context.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 (context.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, context, cb) } } function installManyTop_ (what, where, context, cb) { var nm = path.resolve(where, "node_modules") , names = context.explicit ? what.map(function (w) { return w.split(/@/).shift() }) : [] fs.readdir(nm, function (er, pkgs) { if (er) return installMany(what, where, context, cb) pkgs = pkgs.filter(function (p) { return !p.match(/^[\._-]/) }) 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) { context.family[p[0]] = p[1] }) return installMany(what, where, context, cb) }) }) } function installMany (what, where, context, cb) { // readDependencies takes care of figuring out whether the list of // dependencies we'll iterate below comes from an existing shrinkwrap from a // parent level, a new shrinkwrap at this level, or package.json at this // level, as well as which shrinkwrap (if any) our dependencies should use. readDependencies(context, where, {}, function (er, data, wrap) { if (er) data = {} var parent = data var d = data.dependencies || {} // if we're explicitly installing "what" into "where", then the shrinkwrap // for "where" doesn't apply. This would be the case if someone were adding // a new package to a shrinkwrapped package. (data.dependencies will not be // used here except to indicate what packages are already present, so // there's no harm in using that.) if (context.explicit) wrap = null // what is a list of things. // resolve each one. asyncMap( what , targetResolver(where, context, 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(context.family) , newAnc = Object.create(context.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.info(target._id, "installOne") var newWrap = wrap ? wrap[target.name].dependencies || {} : null var newContext = { family: newPrev , ancestors: newAnc , parent: parent , explicit: false , wrap: newWrap } installOne(target, where, newContext, cb) }, cb) }) }) } function targetResolver (where, context, deps) { var alreadyInstalledManually = context.explicit ? [] : null , nm = path.resolve(where, "node_modules") , parent = context.parent , wrap = context.wrap if (!context.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) { // error means it's not a package, most likely. if (er) return cb(null, []) // if it's a bundled dep, then assume that anything there is valid. // otherwise, make sure that it's a semver match with what we want. var bd = parent.bundleDependencies if (bd && bd.indexOf(d.name) !== -1 || semver.satisfies(d.version, deps[d.name] || "*")) { return cb(null, d.name) } // something is there, but it's not satisfactory. Clobber it. 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, []) } // check for a version installed higher in the tree. // If installing from a shrinkwrap, it must match exactly. if (context.family[what]) { if (wrap && wrap[what].version === context.family[what]) { log.verbose(what, "using existing (matches shrinkwrap)") return cb(null, []) } } // if it's identical to its parent, then it's probably someone // doing `npm install foo` inside of the foo project. Print // a warning, and skip it. if (parent && parent.name === what && !npm.config.get("force")) { log.warn("Refusing to install "+what+" as a dependency of itself" ,"install") return cb(null, []) } if (wrap) { name = what.split(/@/).shift() if (wrap[name]) { var wrapTarget = wrap[name].from || wrap[name].version log.verbose("resolving "+what+" to "+wrapTarget, "shrinkwrap") what = name + "@" + wrapTarget } else { log.verbose("skipping "+what+" (not in shrinkwrap)", "shrinkwrap") } } else if (deps[what]) { what = what + "@" + deps[what] } cache.add(what, function (er, data) { if (er && parent && parent.optionalDependencies && parent.optionalDependencies.hasOwnProperty(what.split("@")[0])) { log.warn(what, "optional dependency failed, continuing") log.verbose([what, er], "optional dependency failed, continuing") return cb(null, []) } if (!er && data && !context.explicit && context.family[data.name] === data.version && !npm.config.get("force")) { log.info(data.name + "@" + data.version, "already installed") return cb(null, []) } if (data) data._from = what 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, context, 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, context, cb) } installOne_(target, where, context, function (er, installedWhat) { // check if this one is optional to its parent. if (er && context.parent && context.parent.optionalDependencies && context.parent.optionalDependencies.hasOwnProperty(target.name)) { log.warn(target._id, "optional dependency failed, continuing") log.verbose([target._id, er], "optional dependency failed, continuing") er = null } cb(er, installedWhat) }) } function localLink (target, where, context, cb) { log.verbose(target._id, "try to link") var jsonFile = path.resolve( npm.dir, target.name , "package.json" ) , parent = context.parent 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 && parent._id)]) }) } } else { log.verbose(target._id, "install locally (no link)") installOne_(target, where, context, cb) } }) } function resultList (target, where, parentId) { 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 && parentId , parentId && prettyWhere , target._from ] } function installOne_ (target, where, context, cb) { var nm = path.resolve(where, "node_modules") , targetFolder = path.resolve(nm, target.name) , prettyWhere = relativize(where, process.cwd() + "/x") , parent = context.parent if (prettyWhere === ".") prettyWhere = null chain ( [ [checkEngine, target] , [checkPlatform, target] , [checkCycle, target, context.ancestors] , [checkGit, targetFolder] , [write, target, targetFolder, context] ] , function (er, d) { if (er) return cb(er) d.push(resultList(target, where, parent && parent._id)) 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 checkPlatform (target, cb) { var platform = process.platform , arch = process.arch , osOk = true , cpuOk = true , force = npm.config.get("force") if (force) { return cb() } if (target.os) { osOk = checkList(platform, target.os) } if (target.cpu) { cpuOk = checkList(arch, target.cpu) } if (!osOk || !cpuOk) { var er = new Error("Unsupported") er.errno = npm.EBADPLATFORM er.os = target.os || ['any'] er.cpu = target.cpu || ['any'] er.pkgid = target._id return cb(er) } return cb() } function checkList (value, list) { var tmp , match = false , blc = 0 if (typeof list === "string") { list = [list] } if (list.length === 1 && list[0] === "any") { return true; } for (var i = 0; i < list.length; ++i) { tmp = list[i] if (tmp[0] === '!') { tmp = tmp.slice(1) if (tmp === value) { return false; } ++blc } else { match = match || tmp === value } } return match || blc === list.length } 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. // // Note: `npm install foo` inside of the `foo` package will abort // earlier if `--force` is not set. However, if it IS set, then // we need to still fail here, but just skip the first level. Of // course, it'll still fail eventually if it's a true cycle, and // leave things in an undefined state, but that's what is to be // expected when `--force` is used. That is why getPrototypeOf // is used *twice* here: to skip the first level of repetition. var p = Object.getPrototypeOf(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, context, cb_) { var up = npm.config.get("unsafe-perm") , user = up ? null : npm.config.get("user") , group = up ? null : npm.config.get("group") , family = context.family 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) 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) }) } var bundled = [] chain ( [ [ cache.unpack, target.name, target.version, targetFolder , null, null, user, group ] , [ fs, "writeFile" , path.resolve(targetFolder, "package.json") , JSON.stringify(target, null, 2) + "\n" ] , [ lifecycle, target, "preinstall", targetFolder ] , function (cb) { if (!target.bundleDependencies) return cb() var bd = path.resolve(targetFolder, "node_modules") fs.readdir(bd, function (er, b) { // nothing bundled, maybe if (er) return cb() bundled = b || [] cb() }) } ] // 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 X (er) { if (er) return cb(er) // before continuing to installing dependencies, check for a shrinkwrap. readDependencies(context, targetFolder, {}, function (er, data, wrap) { var deps = Object.keys(data.dependencies || {}) // don't install bundleDependencies, unless they're missing. if (data.bundleDependencies) { deps = deps.filter(function (d) { return data.bundleDependencies.indexOf(d) === -1 || bundled.indexOf(d) === -1 }) } var newcontext = { family: family , ancestors: context.ancestors , parent: target , explicit: false , wrap: wrap } installMany(deps.filter(function (d) { // prefer to not install things that are satisfied by // something in the "family" list, unless we're installing // from a shrinkwrap. return wrap || !semver.satisfies(family[d], data.dependencies[d]) }).map(function (d) { var t = data.dependencies[d] , parsed = url.parse(t.replace(/^git\+/, "git")) t = d + "@" + t return t }), targetFolder, newcontext, function (er, d) { 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) }) }) }) }) }