romdevtools 0.71.1 → 0.84.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/AGENTS.md +19 -16
  2. package/CHANGELOG.md +184 -0
  3. package/README.md +5 -5
  4. package/examples/README.md +1 -0
  5. package/examples/gametank/templates/gt_draw.h +91 -0
  6. package/examples/gametank/templates/gt_hud.h +89 -0
  7. package/examples/gametank/templates/gt_palette.h +59 -0
  8. package/examples/gametank/templates/gt_sound.h +90 -0
  9. package/examples/gametank/templates/gt_sprites.h +53 -0
  10. package/examples/gametank/templates/platformer.c +318 -0
  11. package/examples/gametank/templates/puzzle.c +295 -0
  12. package/examples/gametank/templates/racing.c +139 -0
  13. package/examples/gametank/templates/shmup.c +277 -0
  14. package/examples/gametank/templates/sports.c +119 -0
  15. package/package.json +24 -22
  16. package/src/cores/capabilities.js +24 -0
  17. package/src/cores/registry.js +7 -1
  18. package/src/host/LibretroHost.js +90 -17
  19. package/src/host/callbacks.js +20 -2
  20. package/src/host/cpu-state.js +17 -0
  21. package/src/host/gametank-acp-state.js +53 -0
  22. package/src/http/skill-doc.js +10 -6
  23. package/src/mcp/tools/audio.js +1 -1
  24. package/src/mcp/tools/cart-parts.js +76 -0
  25. package/src/mcp/tools/lifecycle.js +1 -1
  26. package/src/mcp/tools/platform-tools.js +9 -1
  27. package/src/platforms/gametank/lib/gt/audio/acp_image.bin +0 -0
  28. package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.c +55 -0
  29. package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.h +34 -0
  30. package/src/platforms/gametank/lib/gt/audio/audio_fw.asm +223 -0
  31. package/src/platforms/gametank/lib/gt/audio/audio_fw_blob.s +8 -0
  32. package/src/platforms/gametank/lib/gt/audio/instruments.c +79 -0
  33. package/src/platforms/gametank/lib/gt/audio/instruments.h +25 -0
  34. package/src/platforms/gametank/lib/gt/audio/music.c +370 -0
  35. package/src/platforms/gametank/lib/gt/audio/music.h +35 -0
  36. package/src/platforms/gametank/lib/gt/audio/note_numbers.h +132 -0
  37. package/src/platforms/gametank/lib/gt/audio/scaled_sines.raw +0 -0
  38. package/src/platforms/gametank/lib/gt/audio/sine_256_-63_63.bin +0 -0
  39. package/src/platforms/gametank/lib/gt/banking.c +29 -0
  40. package/src/platforms/gametank/lib/gt/banking.h +8 -0
  41. package/src/platforms/gametank/lib/gt/banking2.s +41 -0
  42. package/src/platforms/gametank/lib/gt/crt0.s +102 -0
  43. package/src/platforms/gametank/lib/gt/draw_logo.s +55 -0
  44. package/src/platforms/gametank/lib/gt/feature/persist/persist.c +103 -0
  45. package/src/platforms/gametank/lib/gt/feature/persist/persist.h +13 -0
  46. package/src/platforms/gametank/lib/gt/feature/random/random.c +40 -0
  47. package/src/platforms/gametank/lib/gt/feature/random/random.h +18 -0
  48. package/src/platforms/gametank/lib/gt/feature/text/text.c +111 -0
  49. package/src/platforms/gametank/lib/gt/feature/text/text.h +25 -0
  50. package/src/platforms/gametank/lib/gt/gametank.c +12 -0
  51. package/src/platforms/gametank/lib/gt/gametank.h +80 -0
  52. package/src/platforms/gametank/lib/gt/gametank_logo.inc +393 -0
  53. package/src/platforms/gametank/lib/gt/gen/assets/assets_index.h +9 -0
  54. package/src/platforms/gametank/lib/gt/gen/assets/sdk_default.h +5 -0
  55. package/src/platforms/gametank/lib/gt/gen/bank_nums.h +11 -0
  56. package/src/platforms/gametank/lib/gt/gen/modules_enabled.h +4 -0
  57. package/src/platforms/gametank/lib/gt/gen/modules_enabled.inc +4 -0
  58. package/src/platforms/gametank/lib/gt/gfx/draw_direct.c +132 -0
  59. package/src/platforms/gametank/lib/gt/gfx/draw_direct.h +75 -0
  60. package/src/platforms/gametank/lib/gt/gfx/draw_queue.c +177 -0
  61. package/src/platforms/gametank/lib/gt/gfx/draw_queue.h +35 -0
  62. package/src/platforms/gametank/lib/gt/gfx/draw_util.s +157 -0
  63. package/src/platforms/gametank/lib/gt/gfx/gfx_sys.c +43 -0
  64. package/src/platforms/gametank/lib/gt/gfx/gfx_sys.h +46 -0
  65. package/src/platforms/gametank/lib/gt/gfx/sprites.c +216 -0
  66. package/src/platforms/gametank/lib/gt/gfx/sprites.h +41 -0
  67. package/src/platforms/gametank/lib/gt/init.c +9 -0
  68. package/src/platforms/gametank/lib/gt/input.c +26 -0
  69. package/src/platforms/gametank/lib/gt/input.h +20 -0
  70. package/src/platforms/gametank/lib/gt/interrupt.s +67 -0
  71. package/src/platforms/gametank/lib/gt/vectors.s +14 -0
  72. package/src/platforms/gametank/lib/gt/wait.s +53 -0
  73. package/src/playtest/playtest.js +174 -26
  74. package/src/toolchains/arm-none-eabi-gcc/gcc.js +39 -188
  75. package/src/toolchains/asar/asar.js +10 -15
  76. package/src/toolchains/cc65/cc65.js +82 -92
  77. package/src/toolchains/cc65/da65.js +12 -17
  78. package/src/toolchains/cc65/preset-resolver.js +13 -2
  79. package/src/toolchains/cc65/presets/gametank/gametank.h +80 -0
  80. package/src/toolchains/cc65/presets/gametank/sdk.cfg +32 -0
  81. package/src/toolchains/cc65/presets/gametank/sdk.crt0.s +61 -0
  82. package/src/toolchains/cc65/presets/gametank/sdk.vectors.s +11 -0
  83. package/src/toolchains/cc65/presets/gametank/single-bank.cfg +28 -0
  84. package/src/toolchains/cc65/presets/gametank/single-bank.crt0.s +71 -0
  85. package/src/toolchains/cc65/presets/gametank/single-bank.vectors.s +12 -0
  86. package/src/toolchains/common/c-build.js +109 -0
  87. package/src/toolchains/common/gcc-toolchain.js +164 -0
  88. package/src/toolchains/common/wasm-tool.js +101 -0
  89. package/src/toolchains/dasm/dasm.js +12 -18
  90. package/src/toolchains/gba-c/gba-c.js +253 -305
  91. package/src/toolchains/genesis-c/genesis-c.js +196 -225
  92. package/src/toolchains/index.js +58 -4
  93. package/src/toolchains/m68k-elf-gcc/gcc.js +39 -202
  94. package/src/toolchains/mips-c/mips-c.js +68 -78
  95. package/src/toolchains/mips-elf-gcc/gcc.js +55 -118
  96. package/src/toolchains/rgbds/rgbds.js +7 -19
  97. package/src/toolchains/sdcc/sdcc.js +35 -26
  98. package/src/toolchains/sh-c/sh-c.js +44 -52
  99. package/src/toolchains/sh-elf-gcc/gcc.js +55 -110
  100. package/src/toolchains/sjasm/sjasm.js +10 -14
  101. package/src/toolchains/snes-c/snes-c.js +125 -150
  102. package/src/toolchains/tcc816/tcc816.js +10 -15
  103. package/src/toolchains/vasm68k/vasm68k.js +11 -16
  104. package/src/toolchains/wladx/wladx.js +5 -17
  105. package/src/toolchains/z80/binutils.js +5 -11
