romdevtools 0.71.1 → 0.84.0

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 (110) hide show
  1. package/AGENTS.md +19 -16
  2. package/CHANGELOG.md +175 -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 +23 -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/playtest/resampler/build.sh +19 -0
  75. package/src/playtest/resampler/index.mjs +75 -0
  76. package/src/playtest/resampler/resampler.c +129 -0
  77. package/src/playtest/resampler/resampler.mjs +2 -0
  78. package/src/playtest/resampler/resampler.wasm +0 -0
  79. package/src/toolchains/arm-none-eabi-gcc/gcc.js +39 -188
  80. package/src/toolchains/asar/asar.js +10 -15
  81. package/src/toolchains/cc65/cc65.js +82 -92
  82. package/src/toolchains/cc65/da65.js +12 -17
  83. package/src/toolchains/cc65/preset-resolver.js +13 -2
  84. package/src/toolchains/cc65/presets/gametank/gametank.h +80 -0
  85. package/src/toolchains/cc65/presets/gametank/sdk.cfg +32 -0
  86. package/src/toolchains/cc65/presets/gametank/sdk.crt0.s +61 -0
  87. package/src/toolchains/cc65/presets/gametank/sdk.vectors.s +11 -0
  88. package/src/toolchains/cc65/presets/gametank/single-bank.cfg +28 -0
  89. package/src/toolchains/cc65/presets/gametank/single-bank.crt0.s +71 -0
  90. package/src/toolchains/cc65/presets/gametank/single-bank.vectors.s +12 -0
  91. package/src/toolchains/common/c-build.js +109 -0
  92. package/src/toolchains/common/gcc-toolchain.js +164 -0
  93. package/src/toolchains/common/wasm-tool.js +101 -0
  94. package/src/toolchains/dasm/dasm.js +12 -18
  95. package/src/toolchains/gba-c/gba-c.js +253 -305
  96. package/src/toolchains/genesis-c/genesis-c.js +196 -225
  97. package/src/toolchains/index.js +58 -4
  98. package/src/toolchains/m68k-elf-gcc/gcc.js +39 -202
  99. package/src/toolchains/mips-c/mips-c.js +68 -78
  100. package/src/toolchains/mips-elf-gcc/gcc.js +55 -118
  101. package/src/toolchains/rgbds/rgbds.js +7 -19
  102. package/src/toolchains/sdcc/sdcc.js +35 -26
  103. package/src/toolchains/sh-c/sh-c.js +44 -52
  104. package/src/toolchains/sh-elf-gcc/gcc.js +55 -110
  105. package/src/toolchains/sjasm/sjasm.js +10 -14
  106. package/src/toolchains/snes-c/snes-c.js +125 -150
  107. package/src/toolchains/tcc816/tcc816.js +10 -15
  108. package/src/toolchains/vasm68k/vasm68k.js +11 -16
  109. package/src/toolchains/wladx/wladx.js +5 -17
  110. package/src/toolchains/z80/binutils.js +5 -11
@@ -24,6 +24,7 @@ import { readFile } from "node:fs/promises";
24
24
 
25
25
  import { runTcc816 } from "../tcc816/tcc816.js";
26
26
  import { runWla65816, runWlalink } from "../wladx/wladx.js";
27
+ import { CBuild, BuildError } from "../common/c-build.js";
27
28
 
28
29
  const __filename = fileURLToPath(import.meta.url);
29
30
  const __dirname = path.dirname(__filename);
