/* eslint-disable require-atomic-updates */ import { exists, mkdirp, readFile, remove, stat } from 'fs-extra'; import { log, wasReported } from './log.js'; import { need, system } from 'pkg-fetch'; import assert from 'assert'; import help from './help'; import { isPackageJson } from '../prelude/common.js'; import minimist from 'minimist'; import packer from './packer.js'; import path from 'path'; import { plusx } from './chmod.js'; import producer from './producer.js'; import refine from './refiner.js'; import { shutdown } from './fabricator.js'; import { version } from '../package.json'; import walk from './walker.js'; function isConfiguration (file) { return isPackageJson(file) || file.endsWith('.config.json'); } // http://www.openwall.com/lists/musl/2012/12/08/4 const { hostArch, hostPlatform, isValidNodeRange, knownArchs, knownPlatforms, toFancyArch, toFancyPlatform } = system; const hostNodeRange = 'node' + process.version.match(/^v(\d+)/)[1]; function parseTargets (items) { // [ 'node6-macos-x64', 'node6-linux-x64' ] const targets = []; for (const item of items) { const target = { nodeRange: hostNodeRange, platform: hostPlatform, arch: hostArch }; if (item !== 'host') { for (const token of item.split('-')) { if (!token) continue; if (isValidNodeRange(token)) { target.nodeRange = token; continue; } const p = toFancyPlatform(token); if (knownPlatforms.indexOf(p) >= 0) { target.platform = p; continue; } const a = toFancyArch(token); if (knownArchs.indexOf(a) >= 0) { target.arch = a; continue; } throw wasReported(`Unknown token '${token}' in '${item}'`); } } targets.push(target); } return targets; } function stringifyTarget (target) { const { nodeRange, platform, arch } = target; return `${nodeRange}-${platform}-${arch}`; } function differentParts (targets) { const nodeRanges = {}; const platforms = {}; const archs = {}; for (const target of targets) { nodeRanges[target.nodeRange] = true; platforms[target.platform] = true; archs[target.arch] = true; } const result = {}; if (Object.keys(nodeRanges).length > 1) { result.nodeRange = true; } if (Object.keys(platforms).length > 1) { result.platform = true; } if (Object.keys(archs).length > 1) { result.arch = true; } return result; } function stringifyTargetForOutput (output, target, different) { const a = [ output ]; if (different.nodeRange) a.push(target.nodeRange); if (different.platform) a.push(target.platform); if (different.arch) a.push(target.arch); return a.join('-'); } function fabricatorForTarget (target) { const { nodeRange, arch } = target; return { nodeRange, platform: hostPlatform, arch }; } const dryRunResults = {}; async function needWithDryRun (target) { const target2 = Object.assign({ dryRun: true }, target); const result = await need(target2); assert([ 'exists', 'fetched', 'built' ].indexOf(result) >= 0); dryRunResults[result] = true; } const targetsCache = {}; async function needViaCache (target) { const s = stringifyTarget(target); let c = targetsCache[s]; if (c) return c; c = await need(target); targetsCache[s] = c; return c; } export async function exec (argv2) { // eslint-disable-line complexity const argv = minimist(argv2, { boolean: [ 'b', 'build', 'bytecode', 'd', 'debug', 'h', 'help', 'public', 'v', 'version' ], string: [ '_', 'c', 'config', 'o', 'options', 'output', 'public-packages', 't', 'target', 'targets', 'bin-name' ], default: { bytecode: true, public: true } }); if (argv.h || argv.help) { help(); return; } // version if (argv.v || argv.version) { console.log(version); return; } log.info(`ssb-pkg@${version}`); // debug log.debugMode = argv.d || argv.debug; // forceBuild const forceBuild = argv.b || argv.build; // _ if (!argv._.length) { throw wasReported('Entry file/directory is expected', [ 'Pass --help to see usage information' ]); } if (argv._.length > 1) { throw wasReported('Not more than one entry file/directory is expected'); } // input let input = path.resolve(argv._[0]); if (!await exists(input)) { throw wasReported('Input file does not exist', [ input ]); } if ((await stat(input)).isDirectory()) { input = path.join(input, 'package.json'); if (!await exists(input)) { throw wasReported('Input file does not exist', [ input ]); } } // inputJson let inputJson, inputJsonName; if (isConfiguration(input)) { inputJson = JSON.parse(await readFile(input)); inputJsonName = inputJson.name; if (inputJsonName) { inputJsonName = inputJsonName.split('/').pop(); // @org/foo } } // inputBin, binName, otherBins let inputBin, binName, otherBins; if (inputJson) { let bin = inputJson.bin; if (bin) { if (typeof bin === 'object') { if (bin[inputJsonName]) { binName = inputJsonName bin = bin[inputJsonName]; } else { binName = Object.keys(bin)[0] bin = bin[binName] } otherBins = {} var dir = path.dirname(input) for (var name in inputJson.bin) { if (name !== binName) { otherBins[name] = path.resolve(dir, inputJson.bin[name]) } } } else { binName = inputJsonName } inputBin = path.resolve(path.dirname(input), bin); if (!await exists(inputBin)) { throw wasReported('Bin file does not exist (taken from package.json ' + '\'bin\' property)', [ inputBin ]); } } } if (!binName) { if (argv['bin-name']) { binName = argv['bin-name'] } else { throw wasReported('Missing binary name. Use --bin-name or use package.json with \'bin\' property'); } } if (inputJson && !inputBin) { throw wasReported('Property \'bin\' does not exist in', [ input ]); } // inputFin const inputFin = inputBin || input; // config let config = argv.c || argv.config; if (inputJson && config) { throw wasReported('Specify either \'package.json\' or config. Not both'); } // configJson let configJson; if (config) { config = path.resolve(config); if (!await exists(config)) { throw wasReported('Config file does not exist', [ config ]); } configJson = require(config); // may be either json or js if (!configJson.name && !configJson.files && !configJson.dependencies && !configJson.pkg) { // package.json not detected configJson = { pkg: configJson }; } } // output let output = argv.o || argv.output; let autoOutput = false; if (!output) { let name; if (inputJson) { name = inputJsonName; if (!name) { throw wasReported('Property \'name\' does not exist in', [ argv._[0] ]); } } else if (configJson) { name = configJson.name; } if (!name) { name = path.basename(inputFin); } autoOutput = true; const ext = path.extname(name); output = name.slice(0, -ext.length || undefined); } else { output = path.resolve(output); } // targets const sTargets = argv.t || argv.target || argv.targets || ''; if (typeof sTargets !== 'string') { throw wasReported(`Something is wrong near ${JSON.stringify(sTargets)}`); } let targets = parseTargets( sTargets.split(',').filter((t) => t) ); if (!targets.length) { let jsonTargets; if (inputJson && inputJson.pkg) { jsonTargets = inputJson.pkg.targets; } else if (configJson && configJson.pkg) { jsonTargets = configJson.pkg.targets; } if (jsonTargets) { targets = parseTargets(jsonTargets); } } if (!targets.length) { if (!autoOutput) { targets = parseTargets([ 'host' ]); assert(targets.length === 1); } else { targets = parseTargets([ 'linux', 'macos', 'win' ]); } log.info('Targets not specified. Assuming:', `${targets.map(stringifyTarget).join(', ')}`); } // differentParts const different = differentParts(targets); // targets[].output for (const target of targets) { let file; if (targets.length === 1) { file = output; } else { file = stringifyTargetForOutput(output, target, different); } if (target.platform === 'win' && path.extname(file) !== '.exe') file += '.exe'; } // bakes const bakes = (argv.options || '').split(',') .filter((bake) => bake).map((bake) => '--' + bake); // fetch targets const { bytecode } = argv; let fabricator; for (const target of targets) { target.forceBuild = forceBuild; await needWithDryRun(target); const f = target.fabricator = fabricatorForTarget(target); f.forceBuild = forceBuild; if (bytecode) { await needWithDryRun(f); } } if (dryRunResults.fetched && !dryRunResults.built) { log.info('Fetching base Node.js binaries to PKG_CACHE_PATH'); } for (const target of targets) { target.binaryPath = await needViaCache(target); const f = target.fabricator; if (bytecode) { if (f && fabricator && ( f.nodeRange !== fabricator.nodeRange || f.arch !== fabricator.arch )) { throw new Error('If using bytecode, Node.js version and architecture must be same for all targets') } f.binaryPath = await needViaCache(f); if (f.platform !== 'win') { await plusx(f.binaryPath); } } fabricator = f } // marker let marker; if (configJson) { marker = { config: configJson, base: path.dirname(config), configPath: config }; } else { marker = { config: inputJson || {}, // not `inputBin` because only `input` base: path.dirname(input), // is the place for `inputJson` configPath: input }; } marker.toplevel = true; // public const params = {}; if (argv.public) { params.publicToplevel = true; params.publicPackages = [ '*' ]; } if (argv['public-packages']) { params.publicPackages = argv['public-packages'].split(','); if (params.publicPackages.indexOf('*') !== -1) { params.publicPackages = [ '*' ]; } } // records let records; let entrypoint = inputFin; let otherEntrypoints = otherBins const addition = isConfiguration(input) ? input : undefined; const walkResult = await walk(marker, entrypoint, addition, params, targets, otherEntrypoints); entrypoint = walkResult.entrypoint; records = walkResult.records; const refineResult = refine(records, entrypoint, otherEntrypoints); entrypoint = refineResult.entrypoint; otherEntrypoints = refineResult.otherEntrypoints; records = refineResult.records; const backpack = packer({ records, entrypoint, bytecode, otherEntrypoints }); log.debug('Targets:', JSON.stringify(targets, null, 2)); if (await exists(output)) { if ((await stat(output)).isFile()) { await remove(output); } else { throw wasReported('Refusing to overwrite non-file output', [ output ]); } } else { await mkdirp(path.dirname(output)); } // TODO // const slash = target.platform === 'win' ? '\\' : '/'; const slash = '/'; await producer({ backpack, bakes, slash, targets, fabricator, output, binName }); if (hostPlatform !== 'win') { await plusx(output); } shutdown(); }