@@ -8,32 +8,20 @@
8
8
  // crash? } in addition to tool-specific fields.
9
9
 
10
10
  import { fileURLToPath } from "node:url";
11
- import { existsSync } from "node:fs";
12
11
  import path from "node:path";
13
12
 
14
13
  import { runIsolated, textFile, binaryFile, getOutputBytes } from "../_worker/run.js";
14
+ import { makeGlueResolver } from "../common/wasm-tool.js";
15
15
 
16
16
  const __filename = fileURLToPath(import.meta.url);
17
17
  const __dirname = path.dirname(__filename);
18
18
 
19
- // RGBDS's WASM ships in romdev-toolchain-rgbds. Resolve each tool's glue
20
- // from that package; fall back to a local copy under src/ if present
21
- // (transition / dev). The package is a hard dep of romdev.
22
- function resolveRgbdsGlue(file) {
23
- try {
24
- const u = import.meta.resolve("romdev-toolchain-rgbds");
25
- const p = path.join(path.dirname(fileURLToPath(u)), "wasm", file);
26
- if (existsSync(p)) return p;
27
- } catch { /* package not resolvable — fall through to local */ }
28
- const local = path.join(__dirname, "wasm", file);
29
- if (existsSync(local)) return local;
30
- throw new Error(`RGBDS WASM (${file}) not found — install romdev-toolchain-rgbds`);
31
- }
32
- // Lazy + memoized per tool: resolve (and possibly throw "not installed") only
33
- // on the first GB/GBC asm build that uses each tool, not at module load — so
34
- // booting the server never touches this package unless RGBDS is actually used.
35
- const _glue = {};
36
- const rgbdsGlue = (file) => (_glue[file] ??= resolveRgbdsGlue(file));
19
+ // RGBDS's WASM ships in romdev-toolchain-rgbds. Resolve each tool's glue from
20
+ // that package; fall back to a local copy under src/ if present (transition /
21
+ // dev). Lazy + memoized per tool: resolve (and possibly throw "not installed")
22
+ // only on the first GB/GBC asm build that uses each tool, not at module load —
23
+ // so booting the server never touches this package unless RGBDS is actually used.
24
+ const rgbdsGlue = makeGlueResolver({ pkg: "romdev-toolchain-rgbds", localDir: __dirname, label: "RGBDS" });
37
25
 