@@ -171,7 +172,7 @@ function normalizeSnesSources(args) {
171
172
  * substitution fires. (We do that in wla-65816.js unconditionally.)
172
173
  */
173
174
  async function buildWithPvSnesLib({ sources, headers, tccOptions, wlaOptions, binaryIncludes = {} }) {
174
- let log = "";
175
+ const cb = new CBuild();
175
176
  const pvsnesHeaders = await loadPvSnesLibHeaders();
176
177
  const pvsnesHdr = await readFile(path.join(PVSNESLIB_DIR, "include", "hdr.asm"), "utf-8");
177
178
  // wla can `.include "<sibling>.asm"` from same dir, so all sources need
@@ -186,102 +187,91 @@ async function buildWithPvSnesLib({ sources, headers, tccOptions, wlaOptions, bi
186
187
  // map. tcc only looks in /work (we use `-I /work`) so a flat namespace works.
187
188
  const tccHeaders = { ...pvsnesHeaders, ...headers };
188
189
 
189
- /** @type {Record<string, Uint8Array>} */
190
- const userObjs = {};
191
- // ── Stage 1: build each .c via tcc → .asm → .obj ────────────────
192
- const cFiles = Object.keys(sources).filter((n) => /\.c$/i.test(n));
193
- for (const cName of cFiles) {
194
- const tcc = await runTcc816({
195
- source: sources[cName],
196
- headers: tccHeaders,
197
- options: tccOptions,
198
- });
199
- log += `--- tcc-65816 (${cName}) ---\n` + (tcc.log || "(ok)") + "\n";
200
- if (tcc.exitCode !== 0 || !tcc.asmSource) {
201
- return { ok: false, binary: null, log, exitCode: tcc.exitCode || 1, stage: `tcc-65816 (${cName})`, ...(tcc.crash ? { crash: tcc.crash } : {}) };
190
+ try {
191
+ /** @type {Record<string, Uint8Array>} */
192
+ const userObjs = {};
193
+ // ── Stage 1: build each .c via tcc → .asm .obj ────────────────
194
+ const cFiles = Object.keys(sources).filter((n) => /\.c$/i.test(n));
195
+ for (const cName of cFiles) {
196
+ const tcc = await cb.stage(`tcc-65816 (${cName})`, () => runTcc816({
197
+ source: sources[cName],
198
+ headers: tccHeaders,
199
+ options: tccOptions,
200
+ }), (r) => r.asmSource);
201
+ // KEEP tcc's `.include "hdr.asm"` wla resolves it via the includes
202
+ // map below. (We strip it from library .obj builds where libc.asm
203
+ // already includes hdr.asm; user code includes hdr.asm directly.)
204
+ const wla = await cb.stage(`wla-65816 (${cName})`, () => runWla65816({
205
+ source: tcc.asmSource,
206
+ includes: { "hdr.asm": pvsnesHdr, ...asmSiblings },
207
+ options: wlaOptions,
208
+ }), (r) => r.object, { logName: `wla-65816 (${cName} → .obj)` });
209
+ const objName = cName.replace(/\.c$/i, ".obj");
210
+ userObjs[objName] = wla.object;
202
211
  }
203
- // KEEP tcc's `.include "hdr.asm"` — wla resolves it via the includes
204
- // map below. (We strip it from library .obj builds where libc.asm
205
- // already includes hdr.asm; user code includes hdr.asm directly.)
206
- const wla = await runWla65816({
207
- source: tcc.asmSource,
208
- includes: { "hdr.asm": pvsnesHdr, ...asmSiblings },
209
- options: wlaOptions,
210
- });
211
- log += `--- wla-65816 (${cName} → .obj) ---\n` + (wla.log || "(ok)") + "\n";
212
- if (wla.exitCode !== 0 || !wla.object) {
213
- return { ok: false, binary: null, log, exitCode: wla.exitCode || 1, stage: `wla-65816 (${cName})`, ...(wla.crash ? { crash: wla.crash } : {}) };
212
+
213
+ // ── Stage 2: assemble each user .asm / .s sibling directly ──────
214
+ for (const asmName of Object.keys(asmSiblings)) {
215
+ const wla = await cb.stage(`wla-65816 (${asmName})`, () => runWla65816({
216
+ source: asmSiblings[asmName],
217
+ includes: { "hdr.asm": pvsnesHdr, ...asmSiblings },
218
+ binaryIncludes,
219
+ options: wlaOptions,
220
+ }), (r) => r.object, { logName: `wla-65816 (${asmName} → .obj)` });
221
+ const objName = asmName.replace(/\.(asm|s)$/i, ".obj");
222
+ userObjs[objName] = wla.object;
214
223
  }
215
- const objName = cName.replace(/\.c$/i, ".obj");
216
- userObjs[objName] = wla.object;
217
- }
218
224
 
219
- // ── Stage 2: assemble each user .asm / .s sibling directly ──────
220
- for (const asmName of Object.keys(asmSiblings)) {
221
- const wla = await runWla65816({
222
- source: asmSiblings[asmName],
223
- includes: { "hdr.asm": pvsnesHdr, ...asmSiblings },
224
- binaryIncludes,
225
- options: wlaOptions,
226
- });
227
- log += `--- wla-65816 (${asmName} → .obj) ---\n` + (wla.log || "(ok)") + "\n";
228
- if (wla.exitCode !== 0 || !wla.object) {
229
- return { ok: false, binary: null, log, exitCode: wla.exitCode || 1, stage: `wla-65816 (${asmName})`, ...(wla.crash ? { crash: wla.crash } : {}) };
225
+ // ── Stage 3: assemble PVSnesLib runtime FROM SOURCE, then link ──
226
+ const pvObjs = await assemblePvSnesLibObjs();
227
+ if (!pvObjs.ok) {
228
+ return { ok: false, binary: null, log: cb.log + (pvObjs.log || ""), exitCode: 1, stage: `pvsneslib runtime: ${pvObjs.stage}`, runtime: "pvsneslib" };
230
229
  }
231
- const objName = asmName.replace(/\.(asm|s)$/i, ".obj");
232
- userObjs[objName] = wla.object;
233
- }
230
+ const crt0Obj = pvObjs.objs["crt0_snes.obj"];
231
+ const libmObj = pvObjs.objs["libm.obj"];
232
+ const libtccObj = pvObjs.objs["libtcc.obj"];
233
+ const libcObj = pvObjs.objs["libc.obj"];
234
234
 
235
- // ── Stage 3: assemble PVSnesLib runtime FROM SOURCE, then link ──
236
- const pvObjs = await assemblePvSnesLibObjs();
237
- if (!pvObjs.ok) {
238
- return { ok: false, binary: null, log: log + (pvObjs.log || ""), exitCode: 1, stage: `pvsneslib runtime: ${pvObjs.stage}`, runtime: "pvsneslib" };
239
- }
240
- const crt0Obj = pvObjs.objs["crt0_snes.obj"];
241
- const libmObj = pvObjs.objs["libm.obj"];
242
- const libtccObj = pvObjs.objs["libtcc.obj"];
243
- const libcObj = pvObjs.objs["libc.obj"];
235
+ // PVSnesLib's linkfile convention puts crt0 first (reset-vector tie-break),
236
+ // then libm/libtcc/libc, then user code. wlalink's order matters for which
237
+ // section "wins" when sections collide.
238
+ const userObjLines = Object.keys(userObjs).map((n) => `/work/${n}`).join("\n");
239
+ const linkfile =
240
+ "[objects]\n" +
241
+ "/work/crt0_snes.obj\n" +
242
+ "/work/libm.obj\n" +
243
+ "/work/libtcc.obj\n" +
244
+ "/work/libc.obj\n" +
245
+ userObjLines + "\n";
244
246
 
245
- // PVSnesLib's linkfile convention puts crt0 first (reset-vector tie-break),
246
- // then libm/libtcc/libc, then user code. wlalink's order matters for which
247
- // section "wins" when sections collide.
248
- const userObjLines = Object.keys(userObjs).map((n) => `/work/${n}`).join("\n");
249
- const linkfile =
250
- "[objects]\n" +
251
- "/work/crt0_snes.obj\n" +
252
- "/work/libm.obj\n" +
253
- "/work/libtcc.obj\n" +
254
- "/work/libc.obj\n" +
255
- userObjLines + "\n";
247
+ const link = await cb.stage("wlalink", () => runWlalink({
248
+ objects: {
249
+ "crt0_snes.obj": crt0Obj,
250
+ "libm.obj": libmObj,
251
+ "libtcc.obj": libtccObj,
252
+ "libc.obj": libcObj,
253
+ ...userObjs,
254
+ },
255
+ linkfile,
256
+ // -d: disable label-arith opts (PVSnesLib's library .obj files were
257
+ // built with -d so user link must match). -A: cart-size check.
258
+ // -c: allow duplicate labels (PVSnesLib's libc has known dupes between
259
+ // consoles/input regs that they ship with -c). -b: program output.
260
+ options: ["-d", "-A", "-c", "-b"],
261
+ }), (r) => r.binary);
256
262
 
257
- const link = await runWlalink({
258
- objects: {
259
- "crt0_snes.obj": crt0Obj,
260
- "libm.obj": libmObj,
261
- "libtcc.obj": libtccObj,
262
- "libc.obj": libcObj,
263
- ...userObjs,
264
- },
265
- linkfile,
266
- // -d: disable label-arith opts (PVSnesLib's library .obj files were
267
- // built with -d so user link must match). -A: cart-size check.
268
- // -c: allow duplicate labels (PVSnesLib's libc has known dupes between
269
- // consoles/input regs that they ship with -c). -b: program output.
270
- options: ["-d", "-A", "-c", "-b"],
271
- });
272
- log += "--- wlalink ---\n" + (link.log || "(ok)") + "\n";
273
- if (link.exitCode !== 0 || !link.binary) {
274
- return { ok: false, binary: null, log, exitCode: link.exitCode || 1, stage: "wlalink", ...(link.crash ? { crash: link.crash } : {}) };
263
+ return {
264
+ ok: true,
265
+ binary: link.binary,
266
+ log: cb.log,
267
+ exitCode: 0,
268
+ stage: "done",
269
+ runtime: "pvsneslib",
270
+ };
271
+ } catch (e) {
272
+ if (e instanceof BuildError) return e.toResult();
273
+ throw e;
275
274
  }
276
-
277
- return {
278
- ok: true,
279
- binary: link.binary,
280
- log,
281
- exitCode: 0,
282
- stage: "done",
283
- runtime: "pvsneslib",
284
- };
285
275
  }
286
276
 
287
277
  /**
@@ -289,7 +279,7 @@ async function buildWithPvSnesLib({ sources, headers, tccOptions, wlaOptions, bi
289
279
  * bundled original crt0.asm + hdr.asm support a bare `int main()`.
290
280
  */
291
281
  async function buildMinimal({ sources, headers, tccOptions, wlaOptions, _binaryIncludes = {} }) {
292
- let log = "";
282
+ const cb = new CBuild();
293
283
  const hdrAsm = await readFile(path.join(MINIMAL_LIB_DIR, "hdr.asm"), "utf-8");
294
284
  const crt0Asm = await readFile(path.join(MINIMAL_LIB_DIR, "crt0.asm"), "utf-8");
295
285
 
@@ -300,73 +290,58 @@ async function buildMinimal({ sources, headers, tccOptions, wlaOptions, _binaryI
300
290
  if (/\.(asm|s)$/i.test(name)) asmSiblings[name] = contents;
301
291
  }
302
292
 
303
- /** @type {Record<string, Uint8Array>} */
304
- const userObjs = {};
305
- const cFiles = Object.keys(sources).filter((n) => /\.c$/i.test(n));
306
- for (const cName of cFiles) {
307
- const tcc = await runTcc816({ source: sources[cName], headers, options: tccOptions });
308
- log += `--- tcc-65816 (${cName}) ---\n` + (tcc.log || "(ok)") + "\n";
309
- if (tcc.exitCode !== 0 || !tcc.asmSource) {
310
- return { ok: false, binary: null, log, exitCode: tcc.exitCode || 1, stage: `tcc-65816 (${cName})`, ...(tcc.crash ? { crash: tcc.crash } : {}) };
293
+ try {
294
+ /** @type {Record<string, Uint8Array>} */
295
+ const userObjs = {};
296
+ const cFiles = Object.keys(sources).filter((n) => /\.c$/i.test(n));
297
+ for (const cName of cFiles) {
298
+ const tcc = await cb.stage(`tcc-65816 (${cName})`, () => runTcc816({ source: sources[cName], headers, options: tccOptions }), (r) => r.asmSource);
299
+ const wla = await cb.stage(`wla-65816 (${cName})`, () => runWla65816({
300
+ source: tcc.asmSource,
301
+ includes: { "hdr.asm": hdrAsm, ...asmSiblings },
302
+ options: wlaOptions,
303
+ }), (r) => r.object, { logName: `wla-65816 (${cName} → .obj)` });
304
+ const objName = cName.replace(/\.c$/i, ".o");
305
+ userObjs[objName] = wla.object;
311
306
  }
312
- const wla = await runWla65816({
313
- source: tcc.asmSource,
314
- includes: { "hdr.asm": hdrAsm, ...asmSiblings },
315
- options: wlaOptions,
316
- });
317
- log += `--- wla-65816 (${cName} → .obj) ---\n` + (wla.log || "(ok)") + "\n";
318
- if (wla.exitCode !== 0 || !wla.object) {
319
- return { ok: false, binary: null, log, exitCode: wla.exitCode || 1, stage: `wla-65816 (${cName})`, ...(wla.crash ? { crash: wla.crash } : {}) };
307
+
308
+ for (const asmName of Object.keys(asmSiblings)) {
309
+ const wla = await cb.stage(`wla-65816 (${asmName})`, () => runWla65816({
310
+ source: asmSiblings[asmName],
311
+ includes: { "hdr.asm": hdrAsm, ...asmSiblings },
312
+ options: wlaOptions,
313
+ }), (r) => r.object, { logName: `wla-65816 (${asmName} → .obj)` });
314
+ const objName = asmName.replace(/\.(asm|s)$/i, ".o");
315
+ userObjs[objName] = wla.object;
320
316
  }
321
- const objName = cName.replace(/\.c$/i, ".o");
322
- userObjs[objName] = wla.object;
323
- }
324
317
 
325
- for (const asmName of Object.keys(asmSiblings)) {
326
- const wla = await runWla65816({
327
- source: asmSiblings[asmName],
328
- includes: { "hdr.asm": hdrAsm, ...asmSiblings },
318
+ // crt0 from our minimum-viable runtime.
319
+ const wlaCrt0 = await cb.stage("wla-65816 (crt0)", () => runWla65816({
320
+ source: crt0Asm,
321
+ includes: { "hdr.asm": hdrAsm },
329
322
  options: wlaOptions,
330
- });
331
- log += `--- wla-65816 (${asmName} → .obj) ---\n` + (wla.log || "(ok)") + "\n";
332
- if (wla.exitCode !== 0 || !wla.object) {
333
- return { ok: false, binary: null, log, exitCode: wla.exitCode || 1, stage: `wla-65816 (${asmName})`, ...(wla.crash ? { crash: wla.crash } : {}) };
334
- }
335
- const objName = asmName.replace(/\.(asm|s)$/i, ".o");
336
- userObjs[objName] = wla.object;
337
- }
323
+ }), (r) => r.object, { logName: "wla-65816 (crt0.asm)" });
338
324
 
339
- // crt0 from our minimum-viable runtime.
340
- const wlaCrt0 = await runWla65816({
341
- source: crt0Asm,
342
- includes: { "hdr.asm": hdrAsm },
343
- options: wlaOptions,
344
- });
345
- log += "--- wla-65816 (crt0.asm) ---\n" + (wlaCrt0.log || "(ok)") + "\n";
346
- if (wlaCrt0.exitCode !== 0 || !wlaCrt0.object) {
347
- return { ok: false, binary: null, log, exitCode: wlaCrt0.exitCode || 1, stage: "wla-65816 (crt0)", ...(wlaCrt0.crash ? { crash: wlaCrt0.crash } : {}) };
348
- }
325
+ const userObjLines = Object.keys(userObjs).map((n) => `/work/${n}`).join("\n");
326
+ const linkfile = "[objects]\n/work/crt0.o\n" + userObjLines + "\n";
327
+ const link = await cb.stage("wlalink", () => runWlalink({
328
+ objects: { "crt0.o": wlaCrt0.object, ...userObjs },
329
+ linkfile,
330
+ options: ["-d", "-b"],
331
+ }), (r) => r.binary);
349
332
 
350
- const userObjLines = Object.keys(userObjs).map((n) => `/work/${n}`).join("\n");
351
- const linkfile = "[objects]\n/work/crt0.o\n" + userObjLines + "\n";
352
- const link = await runWlalink({
353
- objects: { "crt0.o": wlaCrt0.object, ...userObjs },
354
- linkfile,
355
- options: ["-d", "-b"],
356
- });
357
- log += "--- wlalink ---\n" + (link.log || "(ok)") + "\n";
358
- if (link.exitCode !== 0 || !link.binary) {
359
- return { ok: false, binary: null, log, exitCode: link.exitCode || 1, stage: "wlalink", ...(link.crash ? { crash: link.crash } : {}) };
333
+ return {
334
+ ok: true,
335
+ binary: link.binary,
336
+ log: cb.log,
337
+ exitCode: 0,
338
+ stage: "done",
339
+ runtime: "minimal",
340
+ };
341
+ } catch (e) {
342
+ if (e instanceof BuildError) return e.toResult();
343
+ throw e;
360
344
  }
361
-
362
- return {
363
- ok: true,
364
- binary: link.binary,
365
- log,
366
- exitCode: 0,
367
- stage: "done",
368
- runtime: "minimal",
369
- };
370
345
  }
371
346
 
372
347
  /**
@@ -11,30 +11,25 @@
11
11
  // NOT wired into buildSource yet.
12
12
 
13
13
  import { fileURLToPath } from "node:url";
14
- import { existsSync } from "node:fs";
15
14
  import path from "node:path";
16
15
 
17
16
  import { runIsolated, textFile, getOutputText } from "../_worker/run.js";
17
+ import { resolveGlueFile } from "../common/wasm-tool.js";
18
18
 
19
19
  const __filename = fileURLToPath(import.meta.url);
20
20
  const __dirname = path.dirname(__filename);
21
21
 
22
22
  // tcc816's WASM ships in romdev-platform-snes (the SNES platform is the only
23
- // consumer). Resolve from that package; fall back to a local copy under src/
24
- // if present (transition / dev). The package is a hard dep of romdev.
25
- function resolveTcc816Glue() {
26
- try {
27
- const u = import.meta.resolve("romdev-platform-snes");
28
- const p = path.join(path.dirname(fileURLToPath(u)), "wasm", "tcc816.js");
29
- if (existsSync(p)) return p;
30
- } catch { /* package not resolvable — fall through to local */ }
31
- const local = path.join(__dirname, "wasm", "tcc816.js");
32
- if (existsSync(local)) return local;
33
- throw new Error("tcc816 WASM not found — install romdev-platform-snes");
34
- }
35
- // Lazy + memoized: resolve only on the first tcc816 (SNES C) build, not at boot.
23
+ // consumer); a local src/ copy is the dev fallback. Lazy + memoized: resolve
24
+ // only on the first tcc816 (SNES C) build, not at boot.
36
25
  let _gluePath;
37
- const gluePath = () => (_gluePath ??= resolveTcc816Glue());
26
+ const gluePath = () =>
27
+ (_gluePath ??= resolveGlueFile({
28
+ pkg: "romdev-platform-snes",
29
+ file: "tcc816.js",
30
+ localDir: __dirname,
31
+ label: "tcc816",
32
+ }));
38
33
 
39
34
  /**
40
35
  * Compile a C source to 65816 assembly via tcc-65816.
@@ -1,30 +1,25 @@
1
1
  // vasm68k_mot — bundled m68k assembler with Motorola syntax (Genesis dev).
2
2
 
3
3
  import { fileURLToPath } from "node:url";
4
- import { existsSync } from "node:fs";
5
4
  import path from "node:path";
6
5
 
7
6
  import { runIsolated, textFile, binaryFile, getOutputBytes } from "../_worker/run.js";
7
+ import { resolveGlueFile } from "../common/wasm-tool.js";
8
8
 
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = path.dirname(__filename);
11
11
 
12
- // vasm68k's WASM ships in romdev-toolchain-vasm. Resolve from that package;
13
- // fall back to a local copy under src/ if present (transition / dev). The
14
- // package is a hard dep of romdev.
15
- function resolveVasm68kGlue() {
16
- try {
17
- const u = import.meta.resolve("romdev-toolchain-vasm");
18
- const p = path.join(path.dirname(fileURLToPath(u)), "wasm", "vasm68k.js");
19
- if (existsSync(p)) return p;
20
- } catch { /* package not resolvable — fall through to local */ }
21
- const local = path.join(__dirname, "wasm", "vasm68k.js");
22
- if (existsSync(local)) return local;
23
- throw new Error("vasm68k WASM not found — install romdev-toolchain-vasm");
24
- }
25
- // Lazy + memoized: resolve only on the first vasm (Genesis asm) build, not at boot.
12
+ // vasm68k's WASM ships in romdev-toolchain-vasm; a local src/ copy is the dev
13
+ // fallback. Lazy + memoized: resolve only on the first vasm (Genesis asm)
14
+ // build, not at boot.
26
15
  let _gluePath;
27
- const gluePath = () => (_gluePath ??= resolveVasm68kGlue());
16
+ const gluePath = () =>
17
+ (_gluePath ??= resolveGlueFile({
18
+ pkg: "romdev-toolchain-vasm",
19
+ file: "vasm68k.js",
20
+ localDir: __dirname,
21
+ label: "vasm68k",
22
+ }));
28
23
 
29
24
  /**
30
25
  * Static analyzer for known Genesis landmines. Mirrors asar preflight
@@ -12,31 +12,19 @@
12
12
  // See `src/toolchains/index.js` SNES dispatch.
13
13
 
14
14
  import { fileURLToPath } from "node:url";
15
- import { existsSync } from "node:fs";
16
15
  import path from "node:path";
17
16
 
18
17
  import { runIsolated, textFile, binaryFile, getOutputBytes } from "../_worker/run.js";
18
+ import { makeGlueResolver } from "../common/wasm-tool.js";
19
19
 
20
20
  const __filename = fileURLToPath(import.meta.url);
21
21
  const __dirname = path.dirname(__filename);
22
22
 
23
23
  // wla-dx's WASM ships in romdev-platform-snes (the SNES platform is the only
24
- // consumer). Resolve from that package; fall back to a local copy under src/
25
- // if present (transition / dev). The package is a hard dep of romdev.
26
- function resolveWladxGlue(file) {
27
- try {
28
- const u = import.meta.resolve("romdev-platform-snes");
29
- const p = path.join(path.dirname(fileURLToPath(u)), "wasm", file);
30
- if (existsSync(p)) return p;
31
- } catch { /* package not resolvable — fall through to local */ }
32
- const local = path.join(__dirname, "wasm", file);
33
- if (existsSync(local)) return local;
34
- throw new Error(`wla-dx WASM (${file}) not found — install romdev-platform-snes`);
35
- }
36
- // Lazy + memoized per tool: resolve only on the first wla-dx (SNES asm) build
37
- // that uses each tool, not at module load.
38
- const _glue = {};
39
- const wladxGlue = (file) => (_glue[file] ??= resolveWladxGlue(file));
24
+ // consumer); a local src/ copy is the dev fallback. Lazy + memoized per tool:
25
+ // resolve only on the first wla-dx (SNES asm) build that uses each tool, not at
26
+ // module load.
27
+ const wladxGlue = makeGlueResolver({ pkg: "romdev-platform-snes", localDir: __dirname, label: "wla-dx" });
40
28
 
41
29
  /**
42
30
  * Assemble a single .asm source with wla-65816.
@@ -5,22 +5,16 @@
5
5
  // romdev-toolchain-sdcc alongside sdas/sdcc.
6
6
 
7
7
  import { fileURLToPath } from "node:url";
8
- import { existsSync } from "node:fs";
9
8
  import path from "node:path";
10
9
  import { runIsolated, textFile, binaryFile, getOutputBytes } from "../_worker/run.js";
10
+ import { makeGlueResolver } from "../common/wasm-tool.js";
11
11
 
12
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
13
 
14
- function z80Glue(file) {
15
- try {
16
- const u = import.meta.resolve("romdev-toolchain-sdcc");
17
- const p = path.join(path.dirname(fileURLToPath(u)), "wasm", file);
18
- if (existsSync(p)) return p;
19
- } catch { /* fall through to local */ }
20
- const local = path.join(__dirname, "wasm", file);
21
- if (existsSync(local)) return local;
22
- throw new Error(`z80 binutils WASM (${file}) not found — install romdev-toolchain-sdcc`);
23
- }
14
+ // z80-elf binutils ship in romdev-toolchain-sdcc (alongside sdas/sdcc); a local
15
+ // src/ copy is the dev fallback. Lazy + memoized: resolve each glue only on its
16
+ // first use, not at boot.
17
+ const z80Glue = makeGlueResolver({ pkg: "romdev-toolchain-sdcc", localDir: __dirname, label: "sdas" });
24
18
 
25
19
  /** True if the z80 GNU assembler chain is available. */
26
20
  export function z80GnuAvailable() {