Files
8bitworkshop/gen/tools/8bws.js
T

791 lines
36 KiB
JavaScript

#!/usr/bin/env node
"use strict";
// 8bws - 8bitworkshop command line tool.
//
// 8bws build --platform c64 hello.c -o hello.prg
// 8bws run --platform c64 hello.c -e "run 100; screen"
// 8bws run --platform c64 hello.prg --png shot.png
//
// Two verbs: `build` compiles a source file, `run` executes a ROM -- or a
// source file, which it builds first. Emulation goes through EmuTarget
// (emutarget.ts).
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 os = __importStar(require("os"));
const path = __importStar(require("path"));
const util_1 = require("../common/util");
const cliformat_1 = require("./cliformat");
const emutarget_1 = require("./emutarget");
const runscript_1 = require("./runscript");
const verifyreplay_1 = require("./verifyreplay");
const vcdfile_1 = require("./vcdfile");
const debugcontroller_1 = require("../common/debugcontroller");
const workertypes_1 = require("../common/workertypes");
const symbolfile_1 = require("../common/symbols/symbolfile");
const testlib_1 = require("./testlib");
const detect_1 = require("../common/detect");
const toolroot_1 = require("./toolroot");
const toolmeta_1 = require("../common/toolmeta");
/** Options that may be repeated; each occurrence adds to a list. */
const REPEATABLE_FLAGS = new Set([
'define', 'as-define', 'ld-define', 'cflag', 'asflag', 'ldflag',
'option',
]);
/** Extensions always treated as ROMs, even if the contents look like text. */
const ROM_EXTS = new Set(['.rom', '.bin', ...Object.keys(detect_1.ROM_PLATFORMS)]);
const SHORT_FLAGS = {
p: 'platform', t: 'tool', o: 'output', f: 'frames', e: 'eval',
};
// Flags that never take a value, per command. Everything else consumes the
// next argument unless that argument is another flag.
const BOOLEAN_FLAGS = {
build: ['check', 'symbols', 'save', 'no-warnings'],
run: ['info', 'no-warnings'],
'verify-replay': ['verbose'],
new: ['libs', 'force'],
};
const ALIASES = {
compile: 'build',
check: 'build',
compilerun: 'run',
verify: 'verify-replay',
};
function parseArgs(argv) {
let command = argv[2] || 'help';
if (command.startsWith('-'))
command = 'help';
const resolved = ALIASES[command] || command;
const booleans = new Set(['json', ...(BOOLEAN_FLAGS[resolved] || [])]);
const args = {};
const positional = [];
if (command === 'check')
args['check'] = true;
for (let i = 3; i < argv.length; i++) {
const arg = argv[i];
if (!arg.startsWith('-') || arg === '-') {
positional.push(arg);
continue;
}
const key = arg.startsWith('--') ? arg.substring(2) : (SHORT_FLAGS[arg.substring(1)] || arg.substring(1));
const next = argv[i + 1];
if (!booleans.has(key) && next != null && !next.startsWith('--')) {
const val = argv[++i];
if (REPEATABLE_FLAGS.has(key)) {
const cur = args[key];
if (cur == null)
args[key] = [val];
else if (Array.isArray(cur))
cur.push(val);
else
args[key] = [cur, val];
}
else
args[key] = val;
}
else
args[key] = true;
}
return { command: resolved, args, positional };
}
function str(args, key) {
const v = args[key];
return typeof v === 'string' ? v : undefined;
}
/** All values for a repeatable option (empty if absent). */
function list(args, key) {
const v = args[key];
if (v == null)
return [];
return Array.isArray(v) ? v : [v];
}
/** Build-symbol / build-arg overrides from the command line. */
function buildOverrides(args) {
return {
symbols: {
compiler: list(args, 'define'),
assembler: list(args, 'as-define'),
linker: list(args, 'ld-define'),
},
buildArgs: {
compiler: list(args, 'cflag'),
assembler: list(args, 'asflag'),
linker: list(args, 'ldflag'),
},
};
}
/** Compile one source file. Exits with the compiler's errors if it fails. */
async function compileSource(args, source, platform) {
const { compileSourceFile, getToolForFilename, initialize, preload, TOOLS } = await Promise.resolve().then(() => __importStar(require('./testlib')));
await initialize();
const tool = str(args, 'tool') || getToolForFilename(source, platform);
if (!TOOLS[tool]) {
(0, cliformat_1.fail)('build', `Unknown tool: ${tool}. Use list-tools to see available tools.`);
}
await (0, toolroot_1.ensureToolchains)(platform, tool, fs.readFileSync(source, 'utf8'));
await preload(tool, platform);
const result = await compileSourceFile(tool, platform, source, undefined, buildOverrides(args));
if (result.internal) {
(0, cliformat_1.warn)(`${result.internal.tool} crashed; the IDE would send an error report: ${result.internal.msg}`);
if (process.env.DEBUG)
console.error(result.internal.stack);
}
if (!result.success) {
(0, cliformat_1.fail)('build', `${tool} failed on ${source}`, { errors: result.errors, internal: result.internal });
}
if (result.warnings && result.warnings.length && !args['no-warnings']) {
for (const w of result.warnings)
(0, cliformat_1.warn)(`${w.path || source}:${w.line}: ${w.msg}`);
}
return { rom: (0, testlib_1.romBytes)(result), symbolmap: result.symbolmap || {}, tool, platform, source, result };
}
async function doBuild(args, positional) {
var _a;
let source = positional[0];
const checkOnly = !!args['check'];
if (!source) {
(0, cliformat_1.fail)('build', 'Required: build [--platform <platform>] <source> [--tool <tool>] [-o <file>]');
}
if (!fs.existsSync(source))
(0, cliformat_1.fail)('build', `No such file: ${source}`);
const platformArg = str(args, 'platform');
let resolvedPlatform;
if (fs.statSync(source).isDirectory()) {
const resolved = await resolveDirectory('build', source, platformArg);
source = resolved.source;
resolvedPlatform = resolved.platform;
}
const platform = platformArg || resolvedPlatform || await inferPlatform('build', source);
const built = await compileSource(args, source, platform);
const outputFile = str(args, 'output');
if (outputFile && !checkOnly) {
if (!built.rom)
(0, cliformat_1.fail)('build', `${built.tool} produces no ROM image to write`);
fs.writeFileSync(outputFile, Buffer.from(built.rom));
}
const data = {
tool: built.tool, platform, source,
outputSize: built.rom ? built.rom.length : null,
outputFile: outputFile || null,
};
if ((_a = built.result.warnings) === null || _a === void 0 ? void 0 : _a.length)
data.warnings = built.result.warnings;
if (args['symbols']) {
if (built.result.symbolmap)
data.symbolmap = built.result.symbolmap;
if (built.result.segments)
data.segments = built.result.segments;
}
if (args['save'])
await saveBuildFiles(source, data);
(0, cliformat_1.output)({ success: true, command: checkOnly ? 'check' : 'build', data });
}
/** Dump every intermediate file the build produced to a temp directory. */
async function saveBuildFiles(source, data) {
const { store } = await Promise.resolve().then(() => __importStar(require('./testlib')));
const saveDir = path.join(os.tmpdir(), `8bws-${path.basename(source, path.extname(source))}`);
fs.mkdirSync(saveDir, { recursive: true });
data.savedFiles = [];
for (const [filePath, entry] of Object.entries(store.workfs)) {
const outPath = path.join(saveDir, filePath);
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, entry.data);
data.savedFiles.push(filePath);
}
data.saveDir = saveDir;
}
////////////////////////////////////////////////////////////////////////
// run
/** A source file is anything that isn't obviously a ROM image. */
function looksLikeROM(file) {
if (ROM_EXTS.has(path.extname(file).toLowerCase()))
return true;
return (0, util_1.isProbablyBinary)(file, fs.readFileSync(file));
}
async function openTarget(args, platformId) {
const target = await (0, emutarget_1.loadPlatform)(platformId, parsePlatformOptions(args));
const vectorSize = str(args, 'vector-size');
if (vectorSize)
target.vectorSize = parseInt(vectorSize);
await target.start();
const bios = str(args, 'bios');
if (bios && !target.loadBIOS(new Uint8Array(fs.readFileSync(bios)))) {
(0, cliformat_1.fail)('run', `'${target.id}' does not accept a BIOS image`);
}
return target;
}
/** Turn repeated --option key=value flags into a platform options object. */
function parsePlatformOptions(args) {
var _a;
const options = {};
for (const kv of (_a = list(args, 'option')) !== null && _a !== void 0 ? _a : []) {
const i = kv.indexOf('=');
if (i <= 0)
(0, cliformat_1.fail)('run', `bad --option '${kv}' (expected key=value)`);
options[kv.substring(0, i)] = kv.substring(i + 1);
}
return options;
}
/** Turn the run flags into script commands, appended to any --script/-e text. */
function buildScript(args) {
var _a, _b, _c;
const parts = [];
const frameDir = str(args, 'frames-dir');
// --frames-dir records frames instead of just advancing, so it goes last,
// after any --script/-e setup (e.g. `run 60` to reach the title screen)
if (args['frames'] && !frameDir)
parts.push(`run ${(_a = str(args, 'frames')) !== null && _a !== void 0 ? _a : 1}`);
const script = (_b = str(args, 'eval')) !== null && _b !== void 0 ? _b : str(args, 'script');
if (script)
parts.push(fs.existsSync(script) ? fs.readFileSync(script, 'utf8') : script);
if (frameDir)
parts.push(`capture ${(_c = str(args, 'frames')) !== null && _c !== void 0 ? _c : 60} ${frameDir}`);
if (args['info'])
parts.push('info');
const memdump = str(args, 'memdump');
if (memdump) {
const [start, end] = memdump.split(',').map((s) => (0, runscript_1.parseNum)(s.startsWith('$') || /^0x/i.test(s) ? s : '$' + s));
if (isNaN(start) || isNaN(end) || end < start) {
(0, cliformat_1.fail)('run', `Invalid --memdump range: ${memdump} (use hex addresses like 0000,00ff)`);
}
parts.push(`mem $${start.toString(16)} ${end - start + 1}`);
}
return parts.length ? parts.join('\n') : 'run 1';
}
/** Build `input` if it's source, and load it into a new emulator. */
async function openProgram(command, args, input) {
if (!input) {
(0, cliformat_1.fail)(command, `Required: ${command} --platform <id> <rom-or-source>`);
}
if (!fs.existsSync(input))
(0, cliformat_1.fail)(command, `No such file: ${input}`);
const platformArg = str(args, 'platform');
let resolvedPlatform;
if (fs.statSync(input).isDirectory()) {
const resolved = await resolveDirectory(command, input, platformArg);
input = resolved.source;
resolvedPlatform = resolved.platform;
}
// A source file is built first; a ROM is loaded as-is.
let romFile = input;
let symbols = {};
let built;
let platformId = platformArg || resolvedPlatform || detect_1.ROM_PLATFORMS[path.extname(input).toLowerCase()];
if (!looksLikeROM(input)) {
if (!platformId)
platformId = await inferPlatform(command, input);
built = await compileSource(args, input, platformId);
symbols = built.symbolmap;
if (built.rom) {
romFile = path.join(os.tmpdir(), '8bws-' + path.basename(input).replace(/\.\w+$/, '') + '.rom');
fs.writeFileSync(romFile, Buffer.from(built.rom));
(0, cliformat_1.note)(`built ${input} with ${built.tool} -> ${romFile} (${built.rom.length} bytes)`);
}
else {
romFile = null; // loaded straight from the build
(0, cliformat_1.note)(`built ${input} with ${built.tool}`);
}
}
else if (!platformId) {
(0, cliformat_1.fail)(command, `Cannot infer a platform from '${path.basename(input)}': pass --platform`);
}
const target = await openTarget(args, platformId);
// The temp file is always written as .rom, but some platforms use the title
// to pick a load format -- an Atari XEX named .rom would load as a cartridge.
if (built)
target.setFileData(built.result.files || {});
if (romFile) {
let romTitle = path.basename(romFile);
if (built && target.platform.getROMExtension) {
romTitle = path.basename(romFile, '.rom') + target.platform.getROMExtension(new Uint8Array(built.rom));
}
await target.loadROM(new Uint8Array(fs.readFileSync(romFile)), romTitle);
}
else {
await target.loadROM(built.result.output, path.basename(input));
}
let debugInfo;
if (built) {
const mainPath = path.basename(input);
debugInfo = Object.assign(Object.assign({}, (0, workertypes_1.buildProducts)(built.result)), { mainPath, paths: [mainPath] });
}
return { target, source: input, romFile, built, symbols, debugInfo };
}
async function doRun(args, positional) {
var _a, _b, _c;
const input = positional[0];
const { target, romFile, symbols, debugInfo } = await openProgram('run', args, input);
const script = new runscript_1.RunScript(target);
script.addSymbols(symbols);
if (debugInfo)
script.setDebugContext((0, debugcontroller_1.buildDebugContext)(debugInfo));
const symbolFile = str(args, 'symbols');
if (symbolFile)
script.addSymbols((0, symbolfile_1.parseSymbolFile)(fs.readFileSync(symbolFile, 'utf8')));
script.startTracing();
// `vcd FILE` writes as it goes, so a long recording doesn't sit in memory
script.openFile = vcdfile_1.openVcdFile;
// `capture N DIR` / `png FILE`: encode each frame with the same PNG writer as --png
const { encode } = await Promise.resolve().then(() => __importStar(require('fast-png')));
script.writeFrame = (file, video) => {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, encodePNG(encode, video));
};
try {
script.run(buildScript(args));
}
finally {
script.finish();
}
const video = target.getVideo();
const frameDir = str(args, 'frames-dir');
const fps = parseInt((_a = str(args, 'fps')) !== null && _a !== void 0 ? _a : '') || target.frameRate;
(0, cliformat_1.output)({
success: true,
command: 'run',
data: {
platform: target.id,
rom: romFile,
frames: target.frameCount,
width: (_b = video === null || video === void 0 ? void 0 : video.width) !== null && _b !== void 0 ? _b : null,
height: (_c = video === null || video === void 0 ? void 0 : video.height) !== null && _c !== void 0 ? _c : null,
png: str(args, 'png') || null,
framesDir: frameDir || null,
fps: frameDir ? fps : null,
ffmpeg: frameDir
? `ffmpeg -framerate ${fps} -i ${frameDir.replace(/[\\/]+$/, '')}/frame_%05d.png -pix_fmt yuv420p out.mp4`
: null,
}
});
await writeScreenshot(video, str(args, 'png'));
}
/** Encode a frame as a 4-channel PNG (the pixel view is already RGBA bytes). */
function encodePNG(encode, video) {
return encode({
width: video.width, height: video.height,
data: new Uint8Array(video.pixels.buffer, video.pixels.byteOffset, video.width * video.height * 4),
channels: 4,
});
}
async function writeScreenshot(video, pngFile) {
if (!video)
return;
const showInTerminal = process.stdout.isTTY;
if (!pngFile && !showInTerminal)
return;
const { encode } = await Promise.resolve().then(() => __importStar(require('fast-png')));
const png = encodePNG(encode, video);
if (pngFile)
fs.writeFileSync(pngFile, png);
if (showInTerminal) {
const { displayImageInTerminal } = await Promise.resolve().then(() => __importStar(require('./termimage')));
displayImageInTerminal(png, video.width, video.height);
}
}
////////////////////////////////////////////////////////////////////////
// platform detection
function presetsDir() {
for (const dir of [path.resolve((0, toolroot_1.toolRoot)(), 'presets'), path.resolve(__dirname, '../../presets')]) {
if (fs.existsSync(dir))
return dir;
}
return null;
}
/** Ranked platform guesses for a source file (and its neighbors) or a directory. */
async function detectPath(input) {
const { detectProject, headersFromPresets } = await Promise.resolve().then(() => __importStar(require('../common/detect')));
const { PLATFORM_PARAMS } = await Promise.resolve().then(() => __importStar(require('../worker/platforms')));
const isDir = fs.statSync(input).isDirectory();
const dir = isDir ? input : path.dirname(input);
let files = fs.readdirSync(dir).filter((f) => !f.endsWith('~') && fs.statSync(path.join(dir, f)).isFile());
// for a file, only it and the files that could be its libraries or build files
if (!isDir) {
const main = path.basename(input);
files = [main, ...files.filter((f) => f !== main && (/\.(h|inc|i|cfg|mk)$/i.test(f) || /^(makefile|readme(\.md)?)$/i.test(f)))];
}
const listing = {};
const presets = presetsDir();
const platforms = Object.keys(PLATFORM_PARAMS).filter((p) => p.indexOf('.') < 0);
if (presets) {
for (const p of fs.readdirSync(presets)) {
if (platforms.includes(p))
listing[p] = fs.readdirSync(path.join(presets, p));
}
}
const read = (f) => {
const full = path.join(dir, f);
if ((0, util_1.isProbablyBinary)(f))
return null;
try {
return fs.readFileSync(full, 'utf8');
}
catch (e) {
return null;
}
};
return detectProject({ files, read, platforms, headers: headersFromPresets(listing), dirName: path.basename(path.resolve(dir)) });
}
/** The platform for a source file given without --platform, or fail with the candidates. */
async function inferPlatform(command, input) {
const { isClearWinner } = await Promise.resolve().then(() => __importStar(require('../common/detect')));
const found = await detectPath(input);
if (!isClearWinner(found)) {
const list = found.slice(0, 5).map((d) => `${d.platform} (${d.score})`).join(', ');
(0, cliformat_1.fail)(command, `Cannot tell the platform of ${input}${list ? '; candidates: ' + list : ''}. Pass --platform.`);
}
const ev = found[0].evidence[0];
(0, cliformat_1.note)(`platform ${found[0].platform}: ${ev.file}${ev.line ? ':' + ev.line : ''} ${ev.reason}`);
return found[0].platform;
}
/**
* Resolve a directory to its main source file and platform with detect.
* Fails when no platform is clear or no main file stands out.
*/
async function resolveDirectory(command, input, platformArg) {
var _a;
const { isClearWinner } = await Promise.resolve().then(() => __importStar(require('../common/detect')));
const detections = await detectPath(input);
const d = (platformArg && detections.find((x) => x.platform === platformArg)) || detections[0];
if (!d) {
const list = detections.slice(0, 5).map((x) => `${x.platform} (${x.score})`).join(', ');
(0, cliformat_1.fail)(command, `Cannot tell the platform of ${input}${list ? '; candidates: ' + list : ''}. Pass --platform.`);
}
if (!platformArg && !isClearWinner(detections)) {
const list = detections.slice(0, 5).map((x) => `${x.platform} (${x.score})`).join(', ');
(0, cliformat_1.fail)(command, `Cannot tell the platform of ${input}${list ? '; candidates: ' + list : ''}. Pass --platform.`);
}
if (!d.mainFile) {
const n = ((_a = d.mainCandidates) === null || _a === void 0 ? void 0 : _a.length) || 0;
const hint = n > 1 ? ` (${n} programs: ${d.mainCandidates.slice(0, 5).join(', ')}${n > 5 ? ', ...' : ''})` : '';
(0, cliformat_1.fail)(command, `No main file detected in ${input}${hint}. Pass a source file.`);
}
const source = findProjectFile(input, d.mainFile);
if (!source)
(0, cliformat_1.fail)(command, `Main file '${d.mainFile}' not found under ${input}.`);
(0, cliformat_1.note)(`detected ${d.platform}: ${source}`);
return { source, platform: d.platform };
}
/**
* Locate a detection's main file under `root`. A README link names the file
* relative to its own subfolder (the githubURL path), so it may live in a
* subdirectory rather than at the top level.
*/
function findProjectFile(root, rel) {
const direct = path.join(root, rel);
if (fs.existsSync(direct))
return direct;
const suffix = path.sep + rel.split('/').join(path.sep);
const base = path.basename(rel);
let byBase;
const stack = [root];
while (stack.length) {
const dir = stack.pop();
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.name.startsWith('.') || entry.name === 'node_modules')
continue;
const full = path.join(dir, entry.name);
if (entry.isDirectory())
stack.push(full);
else if (entry.name === base) {
if (full.endsWith(suffix))
return full;
byBase = byBase || full;
}
}
}
return byBase;
}
async function doDetect(positional) {
const input = positional[0] || '.';
if (!fs.existsSync(input))
(0, cliformat_1.fail)('detect', `No such file or directory: ${input}`);
const { isClearWinner } = await Promise.resolve().then(() => __importStar(require('../common/detect')));
const detections = await detectPath(input);
(0, cliformat_1.output)({ success: true, command: 'detect', data: { input, clear: isClearWinner(detections), detections: detections.slice(0, 8) } });
}
////////////////////////////////////////////////////////////////////////
// presets
async function openPresetIndex() {
const { findPresetIndex, readPresetIndex } = await Promise.resolve().then(() => __importStar(require('./presets')));
const file = findPresetIndex((0, toolroot_1.toolRoot)(), __dirname);
if (!file)
throw new Error('No examples index (presets.json). In a checkout, run `npm run build` in extension/.');
return readPresetIndex(file, (tool) => { var _a; return (0, toolroot_1.toolchainProvidesTool)(tool, (_a = toolmeta_1.TOOL_META[tool]) === null || _a === void 0 ? void 0 : _a.wasmModule); });
}
/** List the platforms with examples, or the examples for one platform. */
async function doPresets(positional) {
const index = await openPresetIndex();
if (!positional[0]) {
const platforms = index.platforms.map((p) => ({ id: p.id, name: p.name, family: p.family, count: p.templates.length }));
(0, cliformat_1.output)({ success: true, command: 'presets', data: { platforms } });
return;
}
const platform = index.platforms.find((p) => p.id === positional[0].toLowerCase());
if (!platform)
(0, cliformat_1.fail)('presets', `No platform '${positional[0]}'. Run 8bws presets for the list.`);
const templates = platform.templates.map((t) => ({ id: t.id, name: t.name, category: t.category, tool: t.tool,
saveAs: t.saveAs, files: t.files, shared: t.shared }));
(0, cliformat_1.output)({ success: true, command: 'presets', data: { platform: platform.id, name: platform.name, templates } });
}
/** Copy an example, with the files it needs, into a directory to start from. */
async function doNew(args, positional) {
if (!positional[0])
(0, cliformat_1.fail)('new', 'Usage: 8bws new <platform>/<preset> [dir] [--libs] [--force]. Run 8bws presets to list them.');
const { copyTemplate, findTemplate } = await Promise.resolve().then(() => __importStar(require('./presets')));
const { platform, template } = findTemplate(await openPresetIndex(), positional[0]);
const presets = presetsDir();
if (!presets)
(0, cliformat_1.fail)('new', 'No presets directory in the toolchains.');
const stem = path.basename(template.id).replace(/\.[^.]*$/, '');
const dir = positional[1] || (stem === 'skeleton' ? `${platform.id}-project` : stem);
const files = copyTemplate(presets, platform, template, dir, { libraries: !!args['libs'], force: !!args['force'] });
(0, cliformat_1.output)({
success: true, command: 'new',
data: {
preset: `${platform.id}/${template.id}`, platform: platform.id, dir, main: files[0], files,
// libraries other examples use too: a build finds them in the toolchains unless copied
libraries: args['libs'] ? [] : template.shared,
build: `cd ${dir} && 8bws build -p ${platform.id} ${files[0]}`,
},
});
}
////////////////////////////////////////////////////////////////////////
// determinism
/**
* Serve the Debug Adapter Protocol on stdin/stdout. A launch request names
* the program (and platform); flags given here (--tool, --bios, --define)
* apply to every launch.
*/
async function doDap(args) {
(0, cliformat_1.setServerMode)();
const { EmuDebugSession } = await Promise.resolve().then(() => __importStar(require('./dapsession')));
const { LocalDebugBackend } = await Promise.resolve().then(() => __importStar(require('./daplocal')));
const backend = new LocalDebugBackend(async (launch) => {
const launchArgs = Object.assign({}, args);
if (launch.platform)
launchArgs['platform'] = launch.platform;
const prog = await openProgram('dap', launchArgs, launch.program || launch.mainFile);
return { target: prog.target, debugInfo: prog.debugInfo, root: path.dirname(path.resolve(prog.source)) };
});
new EmuDebugSession(backend).start(process.stdin, process.stdout);
await new Promise(resolve => process.stdin.on('end', resolve));
process.exit(0);
}
/** Record a run with random key input, replay it, and check every frame matches. */
async function doVerifyReplay(args, positional) {
var _a, _b, _c;
const input = positional[0];
const platformId = str(args, 'platform') || (input ? detect_1.ROM_PLATFORMS[path.extname(input).toLowerCase()] : null);
if (!platformId)
(0, cliformat_1.fail)('verify-replay', 'Required: verify-replay --platform <id> [rom]');
const target = await openTarget(args, platformId);
if (input) {
if (!fs.existsSync(input))
(0, cliformat_1.fail)('verify-replay', `No such file: ${input}`);
await target.loadROM(new Uint8Array(fs.readFileSync(input)), path.basename(input));
}
if (!target.history)
(0, cliformat_1.fail)('verify-replay', `'${platformId}' cannot save and restore state`);
const frames = parseInt((_a = str(args, 'frames')) !== null && _a !== void 0 ? _a : '') || 60;
const seed = parseInt((_b = str(args, 'seed')) !== null && _b !== void 0 ? _b : '') || 12345;
const keys = ((_c = str(args, 'input')) !== null && _c !== void 0 ? _c : 'keys') !== 'none';
const r = (0, verifyreplay_1.verifyReplay)(target, { frames, keys, seed });
const ok = r.diverged.length === 0;
if (!ok)
(0, cliformat_1.note)(`first divergence at frame ${r.diverged[0]} of ${frames}`);
(0, cliformat_1.output)({
success: ok,
command: 'verify-replay',
error: ok ? undefined : `replay diverged at frame ${r.diverged[0]}`,
data: {
platform: platformId,
rom: input !== null && input !== void 0 ? input : null,
frames,
input: r.input === 'keys' && r.controlStates < 2 ? 'keys (no effect)' : r.input,
controlStates: r.controlStates,
keyEvents: r.keyEvents,
seed: r.input === 'keys' ? seed : null,
granularity: r.granularity,
deterministic: ok,
firstDivergentFrame: ok ? null : r.diverged[0],
divergentFrames: r.diverged,
recordedTo: r.recordedTo,
}
});
}
////////////////////////////////////////////////////////////////////////
// listings & help
async function doList(command) {
const { initialize, listPlatforms, listTools, PLATFORM_PARAMS } = await Promise.resolve().then(() => __importStar(require('./testlib')));
await initialize();
if (command === 'list-tools') {
// an install made from packs leaves some toolchains out (see assetpacks.ts)
const tools = listTools().filter((t) => { var _a; return (0, toolroot_1.toolchainProvidesTool)(t, (_a = toolmeta_1.TOOL_META[t]) === null || _a === void 0 ? void 0 : _a.wasmModule); });
(0, cliformat_1.output)({ success: true, command, data: { tools } });
return;
}
const platforms = {};
for (const p of listPlatforms().filter(toolroot_1.toolchainSupportsPlatform))
platforms[p] = { arch: PLATFORM_PARAMS[p].arch || 'unknown' };
(0, cliformat_1.output)({ success: true, command, data: { platforms, count: Object.keys(platforms).length } });
}
function usage(error) {
(0, cliformat_1.output)({
success: !error,
command: 'help',
error,
data: {
commands: {
'build': 'compile a source file or folder to a ROM',
'run': 'run a ROM -- or a source file or folder, built first',
'presets': 'list the examples: all platforms, or those of one (8bws presets nes)',
'new': 'copy an example and the files it needs into a directory to start from (8bws new nes/hello.c [dir])',
'detect': 'guess the platform and main file of a source file or directory',
'dap': 'serve the Debug Adapter Protocol on stdin/stdout, for editors',
'verify-replay': 'record a run with random key input, replay it, and check every frame matches',
'list-platforms': 'platforms available to --platform',
'list-tools': 'compilers and assemblers available to --tool',
},
options: {
'build options': {
'-p, --platform <id>': 'target platform (default: detected from the source or folder)',
'-t, --tool <tool>': 'compiler/assembler (default: from file extension)',
'-o, --output <file>': 'write the ROM here',
'--check': 'compile without writing anything',
'--symbols': 'dump the symbol table and segments',
'--save': 'save all intermediate build files to a temp dir',
'--no-warnings': 'don\'t print compiler warnings',
'--define <N[=V]>': 'preprocessor define for the compiler (repeatable)',
'--as-define <N[=V]>': 'symbol for the assembler (repeatable)',
'--ld-define <N=INT>': 'linker symbol, integer expression (repeatable)',
'--cflag <arg>': 'extra compiler argument (repeatable)',
'--asflag <arg>': 'extra assembler argument (repeatable)',
'--ldflag <arg>': 'extra linker argument (repeatable)',
},
'run options': {
'-p, --platform <id>': 'platform emulator (Platform interface)',
'-f, --frames <n>': 'advance N frames',
'-e <commands>': 'inline run-script, e.g. -e "run 60; screen"',
'--script <file>': 'run-script file',
'--png <file>': 'write a screenshot of the last frame',
'--frames-dir <dir>': 'write each frame as DIR/frame_NNNNN.png, for ffmpeg',
'--fps <n>': 'frame rate for --frames-dir (default: the platform\'s)',
'--symbols <file>': 'load a .lbl/.sym file for symbolic addresses',
'--bios <file>': 'load a BIOS image',
'--vector-size <px>': 'long side of a vector platform\'s screen (default 512)',
'--info': 'dump debug info and disassembly when done',
'--no-warnings': 'don\'t print compiler warnings',
'--memdump <a,b>': 'hexdump a hex address range',
'--option <k=v>': 'pass a platform option, e.g. --option joyport=1 (repeatable)',
},
'new options': {
'--libs': 'also copy the library files other examples share (default: builds find them in the toolchains)',
'--force': 'overwrite files already in the directory',
},
'verify-replay options': {
'-p, --platform <id>': 'platform emulator',
'-f, --frames <n>': 'frames to record (default 60)',
'--input <keys|none>': 'press random keys while recording (default keys)',
'--seed <n>': 'seed for the random keys',
},
'global options': {
'--json': 'machine-readable output on stdout',
},
},
script: runscript_1.RUN_SCRIPT_HELP,
}
});
process.exit(error ? 1 : 0);
}
////////////////////////////////////////////////////////////////////////
async function main() {
const { command, args, positional } = parseArgs(process.argv);
if (args['json'])
(0, cliformat_1.setJsonMode)(true);
// A platform whose start() never settles (usually one that needs a
// browser-only library) drains the event loop and would otherwise exit 0.
process.on('exit', () => {
// dap answers over its protocol, not with a result
if ((0, cliformat_1.hasOutput)() || command === 'dap')
return;
(0, cliformat_1.output)({ success: false, command, error: `${command} did not run to completion -- the emulator never finished starting` });
process.exitCode = 1;
});
try {
switch (command) {
case 'build':
await doBuild(args, positional);
break;
case 'run':
await doRun(args, positional);
break;
case 'detect':
await doDetect(positional);
break;
case 'presets':
await doPresets(positional);
break;
case 'new':
await doNew(args, positional);
break;
case 'verify-replay':
await doVerifyReplay(args, positional);
break;
case 'dap':
await doDap(args);
break;
case 'list-tools':
case 'list-platforms':
await doList(command);
break;
case 'help':
usage();
break;
default: usage(`Unknown command: ${command}`);
}
}
catch (e) {
if (process.env.DEBUG)
console.error(e);
(0, cliformat_1.output)({ success: false, command, error: e.message || String(e) });
process.exit(1);
}
}
// Exit once the result is out: emulator libraries (Javatari's on-screen
// messages) leave timers behind that would keep node running for seconds.
main().then(() => {
process.stdout.write('', () => process.stderr.write('', () => process.exit()));
});
//# sourceMappingURL=8bws.js.map