// npm build command // everything about the installation after the creation of // the .npm/{name}/{version}/package folder. // linking the modules into the npm.root, // resolving dependencies, etc. // This runs AFTER install or link are completed. var npm = require('./npm.js') var log = require('npmlog') var chain = require('slide').chain var fs = require('graceful-fs') var path = require('path') var lifecycle = require('./utils/lifecycle.js') var readJson = require('read-package-json') var link = require('./utils/link.js') var linkIfExists = link.ifExists var cmdShim = require('cmd-shim') var cmdShimIfExists = cmdShim.ifExists var asyncMap = require('slide').asyncMap var ini = require('ini') var writeFile = require('write-file-atomic') var packageId = require('./utils/package-id.js') var output = require('./utils/output.js') module.exports = build build.usage = 'npm build []' build._didBuild = {} build._noLC = {} function build (args, global, didPre, didRB, cb) { if (typeof cb !== 'function') { cb = didRB didRB = false } if (typeof cb !== 'function') { cb = didPre didPre = false } if (typeof cb !== 'function') { cb = global global = npm.config.get('global') } // it'd be nice to asyncMap these, but actually, doing them // in parallel generally munges up the output from node-waf var builder = build_(global, didPre, didRB) chain(args.map(function (arg) { return function (cb) { builder(arg, cb) } }), cb) } function build_ (global, didPre, didRB) { return function (folder, cb) { folder = path.resolve(folder) if (build._didBuild[folder]) log.info('build', 'already built', folder) build._didBuild[folder] = true log.info('build', folder) readJson(path.resolve(folder, 'package.json'), function (er, pkg) { if (er) return cb(er) chain([ !didPre && [lifecycle, pkg, 'preinstall', folder], [linkStuff, pkg, folder, global, didRB], [writeBuiltinConf, pkg, folder], didPre !== build._noLC && [lifecycle, pkg, 'install', folder], didPre !== build._noLC && [lifecycle, pkg, 'postinstall', folder] ], cb) }) } } var writeBuiltinConf = build.writeBuiltinConf = function (pkg, folder, cb) { // the builtin config is "sticky". Any time npm installs // itself globally, it puts its builtin config file there var parent = path.dirname(folder) var dir = npm.globalDir if (pkg.name !== 'npm' || !npm.config.get('global') || !npm.config.usingBuiltin || dir !== parent) { return cb() } var data = ini.stringify(npm.config.sources.builtin.data) writeFile(path.resolve(folder, 'npmrc'), data, cb) } var linkStuff = build.linkStuff = function (pkg, folder, global, didRB, cb) { // allow to opt out of linking binaries. if (npm.config.get('bin-links') === false) return cb() // if it's global, and folder is in {prefix}/node_modules, // then bins are in {prefix}/bin // otherwise, then bins are in folder/../.bin var parent = pkg.name && pkg.name[0] === '@' ? path.dirname(path.dirname(folder)) : path.dirname(folder) var gnm = global && npm.globalDir var gtop = parent === gnm log.info('linkStuff', packageId(pkg)) log.silly('linkStuff', packageId(pkg), 'has', parent, 'as its parent node_modules') if (global) log.silly('linkStuff', packageId(pkg), 'is part of a global install') if (gnm) log.silly('linkStuff', packageId(pkg), 'is installed into a global node_modules') if (gtop) log.silly('linkStuff', packageId(pkg), 'is installed into the top-level global node_modules') shouldWarn(pkg, folder, global, function () { asyncMap( [linkBins, linkMans, !didRB && rebuildBundles], function (fn, cb) { if (!fn) return cb() log.verbose(fn.name, packageId(pkg)) fn(pkg, folder, parent, gtop, cb) }, cb ) }) } function shouldWarn (pkg, folder, global, cb) { var parent = path.dirname(folder) var top = parent === npm.dir var cwd = npm.localPrefix readJson(path.resolve(cwd, 'package.json'), function (er, topPkg) { if (er) return cb(er) var linkedPkg = path.basename(cwd) var currentPkg = path.basename(folder) // current searched package is the linked package on first call if (linkedPkg !== currentPkg) { // don't generate a warning if it's listed in dependencies if (Object.keys(topPkg.dependencies || {}) .concat(Object.keys(topPkg.devDependencies || {})) .indexOf(currentPkg) === -1) { if (top && pkg.preferGlobal && !global) { log.warn('prefer global', packageId(pkg) + ' should be installed with -g') } } } cb() }) } function rebuildBundles (pkg, folder, parent, gtop, cb) { if (!npm.config.get('rebuild-bundle')) return cb() var deps = Object.keys(pkg.dependencies || {}) .concat(Object.keys(pkg.devDependencies || {})) var bundles = pkg.bundleDependencies || pkg.bundledDependencies || [] fs.readdir(path.resolve(folder, 'node_modules'), function (er, files) { // error means no bundles if (er) return cb() log.verbose('rebuildBundles', files) // don't asyncMap these, because otherwise build script output // gets interleaved and is impossible to read chain(files.filter(function (file) { // rebuild if: // not a .folder, like .bin or .hooks return !file.match(/^[\._-]/) && // not some old 0.x style bundle file.indexOf('@') === -1 && // either not a dep, or explicitly bundled (deps.indexOf(file) === -1 || bundles.indexOf(file) !== -1) }).map(function (file) { file = path.resolve(folder, 'node_modules', file) return function (cb) { if (build._didBuild[file]) return cb() log.verbose('rebuild bundle', file) // if file is not a package dir, then don't do it. fs.lstat(path.resolve(file, 'package.json'), function (er) { if (er) return cb() build_(false)(file, cb) }) } }), cb) }) } function linkBins (pkg, folder, parent, gtop, cb) { if (!pkg.bin || !gtop && path.basename(parent) !== 'node_modules') { return cb() } var binRoot = gtop ? npm.globalBin : path.resolve(parent, '.bin') log.verbose('linkBins', [pkg.bin, binRoot, gtop]) asyncMap(Object.keys(pkg.bin), function (b, cb) { linkBin( path.resolve(folder, pkg.bin[b]), path.resolve(binRoot, b), gtop && folder, function (er) { if (er) return cb(er) // bins should always be executable. // XXX skip chmod on windows? var src = path.resolve(folder, pkg.bin[b]) fs.chmod(src, npm.modes.exec, function (er) { if (er && er.code === 'ENOENT' && npm.config.get('ignore-scripts')) { return cb() } if (er || !gtop) return cb(er) var dest = path.resolve(binRoot, b) var out = npm.config.get('parseable') ? dest + '::' + src + ':BINFILE' : dest + ' -> ' + src output(out) cb() }) } ) }, cb) } function linkBin (from, to, gently, cb) { if (process.platform !== 'win32') { return linkIfExists(from, to, gently, cb) } else { return cmdShimIfExists(from, to, cb) } } function linkMans (pkg, folder, parent, gtop, cb) { if (!pkg.man || !gtop || process.platform === 'win32') return cb() var manRoot = path.resolve(npm.config.get('prefix'), 'share', 'man') log.verbose('linkMans', 'man files are', pkg.man, 'in', manRoot) // make sure that the mans are unique. // otherwise, if there are dupes, it'll fail with EEXIST var set = pkg.man.reduce(function (acc, man) { acc[path.basename(man)] = man return acc }, {}) pkg.man = pkg.man.filter(function (man) { return set[path.basename(man)] === man }) asyncMap(pkg.man, function (man, cb) { if (typeof man !== 'string') return cb() log.silly('linkMans', 'preparing to link', man) var parseMan = man.match(/(.*\.([0-9]+)(\.gz)?)$/) if (!parseMan) { return cb(new Error( man + ' is not a valid name for a man file. ' + 'Man files must end with a number, ' + 'and optionally a .gz suffix if they are compressed.' )) } var stem = parseMan[1] var sxn = parseMan[2] var bn = path.basename(stem) var manSrc = path.resolve(folder, man) var manDest = path.join(manRoot, 'man' + sxn, bn) linkIfExists(manSrc, manDest, gtop && folder, cb) }, cb) }