UNPKG

gypkg

Version:

GYP based package manager

669 lines (543 loc) 18 kB
'use strict'; const gypkg = require('../gypkg'); const path = require('path'); const fs = require('fs'); const os = require('os'); const url = require('url'); const util = require('util'); const net = require('net'); const crypto = require('crypto'); const spawn = require('child_process').spawn; const async = require('async'); const semver = require('semver'); const colors = require('colors/safe'); const npmPath = require('npm-path'); const gitSecureTag = require('git-secure-tag'); const ScopedGPG = require('scoped-gpg'); const ProgressBar = require('progress'); const COMMON_GYPI = path.join(__dirname, '..', '..', 'common.gypi'); const GIT = process.env.GIT_SSH_COMMAND || process.env.GIT_SSH || process.env.GIT_EXEC_PATH || 'git'; function Server(options) { net.Server.call(this, this.onClient); this.options = options; this.host = process.env.GYPKG_CMD_HOST || '127.0.0.1'; this.port = process.env.GYPKG_CMD_PORT | 0; this.env = null; this.depsDir = null; // Filter out dependencies duplicates this.dups = new Map(); // Freeze data this.freezeMap = new Map(); this.progress = new ProgressBar('[:bar] :elapseds ', { total: 100 }); this.done = 0; this.total = 0; this.readline = { lock: false, queue: [] }; } util.inherits(Server, net.Server); module.exports = Server; Server.prototype.update = function update(obj) { this.done += obj.done; this.total += obj.total; // No progress bar in verbose mode if (this.options.verbose) return; // Do not interfer with readline if (this.readline.lock) return; this.progress.curr = this.done / this.total * 100; this.progress.render(); }; Server.prototype.onClient = function onClient(c) { const cmd = new gypkg.CommandSocket(c); cmd.on('log', text => this.emit('log', text)); cmd.on('deps', (list) => { this.deps(list, (err, list) => { if (err) return cmd.error('deps-result', err); cmd.send('deps-result', list); }); }); cmd.on('type', (dir) => { this.type(dir, (err, type) => { if (err) return cmd.error('type-result', err); cmd.send('type-result', type); }); }); cmd.on('scoped-gpg', (data) => { this.scopedGPG(data.argv, data.stdin, data.scope, (err) => { if (err) return cmd.error('scoped-gpg-result', err); cmd.send('scoped-gpg-result', {}); }); }); }; Server.prototype.freeze = function freeze(gyp) { const root = path.dirname(path.resolve(gyp)); const deps = path.join(root, 'gypkg_deps'); const file = path.join(root, '.gypkg-freeze'); // XXX(indutny): why not use {} from the start? const res = {}; this.freezeMap.forEach((value, key) => { res[key] = { type: value.type, dir: path.relative(deps, value.dir), source: value.source, gyp: value.gyp, target: value.target, hash: (value.hash || '').trim() }; }); fs.writeFileSync(file, JSON.stringify(res)); }; Server.prototype.generate = function generate(gyp, extra, callback) { async.parallel({ s: (callback) => this.listen(this.port, this.host, () => callback(null)), path: (callback) => npmPath.get(callback) }, (err, result) => { if (err) return callback(err); this.port = this.address().port; this.host = this.address().address; const env = {}; util._extend(env, process.env); env.GYPKG_CMD_HOST = this.host; env.GYPKG_CMD_PORT = this.port; // Add `node_modules/.bin` to PATH env[npmPath.PATH] = result.path + npmPath.SEPARATOR + path.join(__dirname, '..', '..', 'node_modules', '.bin'); this.env = env; this.log(`(Listening on ${this.host}:${this.port})`); this._generate(gyp, extra, done); }); const done = (err) => { if (err) return callback(err); if (this.options.freeze) this.freeze(gyp, callback); else callback(null); }; return this; }; Server.prototype._generate = function _generate(gyp, extra, callback) { const GYP_FILE = path.resolve(gyp); const ROOT_DIR = path.dirname(GYP_FILE); this.depsDir = path.join(ROOT_DIR, 'gypkg_deps'); const OPTIONS_GYPI = path.resolve(ROOT_DIR, 'options.gypi'); let args = [ path.join(__dirname, 'gyp-wrap.js'), GYP_FILE, `-I${COMMON_GYPI}`, `--depth=${path.resolve('.')}` ]; if (fs.existsSync(OPTIONS_GYPI)) args.push(`-I${OPTIONS_GYPI}`); if (!extra.some(arg => /^-Dhost_arch=/.test(arg))) args.push(`-Dhost_arch=${os.arch()}`); if (!extra.some(arg => /^-Dtarget_arch=/.test(arg))) args.push(`-Dtarget_arch=${os.arch()}`); // Compatibility with existing GYP files if (!extra.some(arg => /^-Dlibrary=/.test(arg))) args.push('-Dlibrary=static_library'); args = args.concat(extra); if (this.options.config) args.push(`--build=${this.options.config}`); this.log('Running GYP with the following arguments:'); this.log(`"${args.slice(1).join(' ')}"`); const proc = spawn(process.execPath, args, { env: this.env, stdio: 'inherit' }); proc.on('exit', (code) => { if (code !== 0) return callback(new Error('GYP failure')); // For progress bar if (!this.options.verbose) process.stdout.write('\n'); this.log(colors.green('...done')); this.close(() => callback(null)); }); }; Server.prototype.deps = function deps(list, callback) { async.map(list, (dep, callback) => { const match = dep.match( /^([^\s]*)\s*(?:\[([^\]]*)\])?\s*(?::|=>)\s*([^:]+):([^:]+)$/); if (!match) return callback(new Error('Dependency format is: "url:file.gyp:target"')); this.installDep(match[1], match[2], match[3], match[4], (err, result) => { if (err) return callback(err); if (this.options.freeze) this.freezeMap.set(dep, result); callback(null, result.dep); }); }, callback); }; Server.prototype.log = function log(line) { if (!this.options.verbose) return; // Do not interfer with readline if (this.readline.lock) { this.readline.queue.push(() => this.log(line)); return; } this.emit('log', line, this.total === 0 ? 0 : this.done / this.total); }; Server.prototype.parseFlags = function parseFlags(str) { const res = {}; const parts = str.split(/\s*,\s*/g); for (let i = 0; i < parts.length; i++) { if (parts[i] === '') continue; const match = parts[i].match(/^([^\s=]*)(?:\s*=\s*(.*))?$/); res[match[1]] = match[2] === undefined ? true : match[2]; } return res; }; Server.prototype.installDep = function installDep(uri, flags, gyp, target, callback) { flags = this.parseFlags(flags || ''); let branch = uri.match(/#([^#]+)$/); let version = uri.match(/@([^@:]+)$/); if (branch !== null) { branch = branch[1]; version = null; uri = uri.replace(/#[^#]+$/, ''); } else if (version !== null) { branch = null; version = version[1]; uri = uri.replace(/@([^@:]+)$/, ''); } uri = uri.replace(/^git@([^:]*):(.*)(?:.git)?$/, 'git+ssh://$1/$2'); const parsed = url.parse(uri); // Local file if (!parsed.protocol) { if (branch) return callback(new Error('Can\'t use a branch of a local dependency')); this.log(`local install: ${parsed.path}`); return callback(null, { // Main result dep: path.join(parsed.path, gyp) + ':' + target, // For freeze type: 'local', source: parsed.path, dir: '', gyp: gyp, target: target }); } let id = parsed.path.slice(1); if (branch) id += '@' + branch; else if (version) id += '@semver-' + this.semVerHash(version); else id += '@latest'; if (parsed.host === 'github.com') { // Use team as a gpg scope, if no explicit scope is provided if (flags.gpg === true) flags.gpg = parsed.path.split('/')[1]; } else { // Prefix with full hostname id = path.join(parsed.host, id); } const INSTALL_DIR = path.join(this.depsDir, id); this.update({ total: 1, done: 0 }); // Already installing the dependency, avoid git lock problems if (this.dups.has(INSTALL_DIR)) { const entry = this.dups.get(INSTALL_DIR); if (entry.err !== null || entry.result !== null) { this.update({ total: 0, done: 1 }); process.nextTick(() => callback(entry.err, entry.result)); return; } entry.queue.push((err, result) => { this.update({ total: 0, done: 1 }); callback(err, result); }); return; } const dupsEntry = { err: null, result: null, queue: [] }; this.dups.set(INSTALL_DIR, dupsEntry); const done = (err, dir) => { this.update({ total: 0, done: 1 }); callback(err, dir); dupsEntry.err = err; dupsEntry.result = dir; for (let i = 0; i < dupsEntry.queue.length; i++) dupsEntry.queue[i](err, dir); }; this.log(`${id} => remote install: ${uri}`); // Extra tick for semver if (version) this.update({ total: 1, done: 0 }); async.waterfall([ (callback) => { fs.exists(INSTALL_DIR, (exists) => callback(null, exists)); }, (exists, callback) => { if (exists) this.updateDep(id, INSTALL_DIR, uri, branch, callback); else this.cloneDep(id, INSTALL_DIR, uri, branch, callback); }, (callback) => { if (!version) return callback(null, INSTALL_DIR, null); this.checkoutSemVer(id, INSTALL_DIR, uri, version, callback); }, (depDir, tag, callback) => { this.getSubmodules(id, depDir, err => callback(err, depDir, tag)); }, (depDir, tag, callback) => { this.setGitConfig(id, depDir, err => callback(err, depDir, tag)); }, (depDir, tag, callback) => { if (!version || !flags.gpg) return callback(null, depDir); this.verifyGPG(id, depDir, tag, flags, err => callback(err, depDir)); }, (depDir, callback) => { if (!this.options.freeze) return callback(null, depDir, null); this.getRepoHash(id, depDir, (err, hash) => callback(err, depDir, hash)); } ], (err, depDir, hash) => { if (err) return done(err); done(null, { // Main result dep: path.join(depDir, gyp) + ':' + target, // For freeze dir: depDir, type: 'remote', source: uri, gyp: gyp, target: target, // git specific hash: hash }); }); }; Server.prototype.semVerHash = function semVerHash(version) { return crypto.createHash('sha256').update(version).digest('hex') .slice(0, 7); }; function captureStream(stream) { let chunks = ''; let ended = false; stream.on('data', chunk => chunks += chunk); stream.on('end', () => ended = true); return (callback) => { if (ended) return callback(chunks); stream.once('end', () => callback(chunks)); }; } Server.prototype.git = function git(args, options, callback) { const git = spawn(GIT, args, util._extend({ stdio: [ 'inherit', 'pipe', 'pipe' ], env: this.env }, options.spawn)); const stderr = captureStream(git.stderr); const stdout = captureStream(git.stdout); git.on('exit', (code) => { if (code !== 0) { stderr((stderr) => { let msg = `git ${args.join(' ')} failed`; if (options.spawn && options.spawn.cwd) msg += `\ncwd: ${options.spawn.cwd}`; msg += `\n${stderr}`; return callback(new Error(msg)); }); return; } if (options.stdout) { stdout((stdout) => callback(null, stdout)); return; } return callback(null); }); }; Server.prototype.updateDep = function updateDep(id, target, uri, branch, callback) { const options = { spawn: { cwd: target } }; this.log(`${id} => git fetch: ${uri}`); this.git([ 'fetch', 'origin' ], options, (err) => { if (err) return callback(err); if (!branch) return callback(null); this.git([ 'reset', '--hard', `origin/${branch}` ], options, callback); }); }; Server.prototype.cloneDep = function cloneDep(id, target, uri, branch, callback) { const args = [ 'clone' ]; if (branch) args.push('--depth', '1', '--branch', branch); args.push(uri, target); this.log(`${id} => git clone: ${uri}`); this.git(args, {}, callback); }; Server.prototype.getRepoHash = function getRepoHash(id, target, callback) { const args = [ 'rev-parse', 'HEAD' ]; this.log(`${id} => git rev-parse HEAD`); const options = { spawn: { cwd: target }, stdout: true }; this.git(args, options, callback); }; Server.prototype.checkoutSemVer = function checkoutSemVer(id, target, uri, version, callback) { const options = { spawn: { cwd: target }, stdout: true }; this.log(`${id} => checking out semver: ${version}`); async.waterfall([ (callback) => { this.git([ 'tag' ], options, callback); }, (tags, callback) => { tags = tags.split('\n').filter((tag) => { return /^v/.test(tag); }).filter((tag) => { try { return semver.satisfies(tag, version); } catch (e) { return false; } }).sort((a, b) => { return semver.gt(a, b) ? -1 : semver.lt(a, b) ? 1 : 0; }); if (tags.length === 0) { return callback( new Error(`No matching version found, ${uri}:${version}`)); } callback(null, tags[0]); }, (tag, callback) => { const options = { spawn: { cwd: target } }; this.log(`${id} => semver match: ` + colors.bold(tag)); this.git([ 'reset', '--hard', tag ], options, (err) => { callback(err, tag); }); }, (tag, callback) => { const src = path.basename(target); const dst = target.replace(/@semver-[0-9a-f]+$/, '') + '@' + tag; fs.exists(dst, (exists) => callback(null, src, dst, exists, tag)); }, (src, dst, exists, tag, callback) => { this.processSemVerLink(target, src, dst, exists, (err, dst) => { callback(err, dst, tag); }); } ], (err, dst, tag) => { this.update({ total: 0, done: 1 }); callback(err, dst, tag); }); }; Server.prototype.getSubmodules = function getSubmodules(id, target, callback) { const options = { spawn: { cwd: target }, stdout: true }; this.log(`${id} => loading possible submodules`); this.git([ 'submodule', 'update', '--init', '--recursive' ], options, callback); }; Server.prototype.setGitConfig = function setGitConfig(id, target, callback) { const options = { spawn: { cwd: target }, stdout: true }; this.log(`${id} => setting git config`); async.series([ (callback) => { this.git([ 'config', 'gpg.program', 'gypkg-scoped-gpg' ], options, callback); } ], callback); }; Server.prototype.verifyGPG = function verifyGPG(id, target, tag, flags, callback) { const gst = new gitSecureTag.API(target); this.log(`${id} => verifying GPG signature: ${tag}`); const env = util._extend({ GYPKG_GPG_SCOPE: flags.gpg === true ? 'default' : flags.gpg }, this.env); gst.verify(tag, { env: env, insecure: this.options.insecure }, (err) => { if (err) { this.log(`${id} => ` + colors.red('GPG signature not ok')); return callback(err); } this.log(`${id} => ` + colors.green('GPG signature ok')); callback(null); }); }; Server.prototype.processSemVerLink = function processSemVerLink(target, src, dst, exists, callback) { if (process.platform !== 'win32') { if (exists) return callback(null, dst); fs.symlink(src, dst, 'dir', (err) => { if (!err || err.code === 'EEXIST') return callback(null, dst); callback(err); }); return; } // Things are "slightly" more complicated on Windows // 1. Symlinks require special permission // 2. `fs.link` does not work with directories // // Thus, we need to create a file at `dst` and put `src` in there. If `dst` // exists - we need to read its contents and return them in callback. // Race conditions are not possible with `gyp.js`, so we are good here. if (!exists) { const out = path.join(path.dirname(dst), src); fs.writeFile(dst, src, (err) => callback(err, out)); return; } fs.readFile(dst, (err, link) => { if (err) return callback(err); const out = path.join(path.dirname(dst), link.toString()); callback(null, out); }); }; Server.prototype.type = function type(dir, callback) { // TODO(indutny): This needs some serious consideration return callback(null, 'static_library'); }; Server.prototype.scopedGPG = function scopedGPG(argv, msg, scope, callback) { if (this.readline.lock) { this.readline.queue.push(() => this.scopedGPG(argv, msg, scope, callback)); return; } // `ScopedGPG` may ask for user input, do not let it do this concurrently this.readline.lock = true; const sgpg = new ScopedGPG({ keyring: path.join(this.depsDir, '.gpg-scope-' + scope) }); let signature = null; for (let i = 0; i < argv.length - 1; i++) { if (argv[i] === '--verify') { signature = argv[i + 1]; break; } } if (signature === null) return callback(new Error('No --verify found in gpg args')); async.waterfall([ (callback) => fs.readFile(signature, callback), (signature, callback) => { sgpg.verify(msg, signature, callback); } ], (err, result) => { this.readline.lock = false; // Re-render progressbar this.update({ total: 0, done: 0 }); while (this.readline.queue.length !== 0 && !this.readline.lock) this.readline.queue.shift()(); callback(err, result); }); };