Files: d456a6cc8589489e558e6164750474ed2cfc961e / lib / index.js
11701 bytesRaw
1 | /* eslint-disable require-atomic-updates */ |
2 | |
3 | import { exists, mkdirp, readFile, remove, stat } from 'fs-extra'; |
4 | import { log, wasReported } from './log.js'; |
5 | import { need, system } from 'ssb-pkg-fetch'; |
6 | import assert from 'assert'; |
7 | import help from './help'; |
8 | import { isPackageJson } from '../prelude/common.js'; |
9 | import minimist from 'minimist'; |
10 | import packer from './packer.js'; |
11 | import path from 'path'; |
12 | import { plusx } from './chmod.js'; |
13 | import producer from './producer.js'; |
14 | import refine from './refiner.js'; |
15 | import { shutdown } from './fabricator.js'; |
16 | import { version } from '../package.json'; |
17 | import walk from './walker.js'; |
18 | import SSB from './ssb.js'; |
19 | |
20 | function isConfiguration (file) { |
21 | return isPackageJson(file) || file.endsWith('.config.json'); |
22 | } |
23 | |
24 | // http://www.openwall.com/lists/musl/2012/12/08/4 |
25 | |
26 | const { hostArch, hostPlatform, isValidNodeRange, knownArchs, |
27 | knownPlatforms, toFancyArch, toFancyPlatform } = system; |
28 | const hostNodeRange = 'node' + process.version.match(/^v(\d+)/)[1]; |
29 | |
30 | function parseTargets (items) { |
31 | // [ 'node6-macos-x64', 'node6-linux-x64' ] |
32 | const targets = []; |
33 | for (const item of items) { |
34 | const target = { |
35 | nodeRange: hostNodeRange, |
36 | platform: hostPlatform, |
37 | arch: hostArch |
38 | }; |
39 | if (item !== 'host') { |
40 | for (const token of item.split('-')) { |
41 | if (!token) continue; |
42 | if (isValidNodeRange(token)) { |
43 | target.nodeRange = token; |
44 | continue; |
45 | } |
46 | const p = toFancyPlatform(token); |
47 | if (knownPlatforms.indexOf(p) >= 0) { |
48 | target.platform = p; |
49 | continue; |
50 | } |
51 | const a = toFancyArch(token); |
52 | if (knownArchs.indexOf(a) >= 0) { |
53 | target.arch = a; |
54 | continue; |
55 | } |
56 | throw wasReported(`Unknown token '${token}' in '${item}'`); |
57 | } |
58 | } |
59 | targets.push(target); |
60 | } |
61 | return targets; |
62 | } |
63 | |
64 | function stringifyTarget (target) { |
65 | const { nodeRange, platform, arch } = target; |
66 | return `${nodeRange}-${platform}-${arch}`; |
67 | } |
68 | |
69 | function differentParts (targets) { |
70 | const nodeRanges = {}; |
71 | const platforms = {}; |
72 | const archs = {}; |
73 | for (const target of targets) { |
74 | nodeRanges[target.nodeRange] = true; |
75 | platforms[target.platform] = true; |
76 | archs[target.arch] = true; |
77 | } |
78 | const result = {}; |
79 | if (Object.keys(nodeRanges).length > 1) { |
80 | result.nodeRange = true; |
81 | } |
82 | if (Object.keys(platforms).length > 1) { |
83 | result.platform = true; |
84 | } |
85 | if (Object.keys(archs).length > 1) { |
86 | result.arch = true; |
87 | } |
88 | return result; |
89 | } |
90 | |
91 | function stringifyTargetForOutput (output, target, different) { |
92 | const a = [ output ]; |
93 | if (different.nodeRange) a.push(target.nodeRange); |
94 | if (different.platform) a.push(target.platform); |
95 | if (different.arch) a.push(target.arch); |
96 | return a.join('-'); |
97 | } |
98 | |
99 | function fabricatorForTarget (target) { |
100 | const { nodeRange, arch } = target; |
101 | return { nodeRange, platform: hostPlatform, arch }; |
102 | } |
103 | |
104 | const dryRunResults = {}; |
105 | |
106 | async function needWithDryRun (target) { |
107 | const target2 = Object.assign({ dryRun: true }, target); |
108 | const result = await need(target2); |
109 | assert([ 'exists', 'fetched', 'built' ].indexOf(result) >= 0); |
110 | dryRunResults[result] = true; |
111 | } |
112 | |
113 | const targetsCache = {}; |
114 | |
115 | async function needViaCache (target) { |
116 | const s = stringifyTarget(target); |
117 | let c = targetsCache[s]; |
118 | if (c) return c; |
119 | c = await need(target); |
120 | targetsCache[s] = c; |
121 | return c; |
122 | } |
123 | |
124 | export async function exec (argv2) { // eslint-disable-line complexity |
125 | const argv = minimist(argv2, { |
126 | boolean: [ 'b', 'build', 'bytecode', 'd', 'debug', |
127 | 'h', 'help', 'public', 'v', 'version' ], |
128 | string: [ '_', 'c', 'config', 'o', 'options', 'output', |
129 | 'public-packages', |
130 | 't', 'target', 'targets', 'bin-name' ], |
131 | default: { bytecode: true, public: true } |
132 | }); |
133 | |
134 | if (argv.h || argv.help) { |
135 | help(); |
136 | return; |
137 | } |
138 | |
139 | // version |
140 | |
141 | if (argv.v || argv.version) { |
142 | console.log(version); |
143 | return; |
144 | } |
145 | |
146 | log.info(`ssb-pkg@${version}`); |
147 | |
148 | // debug |
149 | |
150 | log.debugMode = argv.d || argv.debug; |
151 | |
152 | // forceBuild |
153 | |
154 | const forceBuild = argv.b || argv.build; |
155 | |
156 | // _ |
157 | |
158 | if (!argv._.length) { |
159 | throw wasReported('Entry file/directory is expected', [ |
160 | 'Pass --help to see usage information' ]); |
161 | } |
162 | if (argv._.length > 1) { |
163 | throw wasReported('Not more than one entry file/directory is expected'); |
164 | } |
165 | |
166 | // input |
167 | |
168 | let input = path.resolve(argv._[0]); |
169 | |
170 | if (!await exists(input)) { |
171 | throw wasReported('Input file does not exist', [ input ]); |
172 | } |
173 | if ((await stat(input)).isDirectory()) { |
174 | input = path.join(input, 'package.json'); |
175 | if (!await exists(input)) { |
176 | throw wasReported('Input file does not exist', [ input ]); |
177 | } |
178 | } |
179 | |
180 | // inputJson |
181 | |
182 | let inputJson, inputJsonName; |
183 | |
184 | if (isConfiguration(input)) { |
185 | inputJson = JSON.parse(await readFile(input)); |
186 | inputJsonName = inputJson.name; |
187 | if (inputJsonName) { |
188 | inputJsonName = inputJsonName.split('/').pop(); // @org/foo |
189 | } |
190 | } |
191 | |
192 | // inputBin, binName, otherBins |
193 | |
194 | let inputBin, binName, otherBins; |
195 | |
196 | if (inputJson) { |
197 | let bin = inputJson.bin; |
198 | if (bin) { |
199 | if (typeof bin === 'object') { |
200 | if (bin[inputJsonName]) { |
201 | binName = inputJsonName |
202 | bin = bin[inputJsonName]; |
203 | } else { |
204 | binName = Object.keys(bin)[0] |
205 | bin = bin[binName] |
206 | } |
207 | otherBins = {} |
208 | var dir = path.dirname(input) |
209 | for (var name in inputJson.bin) { |
210 | if (name !== binName) { |
211 | otherBins[name] = path.resolve(dir, inputJson.bin[name]) |
212 | } |
213 | } |
214 | } else { |
215 | binName = inputJsonName |
216 | } |
217 | inputBin = path.resolve(path.dirname(input), bin); |
218 | if (!await exists(inputBin)) { |
219 | throw wasReported('Bin file does not exist (taken from package.json ' + |
220 | '\'bin\' property)', [ inputBin ]); |
221 | } |
222 | } |
223 | } |
224 | |
225 | if (!binName) { |
226 | if (argv['bin-name']) { |
227 | binName = argv['bin-name'] |
228 | } else { |
229 | throw wasReported('Missing binary name. Use --bin-name <name> or use package.json with \'bin\' property'); |
230 | } |
231 | } |
232 | |
233 | if (inputJson && !inputBin) { |
234 | throw wasReported('Property \'bin\' does not exist in', [ input ]); |
235 | } |
236 | |
237 | // inputFin |
238 | |
239 | const inputFin = inputBin || input; |
240 | |
241 | // config |
242 | |
243 | let config = argv.c || argv.config; |
244 | |
245 | if (inputJson && config) { |
246 | throw wasReported('Specify either \'package.json\' or config. Not both'); |
247 | } |
248 | |
249 | // configJson |
250 | |
251 | let configJson; |
252 | |
253 | if (config) { |
254 | config = path.resolve(config); |
255 | if (!await exists(config)) { |
256 | throw wasReported('Config file does not exist', [ config ]); |
257 | } |
258 | configJson = require(config); // may be either json or js |
259 | if (!configJson.name && !configJson.files && |
260 | !configJson.dependencies && !configJson.pkg) { // package.json not detected |
261 | configJson = { pkg: configJson }; |
262 | } |
263 | } |
264 | |
265 | // output |
266 | |
267 | let output = argv.o || argv.output; |
268 | let autoOutput = false; |
269 | |
270 | if (!output) { |
271 | let name; |
272 | if (inputJson) { |
273 | name = inputJsonName; |
274 | if (!name) { |
275 | throw wasReported('Property \'name\' does not exist in', [ argv._[0] ]); |
276 | } |
277 | } else |
278 | if (configJson) { |
279 | name = configJson.name; |
280 | } |
281 | if (!name) { |
282 | name = path.basename(inputFin); |
283 | } |
284 | autoOutput = true; |
285 | const ext = path.extname(name); |
286 | output = name.slice(0, -ext.length || undefined); |
287 | } else { |
288 | output = path.resolve(output); |
289 | } |
290 | |
291 | // targets |
292 | |
293 | const sTargets = argv.t || argv.target || argv.targets || ''; |
294 | |
295 | if (typeof sTargets !== 'string') { |
296 | throw wasReported(`Something is wrong near ${JSON.stringify(sTargets)}`); |
297 | } |
298 | |
299 | let targets = parseTargets( |
300 | sTargets.split(',').filter((t) => t) |
301 | ); |
302 | |
303 | if (!targets.length) { |
304 | let jsonTargets; |
305 | if (inputJson && inputJson.pkg) { |
306 | jsonTargets = inputJson.pkg.targets; |
307 | } else |
308 | if (configJson && configJson.pkg) { |
309 | jsonTargets = configJson.pkg.targets; |
310 | } |
311 | if (jsonTargets) { |
312 | targets = parseTargets(jsonTargets); |
313 | } |
314 | } |
315 | |
316 | if (!targets.length) { |
317 | if (!autoOutput) { |
318 | targets = parseTargets([ 'host' ]); |
319 | assert(targets.length === 1); |
320 | } else { |
321 | targets = parseTargets([ 'linux', 'macos', 'win' ]); |
322 | } |
323 | log.info('Targets not specified. Assuming:', |
324 | `${targets.map(stringifyTarget).join(', ')}`); |
325 | } |
326 | |
327 | // differentParts |
328 | |
329 | const different = differentParts(targets); |
330 | |
331 | // targets[].output |
332 | |
333 | for (const target of targets) { |
334 | let file; |
335 | if (targets.length === 1) { |
336 | file = output; |
337 | } else { |
338 | file = stringifyTargetForOutput(output, target, different); |
339 | } |
340 | if (target.platform === 'win' && |
341 | path.extname(file) !== '.exe') file += '.exe'; |
342 | } |
343 | |
344 | // bakes |
345 | |
346 | const bakes = (argv.options || '').split(',') |
347 | .filter((bake) => bake).map((bake) => '--' + bake); |
348 | |
349 | // fetch targets |
350 | |
351 | const { bytecode } = argv; |
352 | |
353 | let fabricator; |
354 | |
355 | for (const target of targets) { |
356 | target.forceBuild = forceBuild; |
357 | await needWithDryRun(target); |
358 | const f = target.fabricator = fabricatorForTarget(target); |
359 | f.forceBuild = forceBuild; |
360 | if (bytecode) { |
361 | await needWithDryRun(f); |
362 | } |
363 | } |
364 | |
365 | if (dryRunResults.fetched && !dryRunResults.built) { |
366 | log.info('Fetching base Node.js binaries to PKG_CACHE_PATH'); |
367 | } |
368 | |
369 | for (const target of targets) { |
370 | target.binaryPath = await needViaCache(target); |
371 | const f = target.fabricator; |
372 | if (bytecode) { |
373 | if (f && fabricator && ( |
374 | f.nodeRange !== fabricator.nodeRange || |
375 | f.arch !== fabricator.arch |
376 | )) { |
377 | throw new Error('If using bytecode, Node.js version and architecture must be same for all targets') |
378 | } |
379 | f.binaryPath = await needViaCache(f); |
380 | if (f.platform !== 'win') { |
381 | await plusx(f.binaryPath); |
382 | } |
383 | } |
384 | fabricator = f |
385 | } |
386 | |
387 | // marker |
388 | |
389 | let marker; |
390 | |
391 | if (configJson) { |
392 | marker = { |
393 | config: configJson, |
394 | base: path.dirname(config), |
395 | configPath: config |
396 | }; |
397 | } else { |
398 | marker = { |
399 | config: inputJson || {}, // not `inputBin` because only `input` |
400 | base: path.dirname(input), // is the place for `inputJson` |
401 | configPath: input |
402 | }; |
403 | } |
404 | |
405 | marker.toplevel = true; |
406 | |
407 | // public |
408 | |
409 | const params = {}; |
410 | if (argv.public) { |
411 | params.publicToplevel = true; |
412 | params.publicPackages = [ '*' ]; |
413 | } |
414 | if (argv['public-packages']) { |
415 | params.publicPackages = argv['public-packages'].split(','); |
416 | if (params.publicPackages.indexOf('*') !== -1) { |
417 | params.publicPackages = [ '*' ]; |
418 | } |
419 | } |
420 | |
421 | // records |
422 | |
423 | let records; |
424 | let entrypoint = inputFin; |
425 | let otherEntrypoints = otherBins |
426 | const addition = isConfiguration(input) ? input : undefined; |
427 | |
428 | const walkResult = await walk(marker, entrypoint, addition, params, targets, otherEntrypoints); |
429 | entrypoint = walkResult.entrypoint; |
430 | records = walkResult.records; |
431 | |
432 | const refineResult = refine(records, entrypoint, otherEntrypoints); |
433 | entrypoint = refineResult.entrypoint; |
434 | otherEntrypoints = refineResult.otherEntrypoints; |
435 | records = refineResult.records; |
436 | |
437 | const backpack = packer({ records, entrypoint, bytecode, otherEntrypoints }); |
438 | |
439 | log.debug('Targets:', JSON.stringify(targets, null, 2)); |
440 | |
441 | if (await exists(output)) { |
442 | if ((await stat(output)).isFile()) { |
443 | await remove(output); |
444 | } else { |
445 | throw wasReported('Refusing to overwrite non-file output', [ output ]); |
446 | } |
447 | } else { |
448 | await mkdirp(path.dirname(output)); |
449 | } |
450 | |
451 | // TODO |
452 | // const slash = target.platform === 'win' ? '\\' : '/'; |
453 | const slash = '/'; |
454 | |
455 | let ssb = new SSB() |
456 | await ssb.connect() |
457 | |
458 | await producer({ backpack, bakes, slash, targets, fabricator, output, binName, ssb }); |
459 | |
460 | if (hostPlatform !== 'win') { |
461 | await plusx(output); |
462 | } |
463 | |
464 | shutdown(); |
465 | await ssb.close(); |
466 | } |
467 |
Built with git-ssb-web