Skip to content
This repository has been archived by the owner on Aug 11, 2022. It is now read-only.

Npm exec implementation for #3313 #4058

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 24 additions & 0 deletions lib/exec.js
@@ -0,0 +1,24 @@
module.exports = exec

var path = require("path")
, pkgexec = require('./utils/pkgexec')
, chain = require("slide").chain
, log = require("npmlog")
, readJson = require("read-package-json")

exec.usage = "npm exec <command> [<options>]"

function exec(args, cb) {
if (args.length < 1) return cb(exec.usage);

var command = args[0]
, cmd = args.slice(1)
, wd = process.cwd()

log.verbose("exec ", command)

chain([
[readJson, path.resolve(wd, "package.json")]
, [pkgexec, command, cmd, chain.last, wd]
], cb)
}
1 change: 1 addition & 0 deletions lib/npm.js
Expand Up @@ -161,6 +161,7 @@ var commandCache = {}
, "restart"
, "run-script"
, "completion"
, "exec"
]
, plumbing = [ "build"
, "unbuild"
Expand Down
250 changes: 29 additions & 221 deletions lib/utils/lifecycle.js
@@ -1,26 +1,12 @@

exports = module.exports = lifecycle
exports.cmd = cmd

var log = require("npmlog")
, spawn = require("child_process").spawn
, npm = require("../npm.js")
, pkgexec = require("./pkgexec.js")
, path = require("path")
, fs = require("graceful-fs")
, chain = require("slide").chain
, constants = require("constants")
, Stream = require("stream").Stream
, PATH = "PATH"

// windows calls it's path "Path" usually, but this is not guaranteed.
if (process.platform === "win32") {
PATH = "Path"
Object.keys(process.env).forEach(function (e) {
if (e.match(/^PATH$/i)) {
PATH = e
}
})
}

function lifecycle (pkg, stage, wd, unsafe, failOk, cb) {
if (typeof cb !== "function") cb = failOk, failOk = false
Expand All @@ -45,56 +31,25 @@ function lifecycle (pkg, stage, wd, unsafe, failOk, cb) {
return cb()
}

// set the env variables, then run scripts as a child process.
var env = makeEnv(pkg)
env.npm_lifecycle_event = stage
env.npm_node_execpath = env.NODE = env.NODE || process.execPath
env.npm_execpath = require.main.filename

// "nobody" typically doesn't have permission to write to /tmp
// even if it's never used, sh freaks out.
if (!npm.config.get("unsafe-perm")) env.TMPDIR = wd

lifecycle_(pkg, stage, wd, env, unsafe, failOk, cb)
})
}
log.verbose("unsafe-perm in lifecycle", unsafe)

function checkForLink (pkg, cb) {
var f = path.join(npm.dir, pkg.name)
fs.lstat(f, function (er, s) {
cb(null, !(er || !s.isSymbolicLink()))
lifecycle_(pkg, stage, wd, failOk, cb)
})
}

