// commands for packing and unpacking tarballs // this file is used by lib/cache.js var fs = require('graceful-fs') var path = require('path') var writeFileAtomic = require('write-file-atomic') var writeStreamAtomic = require('fs-write-stream-atomic') var log = require('npmlog') var uidNumber = require('uid-number') var readJson = require('read-package-json') var tar = require('tar') var zlib = require('zlib') var fstream = require('fstream') var Packer = require('fstream-npm') var iferr = require('iferr') var inherits = require('inherits') var npm = require('../npm.js') var rm = require('./gently-rm.js') var myUid = process.getuid && process.getuid() var myGid = process.getgid && process.getgid() var readPackageTree = require('read-package-tree') var union = require('lodash.union') if (process.env.SUDO_UID && myUid === 0) { if (!isNaN(process.env.SUDO_UID)) myUid = +process.env.SUDO_UID if (!isNaN(process.env.SUDO_GID)) myGid = +process.env.SUDO_GID } exports.pack = pack exports.unpack = unpack function pack (tarball, folder, pkg, cb) { log.verbose('tar pack', [tarball, folder]) log.verbose('tarball', tarball) log.verbose('folder', folder) var recalculateMetadata = require('../install/deps.js').recalculateMetadata readPackageTree(folder, iferr(cb, function (tree) { recalculateMetadata(tree, log.newGroup('pack:' + pkg), iferr(cb, function () { pack_(tarball, folder, tree, pkg, cb) })) })) } function BundledPacker (props) { Packer.call(this, props) this.tree = props.tree var flattenTree = require('../install/flatten-tree.js') this.flatTree = props.flatTree || flattenTree(props.tree) } inherits(BundledPacker, Packer) BundledPacker.prototype.getChildProps = function (stat) { var props = Packer.prototype.getChildProps.call(this, stat) props.tree = this.tree props.flatTree = this.flatTree return props } BundledPacker.prototype.applyIgnores = function (entry, partial, entryObj) { // package.json files can never be ignored. if (entry === 'package.json') return true // readme files should never be ignored. if (entry.match(/^readme(\.[^\.]*)$/i)) return true // license files should never be ignored. if (entry.match(/^(license|licence)(\.[^\.]*)?$/i)) return true // changelogs should never be ignored. if (entry.match(/^(changes|changelog|history)(\.[^\.]*)?$/i)) return true // special rules. see below. if (entry === 'node_modules' && this.packageRoot) return true // some files are *never* allowed under any circumstances if (entry === '.git' || entry === '.lock-wscript' || entry.match(/^\.wafpickle-[0-9]+$/) || entry === 'CVS' || entry === '.svn' || entry === '.hg' || entry.match(/^\..*\.swp$/) || entry === '.DS_Store' || entry.match(/^\._/) || entry === 'npm-debug.log' ) { return false } // in a node_modules folder, we only include bundled dependencies // also, prevent packages in node_modules from being affected // by rules set in the containing package, so that // bundles don't get busted. // Also, once in a bundle, everything is installed as-is // To prevent infinite cycles in the case of cyclic deps that are // linked with npm link, even in a bundle, deps are only bundled // if they're not already present at a higher level. if (this.bundleMagic) { // bubbling up. stop here and allow anything the bundled pkg allows if (entry.indexOf('/') !== -1) return true // never include the .bin. It's typically full of platform-specific // stuff like symlinks and .cmd files anyway. if (entry === '.bin') return false // the package root. var p = this.parent // the package before this one. var pp = p && p.parent // if this entry has already been bundled, and is a symlink, // and it is the *same* symlink as this one, then exclude it. if (pp && pp.bundleLinks && this.bundleLinks && pp.bundleLinks[entry] && pp.bundleLinks[entry] === this.bundleLinks[entry]) { return false } // since it's *not* a symbolic link, if we're *already* in a bundle, // then we should include everything. if (pp && pp.package && pp.basename === 'node_modules') { return true } // only include it at this point if it's a bundleDependency return this.isBundled(entry) } // if (this.bundled) return true return Packer.prototype.applyIgnores.call(this, entry, partial, entryObj) } function nameMatch (name) { return function (other) { return name === other.package.name } } BundledPacker.prototype.isBundled = function (name) { var bd = this.package && this.package.bundleDependencies if (!bd) return false if (!Array.isArray(bd)) { throw new Error(this.package.name + '\'s `bundledDependencies` should ' + 'be an array') } if (bd.indexOf(name) !== -1) return true var pkg = this.tree.children.filter(nameMatch(name))[0] if (!pkg) return false var requiredBy = union([], pkg.package._requiredBy) var seen = {} while (requiredBy.length) { var req = requiredBy.shift() if (seen[req]) continue seen[req] = true var reqPkg = this.flatTree[req] if (!reqPkg) continue if (reqPkg.parent === this.tree && bd.indexOf(reqPkg.package.name) !== -1) { return true } requiredBy = union(requiredBy, reqPkg.package._requiredBy) } return false } function pack_ (tarball, folder, tree, pkg, cb) { new BundledPacker({ path: folder, tree: tree, type: 'Directory', isDirectory: true }) .on('error', function (er) { if (er) log.error('tar pack', 'Error reading ' + folder) return cb(er) }) // By default, npm includes some proprietary attributes in the // package tarball. This is sane, and allowed by the spec. // However, npm *itself* excludes these from its own package, // so that it can be more easily bootstrapped using old and // non-compliant tar implementations. .pipe(tar.Pack({ noProprietary: !npm.config.get('proprietary-attribs') })) .on('error', function (er) { if (er) log.error('tar.pack', 'tar creation error', tarball) cb(er) }) .pipe(zlib.Gzip()) .on('error', function (er) { if (er) log.error('tar.pack', 'gzip error ' + tarball) cb(er) }) .pipe(writeStreamAtomic(tarball)) .on('error', function (er) { if (er) log.error('tar.pack', 'Could not write ' + tarball) cb(er) }) .on('close', cb) } function unpack (tarball, unpackTarget, dMode, fMode, uid, gid, cb) { log.verbose('tar', 'unpack', tarball) log.verbose('tar', 'unpacking to', unpackTarget) if (typeof cb !== 'function') { cb = gid gid = null } if (typeof cb !== 'function') { cb = uid uid = null } if (typeof cb !== 'function') { cb = fMode fMode = npm.modes.file } if (typeof cb !== 'function') { cb = dMode dMode = npm.modes.exec } uidNumber(uid, gid, function (er, uid, gid) { if (er) return cb(er) unpack_(tarball, unpackTarget, dMode, fMode, uid, gid, cb) }) } function unpack_ (tarball, unpackTarget, dMode, fMode, uid, gid, cb) { rm(unpackTarget, function (er) { if (er) return cb(er) // gzip {tarball} --decompress --stdout \ // | tar -mvxpf - --strip-components=1 -C {unpackTarget} gunzTarPerm(tarball, unpackTarget, dMode, fMode, uid, gid, function (er, folder) { if (er) return cb(er) readJson(path.resolve(folder, 'package.json'), cb) }) }) } function gunzTarPerm (tarball, target, dMode, fMode, uid, gid, cb_) { if (!dMode) dMode = npm.modes.exec if (!fMode) fMode = npm.modes.file log.silly('gunzTarPerm', 'modes', [dMode.toString(8), fMode.toString(8)]) var cbCalled = false function cb (er) { if (cbCalled) return cbCalled = true cb_(er, target) } var fst = fs.createReadStream(tarball) fst.on('open', function (fd) { fs.fstat(fd, function (er, st) { if (er) return fst.emit('error', er) if (st.size === 0) { er = new Error('0-byte tarball\n' + 'Please run `npm cache clean`') fst.emit('error', er) } }) }) // figure out who we're supposed to be, if we're not pretending // to be a specific user. if (npm.config.get('unsafe-perm') && process.platform !== 'win32') { uid = myUid gid = myGid } function extractEntry (entry) { log.silly('gunzTarPerm', 'extractEntry', entry.path) // never create things that are user-unreadable, // or dirs that are user-un-listable. Only leads to headaches. var originalMode = entry.mode = entry.mode || entry.props.mode entry.mode = entry.mode | (entry.type === 'Directory' ? dMode : fMode) entry.mode = entry.mode & (~npm.modes.umask) entry.props.mode = entry.mode if (originalMode !== entry.mode) { log.silly('gunzTarPerm', 'modified mode', [entry.path, originalMode, entry.mode]) } // if there's a specific owner uid/gid that we want, then set that if (process.platform !== 'win32' && typeof uid === 'number' && typeof gid === 'number') { entry.props.uid = entry.uid = uid entry.props.gid = entry.gid = gid } } var extractOpts = { type: 'Directory', path: target, strip: 1 } if (process.platform !== 'win32' && typeof uid === 'number' && typeof gid === 'number') { extractOpts.uid = uid extractOpts.gid = gid } var sawIgnores = {} extractOpts.filter = function () { // symbolic links are not allowed in packages. if (this.type.match(/^.*Link$/)) { log.warn('excluding symbolic link', this.path.substr(target.length + 1) + ' -> ' + this.linkpath) return false } // Note: This mirrors logic in the fs read operations that are // employed during tarball creation, in the fstream-npm module. // It is duplicated here to handle tarballs that are created // using other means, such as system tar or git archive. if (this.type === 'File') { var base = path.basename(this.path) if (base === '.npmignore') { sawIgnores[ this.path ] = true } else if (base === '.gitignore') { var npmignore = this.path.replace(/\.gitignore$/, '.npmignore') if (sawIgnores[npmignore]) { // Skip this one, already seen. return false } else { // Rename, may be clobbered later. this.path = npmignore this._path = npmignore } } } return true } fst .on('error', function (er) { if (er) log.error('tar.unpack', 'error reading ' + tarball) cb(er) }) .on('data', function OD (c) { // detect what it is. // Then, depending on that, we'll figure out whether it's // a single-file module, gzipped tarball, or naked tarball. // gzipped files all start with 1f8b08 if (c[0] === 0x1F && c[1] === 0x8B && c[2] === 0x08) { fst .pipe(zlib.Unzip()) .on('error', function (er) { if (er) log.error('tar.unpack', 'unzip error ' + tarball) cb(er) }) .pipe(tar.Extract(extractOpts)) .on('entry', extractEntry) .on('error', function (er) { if (er) log.error('tar.unpack', 'untar error ' + tarball) cb(er) }) .on('close', cb) } else if (hasTarHeader(c)) { // naked tar fst .pipe(tar.Extract(extractOpts)) .on('entry', extractEntry) .on('error', function (er) { if (er) log.error('tar.unpack', 'untar error ' + tarball) cb(er) }) .on('close', cb) } else { // naked js file var jsOpts = { path: path.resolve(target, 'index.js') } if (process.platform !== 'win32' && typeof uid === 'number' && typeof gid === 'number') { jsOpts.uid = uid jsOpts.gid = gid } fst .pipe(fstream.Writer(jsOpts)) .on('error', function (er) { if (er) log.error('tar.unpack', 'copy error ' + tarball) cb(er) }) .on('close', function () { var j = path.resolve(target, 'package.json') readJson(j, function (er, d) { if (er) { log.error('not a package', tarball) return cb(er) } writeFileAtomic(j, JSON.stringify(d) + '\n', cb) }) }) } // now un-hook, and re-emit the chunk fst.removeListener('data', OD) fst.emit('data', c) }) } function hasTarHeader (c) { return c[257] === 0x75 && // tar archives have 7573746172 at position c[258] === 0x73 && // 257 and 003030 or 202000 at position 262 c[259] === 0x74 && c[260] === 0x61 && c[261] === 0x72 && ((c[262] === 0x00 && c[263] === 0x30 && c[264] === 0x30) || (c[262] === 0x20 && c[263] === 0x20 && c[264] === 0x00)) }