deps/npm/lib/utils/lifecycle.js

Summary

Maintainability
D
2 days
Test Coverage
exports = module.exports = lifecycle
exports.cmd = cmd
exports.makeEnv = makeEnv

var log = require("npmlog")
var spawn = require("./spawn")
var npm = require("../npm.js")
var path = require("path")
var fs = require("graceful-fs")
var chain = require("slide").chain
var Stream = require("stream").Stream
var PATH = "PATH"
var uidNumber = require("uid-number")
var umask = require("./umask")

// 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
  if (typeof cb !== "function") cb = unsafe, unsafe = false
  if (typeof cb !== "function") cb = wd, wd = null

  while (pkg && pkg._data) pkg = pkg._data
  if (!pkg) return cb(new Error("Invalid package data"))

  log.info(stage, pkg._id)
  if (!pkg.scripts || npm.config.get('ignore-scripts')) pkg.scripts = {}

  validWd(wd || path.resolve(npm.dir, pkg.name), function (er, wd) {
    if (er) return cb(er)

    unsafe = unsafe || npm.config.get("unsafe-perm")

    if ((wd.indexOf(npm.dir) !== 0 || path.basename(wd) !== pkg.name)
        && !unsafe && pkg.scripts[stage]) {
      log.warn( "cannot run in wd", "%s %s (wd=%s)"
              , pkg._id, pkg.scripts[stage], wd)
      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)
  })
}

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

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

  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"))

  // 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 (env[PATH]) pathArr.push(env[PATH])
  env[PATH] = pathArr.join(process.platform === "win32" ? ";" : ":")

  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]
  }

  function done (er) {
    if (er) {
      if (npm.config.get("force")) {
        log.info("forced, continuing", er)
        er = null
      } else if (failOk) {
        log.warn("continuing anyway", er.message)
        er = null
      }
    }
    cb(er)
  }

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

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) {
  // run package lifecycle scripts in the package root, or the nearest parent.
  var stage = env.npm_lifecycle_event
    , cmd = env.npm_lifecycle_script

  var note = "\n> " + pkg._id + " " + stage + " " + wd
           + "\n> " + cmd + "\n"
  runCmd(note, cmd, pkg, env, stage, wd, unsafe, cb)
}


var running = false
var queue = []
function dequeue() {
  running = false
  if (queue.length) {
    var r = queue.shift()
    runCmd.apply(null, r)
  }
}

function runCmd (note, cmd, pkg, env, stage, wd, unsafe, cb) {
  if (running) {
    queue.push([note, cmd, pkg, env, stage, wd, unsafe, cb])
    return
  }

  running = true
  log.pause()
  var user = unsafe ? null : npm.config.get("user")
    , group = unsafe ? null : npm.config.get("group")

  if (log.level !== 'silent') {
    if (npm.spinner.int) {
      npm.config.get("logstream").write("\r \r")
    }
    console.log(note)
  }
  log.verbose("unsafe-perm in lifecycle", unsafe)

  if (process.platform === "win32") {
    unsafe = true
  }

  if (unsafe) {
    runCmd_(cmd, pkg, env, wd, stage, unsafe, 0, 0, cb)
  } else {
    uidNumber(user, group, function (er, uid, gid) {
      runCmd_(cmd, pkg, env, wd, stage, unsafe, uid, gid, cb)
    })
  }
}

function runCmd_ (cmd, pkg, env, wd, stage, unsafe, uid, gid, cb_) {

  function cb (er) {
    cb_.apply(null, arguments)
    log.resume()
    process.nextTick(dequeue)
  }

  var conf = { cwd: wd
             , env: env
             , stdio: [ 0, 1, 2 ]
             }

  if (!unsafe) {
    conf.uid = uid ^ 0
    conf.gid = gid ^ 0
  }

  var sh = "sh"
  var shFlag = "-c"

  if (process.platform === "win32") {
    sh = process.env.comspec || "cmd"
    shFlag = "/c"
    conf.windowsVerbatimArguments = true
  }

  var proc = spawn(sh, [shFlag, cmd], conf)
  proc.on("error", procError)
  proc.on("close", function (code, signal) {
    if (signal) {
      process.kill(process.pid, signal);
    } else if (code) {
      var er = new Error("Exit status " + code)
    }
    procError(er)
  })

  function procError (er) {
    if (er && !npm.ROLLBACK) {
      log.info(pkg._id, "Failed to exec "+stage+" script")
      er.message = pkg._id + " "
                 + stage + ": `" + cmd +"`\n"
                 + er.message
      if (er.code !== "EPERM") {
        er.code = "ELIFECYCLE"
      }
      er.pkgid = pkg._id
      er.stage = stage
      er.script = cmd
      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 note = "\n> " + pkg._id + " " + stage + " " + wd
             + "\n> " + cmd
    runCmd(note, hook, pkg, env, stage, wd, unsafe, 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)
      }
    } 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 (i.match(/umask/)) value = umask.toString(value)
    if (!value) value = ""
    else if (typeof value === "number") 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 env
}

function cmd (stage) {
  function CMD (args, cb) {
    npm.commands["run-script"]([stage].concat(args), cb)
  }
  CMD.usage = "npm "+stage+" [-- <args>]"
  var installedShallow = require("./completion/installed-shallow.js")
  CMD.completion = function (opts, cb) {
    installedShallow(opts, function (d) {
      return d.scripts && d.scripts[stage]
    }, cb)
  }
  return CMD
}