38
26
  /**
39
27
  * Run rgbasm on a source program.
@@ -13,10 +13,17 @@
13
13
  // share/sdcc/{include,lib/<port>} is mounted at /share/sdcc/ at call time.
14
14
 
15
15
  import { fileURLToPath } from "node:url";
16
- import { existsSync } from "node:fs";
17
16
  import path from "node:path";
18
17
  import { readFile } from "node:fs/promises";
19
18
 
19
+ import { resolveToolBaseDir } from "../common/wasm-tool.js";
20
+ // sdcc uses CBuild ONLY as the log accumulator (cb.log) — every one of its stages
21
+ // has custom failure handling (the `[buildZ80C] FAILED on TU` context line +
22
+ // failedTU/compiledOK fields, and sdld's raw-exitCode return) that doesn't fit the
23
+ // generic cb.stage(throw-on-fail) shape, so they stay inline. Same partial-fit call
24
+ // the snes-c migration made for its SDK-runtime sites.
25
+ import { CBuild } from "../common/c-build.js";
26
+
20
27
  const __filename = fileURLToPath(import.meta.url);
21
28
  const __dirname = path.dirname(__filename);
22
29
 
@@ -25,21 +32,19 @@ const __dirname = path.dirname(__filename);
25
32
  // back to a local copy under src/ if present (transition / dev). The package
26
33
  // is a hard dep of romdev. The share/ files are mounted into MEMFS at call
27
34
  // time. (Mirrors the cc65 resolver.)
28
- function resolveSdccBaseDir() {
29
- try {
30
- const u = import.meta.resolve("romdev-toolchain-sdcc");
31
- const dir = path.dirname(fileURLToPath(u));
32
- if (existsSync(path.join(dir, "wasm", "sdcc.js"))) return dir;
33
- } catch { /* package not resolvable — fall through to local */ }
34
- if (existsSync(path.join(__dirname, "wasm", "sdcc.js"))) return __dirname;
35
- throw new Error("SDCC WASM not found — install romdev-toolchain-sdcc");
36
- }
35
+ //
37
36
  // Lazy + memoized: resolve (and possibly throw "not installed") only on the
38
37
  // first SDCC build (GB/GBC/SMS/GG C), not at module load — so booting the
39
38
  // server never touches this package unless SDCC is actually used. Resolve the
40
39
  // base dir once; derive each tool glue + the share dir from it on demand.
41
40
  let _sdccBase;
42
- const sdccBase = () => (_sdccBase ??= resolveSdccBaseDir());
41
+ const sdccBase = () =>
42
+ (_sdccBase ??= resolveToolBaseDir({
43
+ pkg: "romdev-toolchain-sdcc",
44
+ sentinel: "wasm/sdcc.js",
45
+ localDir: __dirname,
46
+ label: "SDCC",
47
+ }));
43
48
  const sdccGlue = (file) => path.join(sdccBase(), "wasm", file);
44
49
  const shareDir = () => path.join(sdccBase(), "share", "sdcc");
45
50
 
