mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 15:05:25 +00:00
328 lines
13 KiB
JavaScript
328 lines
13 KiB
JavaScript
"use strict";
|
|
// toolversions - probe the vendored .wasm build tools for version strings.
|
|
//
|
|
// The wasm binaries are committed to the repo, so a tool's version only
|
|
// changes when someone swaps the binary. That makes the version static,
|
|
// commit-time data -- which is why we store it in TOOL_META[].version
|
|
// rather than fetching it at runtime. This script is how it gets refreshed:
|
|
//
|
|
// npm run toolversions # probe everything, print a table
|
|
// npm run toolversions -- --json out # also write machine-readable results
|
|
// npm run toolversions -- --check # exit nonzero if a stored version
|
|
// # disagrees with what the binary reports
|
|
//
|
|
// Each probe runs in its own child process: several of these tools call
|
|
// exit()/abort() when given arguments they don't understand -- some even
|
|
// deep inside emscripten's callMain() -- and that would otherwise take down
|
|
// the whole sweep. The child installs a process.on('exit') handler so the
|
|
// version evidence survives even when the tool hard-exits the process.
|
|
// A tool that wedges (see the timeouts) or prints nothing recognizable is
|
|
// reported as such -- fill those in by hand from docs or release notes.
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const fs = __importStar(require("fs"));
|
|
const path = __importStar(require("path"));
|
|
const child_process_1 = require("child_process");
|
|
const toolmeta_1 = require("../common/toolmeta");
|
|
/** The wasm toolchain directory: the tool root when one is set (a pack cache), else this checkout. */
|
|
function wasmDir() {
|
|
const root = process.env.EIGHTBITWORKSHOP_ROOT || path.resolve(__dirname, '..', '..');
|
|
return path.join(root, 'src', 'worker', 'wasm');
|
|
}
|
|
// parent kill-switch must exceed the child's self-reported deadline,
|
|
// so a well-behaved child gets to file its own report
|
|
const CHILD_PROBE_TIMEOUT_MS = 15000;
|
|
const PROBE_TIMEOUT_MS = CHILD_PROBE_TIMEOUT_MS + 10000;
|
|
//// generic version-string extraction
|
|
function extractVersion(output) {
|
|
if (!output)
|
|
return undefined;
|
|
const patterns = [
|
|
/\bV?v?(\d+\.\d+(\.\d+)?([a-z0-9.\-+]*)?)/, // V2.19 / 4.2.1 / 2.20.13
|
|
/[Vv]ersion\s+(\d+[^\s,;)]*)/, // "version 3.1.2"
|
|
];
|
|
for (var re of patterns) {
|
|
var m = re.exec(output);
|
|
if (m)
|
|
return m[1];
|
|
}
|
|
return undefined;
|
|
}
|
|
/**
|
|
* True if two version strings match. A plain prefix match would hide a real
|
|
* mismatch ('1.1.1' vs '1.1.10'), so only accept the shorter string as a
|
|
* prefix when the next character is not a digit ('1.3.0' vs '1.3.0.47.gc9db').
|
|
*/
|
|
function prefixAgrees(short, long) {
|
|
if (!long.startsWith(short))
|
|
return false;
|
|
var next = long.charAt(short.length);
|
|
return next === '' || !/[0-9]/.test(next);
|
|
}
|
|
function versionAgrees(probe, stored) {
|
|
if (probe === stored)
|
|
return true;
|
|
return prefixAgrees(probe, stored) || prefixAgrees(stored, probe);
|
|
}
|
|
// argument sequences to try, in order: most tools answer --version; some
|
|
// only print their banner when invoked with no args at all; old Unix tools
|
|
// want -v. Stop as soon as one yields a version string.
|
|
const PROBE_ARG_SEQS = [['--version'], [], ['-v']];
|
|
//// child mode state (only used inside --probe processes)
|
|
var childChunks = [];
|
|
var childModule = null;
|
|
var childReported = false;
|
|
function childCollect(line) {
|
|
line = line.trim();
|
|
if (line)
|
|
childChunks.push(line);
|
|
}
|
|
function childFinish(status) {
|
|
// dedupe, cap the evidence to something readable
|
|
var lines = childChunks.filter((s, i) => childChunks.indexOf(s) === i).slice(0, 5);
|
|
var output = lines.join('\n');
|
|
return {
|
|
module: childModule,
|
|
ids: [],
|
|
status,
|
|
output: output || undefined,
|
|
version: extractVersion(output),
|
|
};
|
|
}
|
|
function childReport(status) {
|
|
if (childReported || !childModule)
|
|
return;
|
|
childReported = true;
|
|
console.log(JSON.stringify(childFinish(status)));
|
|
}
|
|
async function probeEmscripten(moduleName, argv) {
|
|
// emscripten glue (.js) present -> load through it, inject the binary so
|
|
// node's fetch() doesn't try to parse the file path as a URL
|
|
var gluePath = path.join(wasmDir(), moduleName + '.js');
|
|
var factory = require(gluePath);
|
|
// pass the callback IN the config: several glues (sdasz80, sdcc, ...)
|
|
// compile & initialize synchronously inside the factory call, so the
|
|
// callback fires before we'd get a chance to assign it afterwards
|
|
var initialized = false;
|
|
var mod = factory({
|
|
print: childCollect,
|
|
printErr: childCollect,
|
|
noInitialRun: true,
|
|
onRuntimeInitialized: () => { initialized = true; },
|
|
wasmBinary: fs.readFileSync(path.join(wasmDir(), moduleName + '.wasm')),
|
|
locateFile: (f) => path.join(wasmDir(), f),
|
|
});
|
|
if (!initialized && !mod.calledRun) {
|
|
await new Promise((resolve, reject) => {
|
|
mod.onRuntimeInitialized = () => resolve();
|
|
setTimeout(() => reject(new Error('runtime init timeout')), CHILD_PROBE_TIMEOUT_MS);
|
|
});
|
|
}
|
|
try {
|
|
mod.callMain(argv);
|
|
}
|
|
catch (e) {
|
|
// ExitStatus after printing is fine; let the output speak
|
|
}
|
|
// give async writers a moment, then report what we got
|
|
await new Promise(resolve => setTimeout(resolve, 250));
|
|
}
|
|
async function probeWASI(moduleName, argv) {
|
|
// raw WASI binary -> run it through the same shim the worker uses
|
|
var { WASIRunner } = require('../common/wasi/wasishim');
|
|
var runner = new WASIRunner();
|
|
runner.loadSync(fs.readFileSync(path.join(wasmDir(), moduleName + '.wasm')));
|
|
runner.setArgs([moduleName, ...argv]);
|
|
runner.addPreopenDirectory('.');
|
|
try {
|
|
runner.run();
|
|
}
|
|
catch (e) {
|
|
// trap after output is fine (dasm does this even on success)
|
|
}
|
|
(runner.fds[1].getBytesAsString() + '\n' + runner.fds[2].getBytesAsString())
|
|
.split('\n').forEach(childCollect);
|
|
}
|
|
async function runProbe(moduleName, argv) {
|
|
childModule = moduleName;
|
|
// rescue hatch: these glues call process.exit() from inside callMain()
|
|
// (their own quit(), abort(), or an internal exit(0)) -- flush whatever
|
|
// they printed before they took the process down
|
|
process.on('exit', () => childReport('crash'));
|
|
if (!fs.existsSync(path.join(wasmDir(), moduleName + '.wasm'))) {
|
|
return childFinish('missing');
|
|
}
|
|
if (fs.existsSync(path.join(wasmDir(), moduleName + '.js'))) {
|
|
await probeEmscripten(moduleName, argv);
|
|
}
|
|
else {
|
|
await probeWASI(moduleName, argv);
|
|
}
|
|
return childFinish('ok');
|
|
}
|
|
// try each argument sequence until one extracts a version; keep the best
|
|
// evidence seen along the way (a tool that exits nonzero after printing
|
|
// usage still counts -- that's where several of them put their version)
|
|
async function probeModule(moduleName) {
|
|
var best = null;
|
|
for (var argv of PROBE_ARG_SEQS) {
|
|
var r = await probeInChild(moduleName, argv);
|
|
if (r.version)
|
|
return r;
|
|
// prefer evidence over silence; don't waste time re-probing wedges
|
|
if (!best || (best.status === 'empty' && r.output))
|
|
best = r;
|
|
if (r.status === 'timeout' || r.status === 'missing')
|
|
break;
|
|
}
|
|
return best;
|
|
}
|
|
//// parent mode: spawn one child per module, collect results
|
|
function probeInChild(moduleName, argv) {
|
|
return new Promise((resolve) => {
|
|
var child = (0, child_process_1.spawn)(process.execPath, [__filename, '--probe', moduleName, '--args', argv.join(',')], {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
var stdout = '';
|
|
var stderr = '';
|
|
var timer = setTimeout(() => {
|
|
child.kill('SIGKILL');
|
|
resolve({ module: moduleName, ids: [], status: 'timeout' });
|
|
}, PROBE_TIMEOUT_MS);
|
|
child.stdout.on('data', (d) => stdout += d);
|
|
child.stderr.on('data', (d) => stderr += d);
|
|
child.on('close', () => {
|
|
clearTimeout(timer);
|
|
// the child prints exactly one JSON object as its last stdout line
|
|
var lines = stdout.trim().split('\n');
|
|
var last = lines[lines.length - 1];
|
|
if (last && last.startsWith('{')) {
|
|
try {
|
|
var result = JSON.parse(last);
|
|
result.ids = [];
|
|
resolve(result);
|
|
return;
|
|
}
|
|
catch (e) { /* fall through */ }
|
|
}
|
|
// died before reporting anything usable
|
|
resolve({
|
|
module: moduleName, ids: [], status: 'crash',
|
|
output: ((stdout + '\n' + stderr).trim().split('\n')
|
|
.filter((s) => s && !s.startsWith('{')).slice(0, 5).join('\n')) || undefined,
|
|
});
|
|
});
|
|
});
|
|
}
|
|
async function main() {
|
|
var _a, _b;
|
|
var args = process.argv.slice(2);
|
|
// internal: single-module probe, used by the parent via spawn()
|
|
var probeIdx = args.indexOf('--probe');
|
|
if (probeIdx >= 0) {
|
|
var moduleName = args[probeIdx + 1];
|
|
var probeArgs = (_b = (_a = args[args.indexOf('--args') + 1]) === null || _a === void 0 ? void 0 : _a.split(',')) !== null && _b !== void 0 ? _b : ['--version'];
|
|
// self-imposed deadline: report 'timeout' as JSON rather than making
|
|
// the parent SIGKILL us, so the protocol stays uniform
|
|
var watchdog = setTimeout(() => {
|
|
childReport('timeout');
|
|
process.exit(0);
|
|
}, CHILD_PROBE_TIMEOUT_MS);
|
|
var result = await runProbe(moduleName, probeArgs);
|
|
clearTimeout(watchdog);
|
|
childReport(result.status === 'ok' && !childChunks.length ? 'empty' : result.status);
|
|
process.exit(0);
|
|
}
|
|
var jsonOut = null;
|
|
var jsonIdx = args.indexOf('--json');
|
|
if (jsonIdx >= 0)
|
|
jsonOut = args[jsonIdx + 1];
|
|
var checkOnly = args.includes('--check');
|
|
// group tool ids by wasm module (arm-tcc serves two tools)
|
|
var byModule = {};
|
|
for (var id of Object.keys(toolmeta_1.TOOL_META)) {
|
|
var meta = toolmeta_1.TOOL_META[id];
|
|
if (meta.wasmModule) {
|
|
(byModule[meta.wasmModule] = byModule[meta.wasmModule] || []).push(id);
|
|
}
|
|
}
|
|
var results = [];
|
|
for (var moduleName of Object.keys(byModule)) {
|
|
process.stderr.write('probing ' + moduleName + '...\n');
|
|
var result = await probeModule(moduleName);
|
|
result.ids = byModule[moduleName];
|
|
results.push(result);
|
|
}
|
|
results.sort((a, b) => a.module.localeCompare(b.module));
|
|
// report
|
|
console.log('');
|
|
var mismatches = 0;
|
|
for (var r of results) {
|
|
var label = r.ids.map((id) => toolmeta_1.TOOL_META[id].version != null ? id : id + '*').join(', ');
|
|
var stored = r.ids.map((id) => toolmeta_1.TOOL_META[id].version).find((v) => v != null);
|
|
var alts = r.ids.map((id) => toolmeta_1.TOOL_META[id].versionAlt).filter((v) => v != null);
|
|
var agree = stored == null || r.version == null || versionAgrees(r.version, stored)
|
|
|| alts.some((a) => versionAgrees(r.version, a));
|
|
if (!agree)
|
|
mismatches++;
|
|
console.log((r.version ? '' : '! ') +
|
|
r.module.padEnd(16) +
|
|
('[' + r.status + ']').padEnd(10) +
|
|
(r.version ? r.version.padEnd(14) : ''.padEnd(14)) +
|
|
'meta:' + (stored !== null && stored !== void 0 ? stored : '-') +
|
|
(agree ? '' : ' MISMATCH') +
|
|
' (' + label + ')');
|
|
if (r.output && !r.version) {
|
|
console.log(''.padEnd(42) + '> ' + r.output.split('\n')[0]);
|
|
}
|
|
}
|
|
console.log('\n* = tool has no version stored in TOOL_META yet\n');
|
|
if (jsonOut) {
|
|
fs.writeFileSync(jsonOut, JSON.stringify(results, null, 2) + '\n');
|
|
console.log('wrote ' + jsonOut);
|
|
}
|
|
if (checkOnly && mismatches > 0) {
|
|
console.error(mismatches + ' tool(s) disagree with TOOL_META');
|
|
process.exit(1);
|
|
}
|
|
process.exit(0);
|
|
}
|
|
if (require.main === module) {
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
//# sourceMappingURL=toolversions.js.map
|