function lifecycle_ (pkg, stage, wd, env, unsafe, failOk, cb) {
var pathArr = []
, p = wd.split("node_modules")
, acc = path.resolve(p.shift())

// first add the directory containing the `node` executable currently
// running, so that any lifecycle script that invoke "node" will execute
// this same one.
pathArr.unshift(path.dirname(process.execPath))

p.forEach(function (pp) {
pathArr.unshift(path.join(acc, "node_modules", ".bin"))
acc = path.join(acc, "node_modules", pp)
})
pathArr.unshift(path.join(acc, "node_modules", ".bin"))
function lifecycle_ (pkg, stage, wd, failOk, cb) {
var packageLifecycle = pkg.scripts && pkg.scripts.hasOwnProperty(stage)
, env = {}

// we also unshift the bundled node-gyp-bin folder so that
// the bundled one will be used for installing things.
pathArr.unshift(path.join(__dirname, "..", "..", "bin", "node-gyp-bin"))
if (!packageLifecycle) return cb()

if (env[PATH]) pathArr.push(env[PATH])
env[PATH] = pathArr.join(process.platform === "win32" ? ";" : ":")
// define this here so it's available to all scripts.
env.npm_lifecycle_event = stage
env.npm_lifecycle_script = pkg.scripts[stage]

var packageLifecycle = pkg.scripts && pkg.scripts.hasOwnProperty(stage)

if (packageLifecycle) {
// define this here so it's available to all scripts.
env.npm_lifecycle_script = pkg.scripts[stage]
}
// "nobody" typically doesn't have permission to write to /tmp
// even if it's never used, sh freaks out.
if (!npm.config.get("unsafe-perm")) env.TMPDIR = wd

if (failOk) {
cb = (function (cb_) { return function (er) {
Expand All @@ -110,184 +65,37 @@ function lifecycle_ (pkg, stage, wd, env, unsafe, failOk, cb) {
}})(cb)
}

chain
( [ packageLifecycle && [runPackageLifecycle, pkg, env, wd, unsafe]
, [runHookLifecycle, pkg, env, wd, unsafe] ]
, cb )
runPackageLifecycle(pkg, env, wd, cb)
}

function validWd (d, cb) {
fs.stat(d, function (er, st) {
if (er || !st.isDirectory()) {
var p = path.dirname(d)
if (p === d) {
return cb(new Error("Could not find suitable wd"))
}
return validWd(p, cb)
}
return cb(null, d)
})
}

function runPackageLifecycle (pkg, env, wd, unsafe, cb) {
function runPackageLifecycle (pkg, env, wd, cb) {
// run package lifecycle scripts in the package root, or the nearest parent.
var stage = env.npm_lifecycle_event
, user = unsafe ? null : npm.config.get("user")
, group = unsafe ? null : npm.config.get("group")
, cmd = env.npm_lifecycle_script
, sh = "sh"
, shFlag = "-c"

if (process.platform === "win32") {
sh = "cmd"
shFlag = "/c"
}
, cmd = env.npm_lifecycle_script.split(/\s+/)
, sh = cmd.shift()
, args = cmd

log.verbose("unsafe-perm in lifecycle", unsafe)

var note = "\n> " + pkg._id + " " + stage + " " + wd
+ "\n> " + cmd + "\n"

console.log(note)

var conf = { cwd: wd, env: env, customFds: [ 0, 1, 2] }
var proc = spawn(sh, [shFlag, cmd], conf)
proc.on("close", function (er, stdout, stderr) {
pkgexec(sh, args, pkg, wd, env, function(er) {
if (er && !npm.ROLLBACK) {
log.info(pkg._id, "Failed to exec "+stage+" script")
er.message = pkg._id + " "
+ stage + ": `" + env.npm_lifecycle_script+"`\n"
+ er.message
if (er.code !== "EPERM") {
er.code = "ELIFECYCLE"
}
er.pkgid = pkg._id
er.stage = stage
er.script = env.npm_lifecycle_script
er.pkgname = pkg.name
return cb(er)
} else if (er) {
log.error(pkg._id+"."+stage, er)
log.error(pkg._id+"."+stage, "continuing anyway")
return cb()
}
cb(er)
})
}

function runHookLifecycle (pkg, env, wd, unsafe, cb) {
// check for a hook script, run if present.
var stage = env.npm_lifecycle_event
, hook = path.join(npm.dir, ".hooks", stage)
, user = unsafe ? null : npm.config.get("user")
, group = unsafe ? null : npm.config.get("group")
, cmd = hook

fs.stat(hook, function (er) {
if (er) return cb()

var conf = { cwd: wd, env: env, customFds: [ 0, 1, 2] }
var proc = spawn("sh", ["-c", cmd], conf)
proc.on("close", function (er) {
if (er) {
er.message += "\nFailed to exec "+stage+" hook script"
log.info(pkg._id, er)
}
if (npm.ROLLBACK) return cb()
cb(er)
})
})
cb()
});
}

function makeEnv (data, prefix, env) {
prefix = prefix || "npm_package_"
if (!env) {
env = {}
for (var i in process.env) if (!i.match(/^npm_/)) {
env[i] = process.env[i]
}

// npat asks for tap output
if (npm.config.get("npat")) env.TAP = 1

// express and others respect the NODE_ENV value.
if (npm.config.get("production")) env.NODE_ENV = "production"

} else if (!data.hasOwnProperty("_lifecycleEnv")) {
Object.defineProperty(data, "_lifecycleEnv",
{ value : env
, enumerable : false
})
}

for (var i in data) if (i.charAt(0) !== "_") {
var envKey = (prefix+i).replace(/[^a-zA-Z0-9_]/g, '_')
if (i === "readme") {
continue
}
if (data[i] && typeof(data[i]) === "object") {
try {
// quick and dirty detection for cyclical structures
JSON.stringify(data[i])
makeEnv(data[i], envKey+"_", env)
} catch (ex) {
// usually these are package objects.
// just get the path and basic details.
var d = data[i]
makeEnv( { name: d.name, version: d.version, path:d.path }
, envKey+"_", env)
function validWd (d, cb) {
fs.stat(d, function (er, st) {
if (er || !st.isDirectory()) {
var p = path.dirname(d)
if (p === d) {
return cb(new Error("Could not find suitable wd"))
}
} else {
env[envKey] = String(data[i])
env[envKey] = -1 !== env[envKey].indexOf("\n")
? JSON.stringify(env[envKey])
: env[envKey]
}

}

if (prefix !== "npm_package_") return env

prefix = "npm_config_"
var pkgConfig = {}
, keys = npm.config.keys
, pkgVerConfig = {}
, namePref = data.name + ":"
, verPref = data.name + "@" + data.version + ":"

keys.forEach(function (i) {
if (i.charAt(0) === "_" && i.indexOf("_"+namePref) !== 0) {
return
}
var value = npm.config.get(i)
if (value instanceof Stream || Array.isArray(value)) return
if (!value) value = ""
else if (typeof value !== "string") value = JSON.stringify(value)

value = -1 !== value.indexOf("\n")
? JSON.stringify(value)
: value
i = i.replace(/^_+/, "")
if (i.indexOf(namePref) === 0) {
var k = i.substr(namePref.length).replace(/[^a-zA-Z0-9_]/g, "_")
pkgConfig[ k ] = value
} else if (i.indexOf(verPref) === 0) {
var k = i.substr(verPref.length).replace(/[^a-zA-Z0-9_]/g, "_")
pkgVerConfig[ k ] = value
}
var envKey = (prefix+i).replace(/[^a-zA-Z0-9_]/g, "_")
env[envKey] = value
})

prefix = "npm_package_config_"
;[pkgConfig, pkgVerConfig].forEach(function (conf) {
for (var i in conf) {
var envKey = (prefix+i)
env[envKey] = conf[i]
return validWd(p, cb)
}
return cb(null, d)
})

return env
}

function cmd (stage) {
Expand Down