@@ -363,7 +368,7 @@ export function ihxToBin(ihx, size, fill = 0xFF) {
363
368
  */
364
369
  export async function buildZ80C(args) {
365
370
  const sources = args.sources ?? { "main.c": args.source };
366
- let log = "";
371
+ const cb = new CBuild();
367
372
  /** @type {Record<string, string>} */
368
373
  const objects = {};
369
374
  // SM83 (Game Boy) uses sdasgb instead of sdasz80 — different instruction set.
@@ -379,10 +384,10 @@ export async function buildZ80C(args) {
379
384
  const asmRun = isSm83 ? runSdasgb : runSdasz80;
380
385
  const asmLabel = isSm83 ? "sdasgb" : "sdasz80";
381
386
  const r = await asmRun({ source: src });
382
- log += `--- ${asmLabel} (${name}) ---\n` + r.log + "\n";
387
+ cb.log += `--- ${asmLabel} (${name}) ---\n` + r.log + "\n";
383
388
  if (r.exitCode !== 0 || !r.rel) {
384
- log += `\n[buildZ80C] FAILED on TU '${name}' (${asmLabel}). Successfully compiled before this: ${compiledOK.length === 0 ? "(none)" : compiledOK.join(", ")}\n`;
385
- return { binary: null, log, exitCode: r.exitCode || 1, stage: asmLabel, failedTU: name, compiledOK: [...compiledOK] };
389
+ cb.log += `\n[buildZ80C] FAILED on TU '${name}' (${asmLabel}). Successfully compiled before this: ${compiledOK.length === 0 ? "(none)" : compiledOK.join(", ")}\n`;
390
+ return { binary: null, log: cb.log, exitCode: r.exitCode || 1, stage: asmLabel, failedTU: name, compiledOK: [...compiledOK] };
386
391
  }
387
392
  compiledOK.push(name);
388
393
  objects[name.replace(/\.(s|asm)$/i, ".rel")] = r.rel;
@@ -393,10 +398,10 @@ export async function buildZ80C(args) {
393
398
  port: args.port,
394
399
  headers: args.headers,
395
400
  });
396
- log += `--- sdcc (${name}) ---\n` + r.log + "\n";
401
+ cb.log += `--- sdcc (${name}) ---\n` + r.log + "\n";
397
402
  if (r.exitCode !== 0 || !r.rel) {
398
- log += `\n[buildZ80C] FAILED on TU '${name}' (sdcc). Successfully compiled before this: ${compiledOK.length === 0 ? "(none)" : compiledOK.join(", ")}\n`;
399
- return { binary: null, log, exitCode: r.exitCode || 1, stage: "sdcc", failedTU: name, compiledOK: [...compiledOK] };
403
+ cb.log += `\n[buildZ80C] FAILED on TU '${name}' (sdcc). Successfully compiled before this: ${compiledOK.length === 0 ? "(none)" : compiledOK.join(", ")}\n`;
404
+ return { binary: null, log: cb.log, exitCode: r.exitCode || 1, stage: "sdcc", failedTU: name, compiledOK: [...compiledOK] };
400
405
  }
401
406
  compiledOK.push(name);
402
407
  objects[name.replace(/\.(c|h)$/i, ".rel")] = r.rel;
@@ -422,16 +427,20 @@ export async function buildZ80C(args) {
422
427
  let crt0Rel = args.crt0;
423
428
  if (crt0Rel && !/^XL[2-4]\b/.test(crt0Rel.trim())) {
424
429
  // Looks like .s source — assemble it via the same path we use for
425
- // user .s sources.
430
+ // user .s sources. (Keeps its custom FAILED-context line, so inline.)
426
431
  const crt0Asm = await (isSm83 ? runSdasgb : runSdasz80)({ source: crt0Rel });
427
- log += `--- ${isSm83 ? "sdasgb" : "sdasz80"} (crt0) ---\n` + crt0Asm.log + "\n";
432
+ cb.log += `--- ${isSm83 ? "sdasgb" : "sdasz80"} (crt0) ---\n` + crt0Asm.log + "\n";
428
433
  if (crt0Asm.exitCode !== 0 || !crt0Asm.rel) {
429
- log += `\n[buildZ80C] FAILED to assemble crt0 source. Successfully compiled before this: ${compiledOK.length === 0 ? "(none)" : compiledOK.join(", ")}\n`;
430
- return { binary: null, log, exitCode: crt0Asm.exitCode || 1, stage: "sdasgb-crt0" };
434
+ cb.log += `\n[buildZ80C] FAILED to assemble crt0 source. Successfully compiled before this: ${compiledOK.length === 0 ? "(none)" : compiledOK.join(", ")}\n`;
435
+ return { binary: null, log: cb.log, exitCode: crt0Asm.exitCode || 1, stage: "sdasgb-crt0" };
431
436
  }
432
437
  crt0Rel = crt0Asm.rel;
433
438
  }
434
439
 
440
+ // sdld is a clean stage (no per-TU context) → cb.stage. NOTE: the failure
441
+ // shape here had NO `exitCode || 1` fallback (raw link.exitCode); CBuild uses
442
+ // `exitCode || 1`. link.exitCode is non-zero on a real link failure, so this
443
+ // is equivalent in practice — but to stay byte-identical, handle it inline.
435
444
  const link = await runSdld({
436
445
  objects,
437
446
  port: args.port,
@@ -440,9 +449,9 @@ export async function buildZ80C(args) {
440
449
  dataLoc: args.dataLoc,
441
450
  crt0: crt0Rel,
442
451
  });
443
- log += "--- sdld ---\n" + link.log;
452
+ cb.log += "--- sdld ---\n" + link.log;
444
453
  if (link.exitCode !== 0 || !link.ihx) {
445
- return { binary: null, log, exitCode: link.exitCode, stage: "sdld" };
454
+ return { binary: null, log: cb.log, exitCode: link.exitCode, stage: "sdld" };
446
455
  }
447
456
  // `romBase` (e.g. MSX $4000) means the cartridge maps at that address: the
448
457
  // ihx writes records at absolute $4000+, so size the buffer to cover them,
@@ -453,7 +462,7 @@ export async function buildZ80C(args) {
453
462
  const span = args.romBase + (args.romSize || 0);
454
463
  const full = ihxToBin(link.ihx, span);
455
464
  const end = args.romSize ? args.romBase + args.romSize : full.length;
456
- return { binary: full.slice(args.romBase, end), log, exitCode: 0, stage: "done", map: link.map ?? null };
465
+ return { binary: full.slice(args.romBase, end), log: cb.log, exitCode: 0, stage: "done", map: link.map ?? null };
457
466
  }
458
467
  // If romSize is null/0, size the buffer to the highest address the ihx
459
468
  // actually wrote (no padding to a ROM size). Used for tape targets like
@@ -466,7 +475,7 @@ export async function buildZ80C(args) {
466
475
  if (!args.romSize && args.codeLoc) {
467
476
  trimmed = binary.slice(args.codeLoc);
468
477
  }
469
- return { binary: trimmed, log, exitCode: 0, stage: "done", map: link.map ?? null };
478
+ return { binary: trimmed, log: cb.log, exitCode: 0, stage: "done", map: link.map ?? null };
470
479
  }
471
480
 
472
481
  /**
@@ -16,6 +16,7 @@ import { fileURLToPath } from "node:url";
16
16
  import path from "node:path";
17
17
 
18
18
  import { runCc1sh, runShAs, runShLd } from "../sh-elf-gcc/gcc.js";
19
+ import { CBuild, BuildError } from "../common/c-build.js";
19
20
 
20
21
  const __filename = fileURLToPath(import.meta.url);
21
22
  const __dirname = path.dirname(__filename);
@@ -45,60 +46,51 @@ export async function buildShC(args) {
45
46
  const hasOpt = userOpts.some((o) => /^-O/.test(o));
46
47
  const cc1Options = [...(hasOpt ? [] : ["-O1"]), ...userOpts, "-ffreestanding", "-fno-builtin", "-Wall"];
47
48
  const sources = args.sources ?? (args.source != null ? { "main.c": args.source } : {});
48
- let log = "";
49
+ const cb = new CBuild();
50
+ const as = (source) => runShAs({ source });
49
51
 
50
- /** @type {Record<string, Uint8Array>} */
51
- const userObjs = {};
52
- for (const cName of Object.keys(sources).filter((n) => /\.c$/i.test(n))) {
53
- const cc = await runCc1sh({ source: sources[cName], headers, options: cc1Options });
54
- log += `--- cc1 (${cName}) ---\n${cc.log || "(ok)"}\n`;
55
- if (cc.exitCode !== 0 || !cc.asmSource)
56
- return { ok: false, binary: null, log, exitCode: cc.exitCode || 1, stage: `cc1 (${cName})`, ...(cc.crash ? { crash: cc.crash } : {}) };
57
- const as = await runShAs({ source: cc.asmSource });
58
- log += `--- as (${cName}) ---\n${as.log || "(ok)"}\n`;
59
- if (as.exitCode !== 0 || !as.object)
60
- return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${cName})`, ...(as.crash ? { crash: as.crash } : {}) };
61
- userObjs[cName.replace(/\.c$/i, ".o")] = as.object;
62
- }
63
- // raw .s sources too
64
- for (const sName of Object.keys(sources).filter((n) => /\.(s|asm)$/i.test(n))) {
65
- const as = await runShAs({ source: sources[sName] });
66
- log += `--- as (${sName}) ---\n${as.log || "(ok)"}\n`;
67
- if (as.exitCode !== 0 || !as.object)
68
- return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${sName})`, ...(as.crash ? { crash: as.crash } : {}) };
69
- userObjs[sName.replace(/\.(s|asm)$/i, ".o")] = as.object;
70
- }
52
+ try {
53
+ /** @type {Record<string, Uint8Array>} */
54
+ const userObjs = {};
55
+ for (const cName of Object.keys(sources).filter((n) => /\.c$/i.test(n))) {
56
+ const cc = await cb.stage(`cc1 (${cName})`, () => runCc1sh({ source: sources[cName], headers, options: cc1Options }), (r) => r.asmSource);
57
+ const ao = await cb.stage(`as (${cName})`, () => as(cc.asmSource), (r) => r.object);
58
+ userObjs[cName.replace(/\.c$/i, ".o")] = ao.object;
59
+ }
60
+ // raw .s sources too
61
+ for (const sName of Object.keys(sources).filter((n) => /\.(s|asm)$/i.test(n))) {
62
+ const ao = await cb.stage(`as (${sName})`, () => as(sources[sName]), (r) => r.object);
63
+ userObjs[sName.replace(/\.(s|asm)$/i, ".o")] = ao.object;
64
+ }
71
65
 
72
- // crt0 (stack + .bss clear + main()).
73
- const crt0Src = await readFile(path.join(LIB, "dc-crt0.s"), "utf-8");
74
- const crt0As = await runShAs({ source: crt0Src });
75
- log += `--- as (dc-crt0.s) ---\n${crt0As.log || "(ok)"}\n`;
76
- if (crt0As.exitCode !== 0 || !crt0As.object)
77
- return { ok: false, binary: null, log, exitCode: crt0As.exitCode || 1, stage: "as (crt0)", ...(crt0As.crash ? { crash: crt0As.crash } : {}) };
66
+ // crt0 (stack + .bss clear + main()).
67
+ const crt0Src = await readFile(path.join(LIB, "dc-crt0.s"), "utf-8");
68
+ const crt0As = await cb.stage("as (dc-crt0.s)", () => as(crt0Src), (r) => r.object);
78
69
 
79
- // link: crt0 + user objects + newlib (libc/libm) + libgcc.
80
- const linkScript = await readFile(path.join(LIB, "dc.ld"), "utf-8");
81
- const [libc, libm, libgcc] = await Promise.all([
82
- readFile(path.join(LIB, "libc.a")),
83
- readFile(path.join(LIB, "libm.a")),
84
- readFile(path.join(LIB, "libgcc.a")),
85
- ]);
86
- const ld = await runShLd({
87
- objects: { "crt0.o": crt0As.object, ...userObjs },
88
- linkScript,
89
- archives: {
90
- "libc.a": new Uint8Array(libc),
91
- "libm.a": new Uint8Array(libm),
92
- "libgcc.a": new Uint8Array(libgcc),
93
- },
94
- libraries: ["c", "m", "gcc"],
95
- libraryPaths: ["/work"],
96
- options: ["--no-warn-rwx-segments"],
97
- });
98
- log += `--- ld ---\n${ld.log || "(ok)"}\n`;
99
- if (ld.exitCode !== 0 || !ld.elf)
100
- return { ok: false, binary: null, log, exitCode: ld.exitCode || 1, stage: "ld", ...(ld.crash ? { crash: ld.crash } : {}) };
70
+ // link: crt0 + user objects + newlib (libc/libm) + libgcc.
71
+ const linkScript = await readFile(path.join(LIB, "dc.ld"), "utf-8");
72
+ const [libc, libm, libgcc] = await Promise.all([
73
+ readFile(path.join(LIB, "libc.a")),
74
+ readFile(path.join(LIB, "libm.a")),
75
+ readFile(path.join(LIB, "libgcc.a")),
76
+ ]);
77
+ const ld = await cb.stage("ld", () => runShLd({
78
+ objects: { "crt0.o": crt0As.object, ...userObjs },
79
+ linkScript,
80
+ archives: {
81
+ "libc.a": new Uint8Array(libc),
82
+ "libm.a": new Uint8Array(libm),
83
+ "libgcc.a": new Uint8Array(libgcc),
84
+ },
85
+ libraries: ["c", "m", "gcc"],
86
+ libraryPaths: ["/work"],
87
+ options: ["--no-warn-rwx-segments"],
88
+ }), (r) => r.elf);
101
89
 
102
- // The ELF IS the deliverable — reios boots it directly.
103
- return { ok: true, binary: ld.elf, log, exitCode: 0, stage: "done", ...(ld.map ? { symbols: ld.map } : {}) };
90
+ // The ELF IS the deliverable — reios boots it directly.
91
+ return { ok: true, binary: ld.elf, log: cb.log, exitCode: 0, stage: "done", ...(ld.map ? { symbols: ld.map } : {}) };
92
+ } catch (e) {
93
+ if (e instanceof BuildError) return e.toResult();
94
+ throw e;
95
+ }
104
96
  }
@@ -1,122 +1,67 @@
1
1
  // sh-elf-gcc — WASM toolchain wrappers for Dreamcast (SH-4) C builds.
2
2
  //
3
- // Pipeline (mirrors mips-elf-gcc/gcc.js — gcc-the-driver can't fork/exec under
4
- // emscripten, so we orchestrate cc1 as ld → objcopy through callMain):
5
- // runCc1sh({source, headers, options}) SH assembly (.s)
6
- // runShAs({source, includes}) → .o ELF object
7
- // runShLd({objects, linkScript, ...}) linked .elf (+ map)
8
- // runShObjcopy({elf}) → raw .bin
3
+ // The full pipeline:
4
+ // runCc1sh({source, headers, options}) SH assembly text (.s)
5
+ // runShAs({source, includes}) .o ELF object
6
+ // runShLd({objects, linkScript}) linked .elf (+ map)
7
+ // runShObjcopy({elf}) raw .bin
9
8
  //
10
- // The Dreamcast SH-4 is little-endian, m4-single-only FP. Single endianness — no
11
- // EL/EB split like MIPS. The staged tool stems keep the createMips* EXPORT_NAMEs
12
- // the build script reused, but the glue filenames are sh-elf-*.
13
-
9
+ // 0.81.0: the 4 stages come from the shared makeGccToolchain() factory
10
+ // (common/gcc-toolchain.js); this file is just the SH-4 config + thin re-exports.
11
+ // SH-4 little-endian, single-precision FP only (-m4-single-only at the cc1 level;
12
+ // --isa=sh4 at as; -EL at ld). NOTE: callers should default cc1 to -O1, not -O2 —
13
+ // the sh-elf cc1.wasm has an -O2-only pass that aborts on common control flow; that
14
+ // default lives in the sh-c builder, not here (this wrapper passes options through).
15
+ //
16
+ // WASM glue ships in romdev-toolchain-sh-gcc; resolution is lazy + memoized.
14
17
  import { fileURLToPath } from "node:url";
15
- import { existsSync } from "node:fs";
16
18
  import path from "node:path";
17
19
 
18
- import { runIsolated, textFile, binaryFile, getOutputBytes, getOutputText } from "../_worker/run.js";
19
-
20
- const __filename = fileURLToPath(import.meta.url);
21
- const __dirname = path.dirname(__filename);
20
+ import { makeGccToolchain } from "../common/gcc-toolchain.js";
22
21
 
23
- // The WASM ships in romdev-toolchain-sh-gcc; fall back to the in-tree dev copy.
24
- function resolveShGlue(file) {
25
- try {
26
- const u = import.meta.resolve("romdev-toolchain-sh-gcc");
27
- const p = path.join(path.dirname(fileURLToPath(u)), "wasm", file);
28
- if (existsSync(p)) return p;
29
- } catch { /* not resolvable — fall through */ }
30
- const local = path.join(__dirname, "wasm", file);
31
- if (existsSync(local)) return local;
32
- throw new Error(`sh-elf-gcc WASM (${file}) not found — build it with scripts/build-sh-wasm-tools.sh`);
33
- }
34
- const _glue = {};
35
- const shGlue = (file) => (_glue[file] ??= resolveShGlue(file));
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
23
 
37
- // SH-4 little-endian, m4-single-only FP. cc1 wants -ml; as wants -little --isa=sh4.
38
- const CC1_ARCH = ["-ml", "-m4-single-only"];
39
- const AS_ARCH = ["-little", "--isa=sh4"];
24
+ const { runCc1, runAs, runLd, runObjcopy } = makeGccToolchain({
25
+ pkg: "romdev-toolchain-sh-gcc",
26
+ localDir: __dirname,
27
+ label: "sh-elf-gcc",
28
+ glue: {
29
+ cc1: "cc1.mjs",
30
+ as: "sh-elf-as.mjs",
31
+ ld: "sh-elf-ld.mjs",
32
+ objcopy: "sh-elf-objcopy.mjs",
33
+ },
34
+ cc1Flags: ["-ml", "-m4-single-only"],
35
+ asFlags: ["-little", "--isa=sh4"],
36
+ ldFlags: ["-EL"],
37
+ ldScriptName: "link.ld",
38
+ outputName: "main.bin",
39
+ });
40
40
 
41
- // ── cc1 — SH gcc C frontend, source → assembly ───────────────────────
42
- export async function runCc1sh(args) {
43
- const { source, options = [] } = args;
44
- const headers = args.headers ?? {};
45
- const inputFiles = [textFile("/work/main.c", source)];
46
- for (const [name, content] of Object.entries(headers)) inputFiles.push(textFile("/work/" + name, content));
47
- const argv = [
48
- ...CC1_ARCH,
49
- "-iquote", "/work", "-I", "/work",
50
- ...options,
51
- "/work/main.c", "-o", "/work/main.s",
52
- ];
53
- const r = await runIsolated({
54
- gluePath: shGlue("cc1.mjs"),
55
- argv, inputFiles,
56
- outputFiles: [{ vfsPath: "/work/main.s", encoding: "utf8" }],
57
- });
58
- return { log: r.log, exitCode: r.exitCode, asmSource: getOutputText(r, "/work/main.s") || null,
59
- ...(r.crash ? { crash: r.crash, stage: "crash" } : {}) };
60
- }
41
+ /**
42
+ * Compile a C source to SH assembly via cc1.
43
+ * @param {{source:string, headers?:Record<string,string>, options?:string[]}} args
44
+ * @returns {Promise<{log:string, exitCode:number, asmSource:string|null, crash?:any}>}
45
+ */
46
+ export const runCc1sh = runCc1;
61
47
 
62
- // ── sh-elf-as — GNU assembler, .s → .o ───────────────────────────────
63
- export async function runShAs(args) {
64
- const { source, options = [] } = args;
65
- const includes = args.includes ?? {};
66
- const binaryIncludes = args.binaryIncludes ?? {};
67
- const inputFiles = [textFile("/work/main.s", source)];
68
- for (const [name, content] of Object.entries(includes)) inputFiles.push(textFile("/work/" + name, content));
69
- for (const [name, bytes] of Object.entries(binaryIncludes)) inputFiles.push(binaryFile("/work/" + name, bytes));
70
- const argv = [...AS_ARCH, "-I", "/work", ...options, "/work/main.s", "-o", "/work/main.o"];
71
- const r = await runIsolated({
72
- gluePath: shGlue("sh-elf-as.mjs"),
73
- argv, inputFiles,
74
- outputFiles: [{ vfsPath: "/work/main.o", encoding: "base64" }],
75
- });
76
- return { log: r.log, exitCode: r.exitCode, object: getOutputBytes(r, "/work/main.o"),
77
- ...(r.crash ? { crash: r.crash, stage: "crash" } : {}) };
78
- }
48
+ /**
49
+ * Assemble SH assembly with sh-elf-as.
50
+ * @param {{source:string, includes?:Record<string,string>, binaryIncludes?:Record<string,Uint8Array>, options?:string[]}} args
51
+ * @returns {Promise<{log:string, exitCode:number, object:Uint8Array|null, crash?:any}>}
52
+ */
53
+ export const runShAs = runAs;
79
54
 
80
- // ── sh-elf-ld — GNU linker, .o + linker script → .elf ────────────────
81
- export async function runShLd(args) {
82
- const { objects, linkScript, libraries = [], libraryPaths = [], options = [] } = args;
83
- const archives = args.archives ?? {};
84
- const inputFiles = [textFile("/work/link.ld", linkScript)];
85
- for (const [name, bytes] of Object.entries(objects)) inputFiles.push(binaryFile("/work/" + name, bytes));
86
- for (const [name, bytes] of Object.entries(archives)) inputFiles.push(binaryFile("/work/" + name, bytes));
87
- const argv = [
88
- "-EL",
89
- "-T", "/work/link.ld",
90
- "-o", "/work/main.elf",
91
- "-Map=/work/main.map",
92
- ...libraryPaths.flatMap((p) => ["-L", p]),
93
- ...Object.keys(objects).map((n) => "/work/" + n),
94
- ...libraries.map((l) => `-l${l}`),
95
- ...options,
96
- ];
97
- const r = await runIsolated({
98
- gluePath: shGlue("sh-elf-ld.mjs"),
99
- argv, inputFiles,
100
- outputFiles: [
101
- { vfsPath: "/work/main.elf", encoding: "base64" },
102
- { vfsPath: "/work/main.map", encoding: "utf8" },
103
- ],
104
- });
105
- return { log: r.log, exitCode: r.exitCode, elf: getOutputBytes(r, "/work/main.elf"),
106
- map: getOutputText(r, "/work/main.map") || null,
107
- ...(r.crash ? { crash: r.crash, stage: "crash" } : {}) };
108
- }
55
+ /**
56
+ * Link SH object files into an ELF executable (+ linker map).
57
+ * @param {{objects:Record<string,Uint8Array>, linkScript:string, libraries?:string[], libraryPaths?:string[], archives?:Record<string,Uint8Array>, options?:string[]}} args
58
+ * @returns {Promise<{log:string, exitCode:number, elf:Uint8Array|null, map:string|null, crash?:any}>}
59
+ */
60
+ export const runShLd = runLd;
109
61
 
110
- // ── sh-elf-objcopy — ELF → raw .bin ──────────────────────────────────
111
- export async function runShObjcopy(args) {
112
- const { elf, options = [] } = args;
113
- const inputFiles = [binaryFile("/work/main.elf", elf)];
114
- const argv = ["-O", "binary", ...options, "/work/main.elf", "/work/main.bin"];
115
- const r = await runIsolated({
116
- gluePath: shGlue("sh-elf-objcopy.mjs"),
117
- argv, inputFiles,
118
- outputFiles: [{ vfsPath: "/work/main.bin", encoding: "base64" }],
119
- });
120
- return { log: r.log, exitCode: r.exitCode, binary: getOutputBytes(r, "/work/main.bin"),
121
- ...(r.crash ? { crash: r.crash, stage: "crash" } : {}) };
122
- }
62
+ /**
63
+ * Strip an ELF down to a raw .bin.
64
+ * @param {{elf:Uint8Array, options?:string[]}} args
65
+ * @returns {Promise<{log:string, exitCode:number, binary:Uint8Array|null, crash?:any}>}
66
+ */
67
+ export const runShObjcopy = runObjcopy;
@@ -5,25 +5,21 @@
5
5
  // #include that generated .h. Built by scripts/build-sjasm.sh.
6
6
 
7
7
  import { fileURLToPath } from "node:url";
8
- import { existsSync } from "node:fs";
9
8
  import path from "node:path";
10
9
 
10
+ import { makeGlueResolver } from "../common/wasm-tool.js";
11
+
11
12
  const __filename = fileURLToPath(import.meta.url);
12
13
  const __dirname = path.dirname(__filename);
13
14
 
14
- function resolveGlue(name) {
15
- // sjasm + bintos ship in romdev-toolchain-m68k-gcc (the Genesis toolchain
16
- // package same place as the m68k compiler they pair with). Resolve from
17
- // there; fall back to a local copy under src/ for dev.
18
- try {
19
- const u = import.meta.resolve("romdev-toolchain-m68k-gcc");
20
- const p = path.join(path.dirname(fileURLToPath(u)), "wasm", name);
21
- if (existsSync(p)) return p;
22
- } catch { /* package not resolvable — fall through to local */ }
23
- const local = path.join(__dirname, "wasm", name);
24
- if (existsSync(local)) return local;
25
- throw new Error(`sjasm WASM not found: ${name} — install romdev-toolchain-m68k-gcc (or run scripts/build-sjasm.sh)`);
26
- }
15
+ // sjasm + bintos ship in romdev-toolchain-m68k-gcc (the Genesis toolchain
16
+ // package same place as the m68k compiler they pair with); a local src/ copy
17
+ // is the dev fallback. Lazy + memoized: resolve only on first use.
18
+ const resolveGlue = makeGlueResolver({
19
+ pkg: "romdev-toolchain-m68k-gcc",
20
+ localDir: __dirname,
21
+ label: "sjasm",
22
+ });
27
23
 
28
24
  let _sjasmFactory, _bintosFactory;
29
25
  async function loadSjasm() {