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
@@ -37,6 +37,7 @@ import {
37
37
  } from "../arm-none-eabi-gcc/gcc.js";
38
38
  import { packAr } from "../common/ar.js";
39
39
  import { resolveSdkArchive } from "../common/sdk-cache.js";
40
+ import { CBuild, BuildError } from "../common/c-build.js";
40
41
 
41
42
  const __filename = fileURLToPath(import.meta.url);
42
43
  const __dirname = path.dirname(__filename);
@@ -139,7 +140,7 @@ function normalizeGbaSources(args) {
139
140
  * 6. objcopy -O binary → final .gba ROM
140
141
  */
141
142
  async function buildWithLibtonc({ sources, headers, cc1Options, binaryIncludes = {}, maxmod = false, rebuildSdk = false, writeSeed = false }) {
142
- let log = "";
143
+ const cb = new CBuild();
143
144
 
144
145
  const crt0Src = await readFile(path.join(LIBTONC_DIR, "gba_crt0.s"), "utf-8");
145
146
  const linkScript = await readFile(path.join(LIBTONC_DIR, "gba_cart.ld"), "utf-8");
@@ -152,7 +153,7 @@ async function buildWithLibtonc({ sources, headers, cc1Options, binaryIncludes =
152
153
  // ourselves so users don't need to author it by hand.
153
154
  const hasSoundbank = maxmod && Object.prototype.hasOwnProperty.call(binaryIncludes, "soundbank.bin");
154
155
  if (hasSoundbank) {
155
- log += `--- soundbank stub auto-emitted (.incbin "soundbank.bin") ---\n`;
156
+ cb.note(`--- soundbank stub auto-emitted (.incbin "soundbank.bin") ---`);
156
157
  }
157
158
 
158
159
  const libtoncHeaders = await loadLibtoncHeaders();
@@ -166,45 +167,34 @@ async function buildWithLibtonc({ sources, headers, cc1Options, binaryIncludes =
166
167
  "-mthumb-interwork",
167
168
  ];
168
169
 
169
- // ── Stage A: compile each user .c → .s ─────────────────────────
170
- const objects = {};
171
- for (const [name, src] of Object.entries(sources)) {
172
- if (!name.endsWith(".c")) continue;
173
- const cc1 = await runCc1arm({
174
- source: src,
175
- headers: { ...sysHeaders, ...libtoncHeaders, ...maxmodHeaders, ...headers },
176
- options: libtoncCc1Options,
177
- });
178
- log += `--- cc1 (${name}) ---\n${cc1.log || "(ok)"}\n`;
179
- if (cc1.exitCode !== 0 || !cc1.asmSource) {
180
- return { ok: false, binary: null, log, exitCode: cc1.exitCode || 1, stage: `cc1 (${name})`, runtime: "libtonc", ...(cc1.crash ? { crash: cc1.crash } : {}) };
181
- }
182
- const asm = await runArmAs({ source: cc1.asmSource });
183
- log += `--- as (${name}) ---\n${asm.log || "(ok)"}\n`;
184
- if (asm.exitCode !== 0 || !asm.object) {
185
- return { ok: false, binary: null, log, exitCode: asm.exitCode || 1, stage: `as (${name})`, runtime: "libtonc", ...(asm.crash ? { crash: asm.crash } : {}) };
170
+ try {
171
+ // ── Stage A: compile each user .c → .s ─────────────────────────
172
+ const objects = {};
173
+ for (const [name, src] of Object.entries(sources)) {
174
+ if (!name.endsWith(".c")) continue;
175
+ const cc1 = await cb.stage(`cc1 (${name})`, () => runCc1arm({
176
+ source: src,
177
+ headers: { ...sysHeaders, ...libtoncHeaders, ...maxmodHeaders, ...headers },
178
+ options: libtoncCc1Options,
179
+ }), (r) => r.asmSource);
180
+ const asm = await cb.stage(`as (${name})`, () => runArmAs({ source: cc1.asmSource }), (r) => r.object);
181
+ objects[name.replace(/\.c$/, ".o")] = asm.object;
186
182
  }
187
- objects[name.replace(/\.c$/, ".o")] = asm.object;
188
- }
189
183
 
190
- // ── Stage B: assemble gba_crt0.s ─────────────────────────────────
191
- const crt0As = await runArmAs({ source: crt0Src });
192
- log += `--- as (gba_crt0.s) ---\n${crt0As.log || "(ok)"}\n`;
193
- if (crt0As.exitCode !== 0 || !crt0As.object) {
194
- return { ok: false, binary: null, log, exitCode: crt0As.exitCode || 1, stage: "as (gba_crt0.s)", runtime: "libtonc", ...(crt0As.crash ? { crash: crt0As.crash } : {}) };
195
- }
196
- objects["gba_crt0.o"] = crt0As.object;
197
-
198
- // fake_heap_end + `end` stubs. The gba_cart.ld linker script
199
- // defines `__end__` (top of bss) but not the bare `end` symbol that
200
- // newlib's sbrk() references. We provide both `fake_heap_end` AND
201
- // `end` here as tiny data-section labels. Points at end of EWRAM.
202
- //
203
- // For real games you'd want sbrk to start the heap at __end__ and
204
- // track it dynamically — this stub gives a fixed bound which is
205
- // fine for the small allocations newlib does internally.
206
- const fakeHeapEndStub = await runArmAs({
207
- source: `
184
+ // ── Stage B: assemble gba_crt0.s ─────────────────────────────────
185
+ const crt0As = await cb.stage("as (gba_crt0.s)", () => runArmAs({ source: crt0Src }), (r) => r.object);
186
+ objects["gba_crt0.o"] = crt0As.object;
187
+
188
+ // fake_heap_end + `end` stubs. The gba_cart.ld linker script
189
+ // defines `__end__` (top of bss) but not the bare `end` symbol that
190
+ // newlib's sbrk() references. We provide both `fake_heap_end` AND
191
+ // `end` here as tiny data-section labels. Points at end of EWRAM.
192
+ //
193
+ // For real games you'd want sbrk to start the heap at __end__ and
194
+ // track it dynamically this stub gives a fixed bound which is
195
+ // fine for the small allocations newlib does internally.
196
+ const fakeHeapEndStub = await cb.stage("as (fake_heap_end stub)", () => runArmAs({
197
+ source: `
208
198
  .section .data
209
199
  .global fake_heap_end
210
200
  .global end
@@ -214,17 +204,13 @@ async function buildWithLibtonc({ sources, headers, cc1Options, binaryIncludes =
214
204
  end:
215
205
  .word 0x02000000 /* start of EWRAM — sbrk grows from here */
216
206
  `,
217
- });
218
- log += `--- as (fake_heap_end stub) ---\n${fakeHeapEndStub.log || "(ok)"}\n`;
219
- if (fakeHeapEndStub.exitCode !== 0 || !fakeHeapEndStub.object) {
220
- return { ok: false, binary: null, log, exitCode: fakeHeapEndStub.exitCode || 1, stage: "as (fake_heap_end stub)", runtime: "libtonc", ...(fakeHeapEndStub.crash ? { crash: fakeHeapEndStub.crash } : {}) };
221
- }
222
- objects["fake_heap_end.o"] = fakeHeapEndStub.object;
207
+ }), (r) => r.object);
208
+ objects["fake_heap_end.o"] = fakeHeapEndStub.object;
223
209
 
224
- // ── Stage B3: maxmod soundbank embedding stub ───────────────────
225
- if (hasSoundbank) {
226
- const soundbankStub = await runArmAs({
227
- source: `
210
+ // ── Stage B3: maxmod soundbank embedding stub ───────────────────
211
+ if (hasSoundbank) {
212
+ const soundbankStub = await cb.stage("as (soundbank.s)", () => runArmAs({
213
+ source: `
228
214
  .section .rodata
229
215
  .align 2
230
216
  .global soundbank_bin
@@ -236,117 +222,109 @@ async function buildWithLibtonc({ sources, headers, cc1Options, binaryIncludes =
236
222
  soundbank_bin_size:
237
223
  .word soundbank_bin_end - soundbank_bin
238
224
  `,
239
- binaryIncludes: { "soundbank.bin": binaryIncludes["soundbank.bin"] },
240
- });
241
- log += `--- as (soundbank.s) ---\n${soundbankStub.log || "(ok)"}\n`;
242
- if (soundbankStub.exitCode !== 0 || !soundbankStub.object) {
243
- return { ok: false, binary: null, log, exitCode: soundbankStub.exitCode || 1, stage: "as (soundbank.s)", runtime: "libtonc", ...(soundbankStub.crash ? { crash: soundbankStub.crash } : {}) };
225
+ binaryIncludes: { "soundbank.bin": binaryIncludes["soundbank.bin"] },
226
+ }), (r) => r.object);
227
+ objects["soundbank.o"] = soundbankStub.object;
244
228
  }
245
- objects["soundbank.o"] = soundbankStub.object;
246
- }
247
229
 
248
- // ── Stage B4: resolve libtonc (and maxmod) — seed by default, or compile
249
- // from source when rebuildSdk is set. Edits to the vendored SDK source take
250
- // effect with rebuildSdk:true; without it, the fast prebuilt seed is used and
251
- // an edit is flagged (sdkEditIgnored), never silently dropped.
252
- const sdkWarnings = [];
253
- const toncRes = await sdkArchive({
254
- name: "libtonc",
255
- srcDirs: [path.join(LIBTONC_DIR, "src"), SYSBASE_DIR],
256
- seedBase: path.join(LIBTONC_DIR, "libtonc"),
257
- rebuild: rebuildSdk, writeSeed,
258
- compile: async () => {
259
- const r = await compileSdkObjects({
260
- key: "libtonc",
261
- srcDirs: [path.join(LIBTONC_DIR, "src"), SYSBASE_DIR],
262
- headers: { ...sysHeaders, ...libtoncHeaders, ...maxmodHeaders, ...headers },
263
- });
264
- return r.ok ? { ok: true, archive: packAr(r.objects) } : r;
265
- },
266
- });
267
- if (!toncRes.ok) {
268
- return { ok: false, binary: null, log: log + (toncRes.log || ""), exitCode: 1, stage: toncRes.stage, runtime: "libtonc" };
269
- }
270
- if (toncRes.sdkEditIgnored) sdkWarnings.push(toncRes.sdkEditIgnored);
271
- log += `--- libtonc ${toncRes.fromSource ? "compiled from source" : "from prebuilt seed"} ---\n`;
272
-
273
- let maxmodAr = null;
274
- if (maxmod) {
275
- const mmHeaders = await loadMaxmodAsmHeaders();
276
- const mmRes = await sdkArchive({
277
- name: "maxmod",
278
- srcDirs: [path.join(MAXMOD_DIR, "source"), path.join(MAXMOD_DIR, "source_gba")],
279
- seedBase: path.join(MAXMOD_DIR, "maxmod"),
230
+ // ── Stage B4: resolve libtonc (and maxmod) — seed by default, or compile
231
+ // from source when rebuildSdk is set. Edits to the vendored SDK source take
232
+ // effect with rebuildSdk:true; without it, the fast prebuilt seed is used and
233
+ // an edit is flagged (sdkEditIgnored), never silently dropped.
234
+ const sdkWarnings = [];
235
+ const toncRes = await sdkArchive({
236
+ name: "libtonc",
237
+ srcDirs: [path.join(LIBTONC_DIR, "src"), SYSBASE_DIR],
238
+ seedBase: path.join(LIBTONC_DIR, "libtonc"),
280
239
  rebuild: rebuildSdk, writeSeed,
281
240
  compile: async () => {
282
241
  const r = await compileSdkObjects({
283
- key: "maxmod",
284
- srcDirs: [path.join(MAXMOD_DIR, "source"), path.join(MAXMOD_DIR, "source_gba")],
285
- headers: { ...sysHeaders, ...maxmodHeaders, ...mmHeaders },
286
- cppDefines: ["SYS_GBA=1"],
242
+ key: "libtonc",
243
+ srcDirs: [path.join(LIBTONC_DIR, "src"), SYSBASE_DIR],
244
+ headers: { ...sysHeaders, ...libtoncHeaders, ...maxmodHeaders, ...headers },
287
245
  });
288
246
  return r.ok ? { ok: true, archive: packAr(r.objects) } : r;
289
247
  },
290
248
  });
291
- if (!mmRes.ok) {
292
- return { ok: false, binary: null, log: log + (mmRes.log || ""), exitCode: 1, stage: mmRes.stage, runtime: "libtonc" };
249
+ if (!toncRes.ok) {
250
+ return { ok: false, binary: null, log: cb.log + (toncRes.log || ""), exitCode: 1, stage: toncRes.stage, runtime: "libtonc" };
251
+ }
252
+ if (toncRes.sdkEditIgnored) sdkWarnings.push(toncRes.sdkEditIgnored);
253
+ cb.note(`--- libtonc ${toncRes.fromSource ? "compiled from source" : "from prebuilt seed"} ---`);
254
+
255
+ let maxmodAr = null;
256
+ if (maxmod) {
257
+ const mmHeaders = await loadMaxmodAsmHeaders();
258
+ const mmRes = await sdkArchive({
259
+ name: "maxmod",
260
+ srcDirs: [path.join(MAXMOD_DIR, "source"), path.join(MAXMOD_DIR, "source_gba")],
261
+ seedBase: path.join(MAXMOD_DIR, "maxmod"),
262
+ rebuild: rebuildSdk, writeSeed,
263
+ compile: async () => {
264
+ const r = await compileSdkObjects({
265
+ key: "maxmod",
266
+ srcDirs: [path.join(MAXMOD_DIR, "source"), path.join(MAXMOD_DIR, "source_gba")],
267
+ headers: { ...sysHeaders, ...maxmodHeaders, ...mmHeaders },
268
+ cppDefines: ["SYS_GBA=1"],
269
+ });
270
+ return r.ok ? { ok: true, archive: packAr(r.objects) } : r;
271
+ },
272
+ });
273
+ if (!mmRes.ok) {
274
+ return { ok: false, binary: null, log: cb.log + (mmRes.log || ""), exitCode: 1, stage: mmRes.stage, runtime: "libtonc" };
275
+ }
276
+ if (mmRes.sdkEditIgnored) sdkWarnings.push(mmRes.sdkEditIgnored);
277
+ maxmodAr = mmRes.archive;
278
+ cb.note(`--- maxmod ${mmRes.fromSource ? "compiled from source" : "from prebuilt seed"} ---`);
293
279
  }
294
- if (mmRes.sdkEditIgnored) sdkWarnings.push(mmRes.sdkEditIgnored);
295
- maxmodAr = mmRes.archive;
296
- log += `--- maxmod ${mmRes.fromSource ? "compiled from source" : "from prebuilt seed"} ---\n`;
297
- }
298
-
299
- // ── Stage C: link ───────────────────────────────────────────────
300
- // crt*.o + libgcc/libc/libnosys are gcc/newlib toolchain runtime.
301
- const archives = {
302
- "libtonc.a": toncRes.archive,
303
- "crti.o": new Uint8Array(await readFile(path.join(LIBTONC_DIR, "crti.o"))),
304
- "crtn.o": new Uint8Array(await readFile(path.join(LIBTONC_DIR, "crtn.o"))),
305
- "crtbegin.o": new Uint8Array(await readFile(path.join(LIBTONC_DIR, "crtbegin.o"))),
306
- "crtend.o": new Uint8Array(await readFile(path.join(LIBTONC_DIR, "crtend.o"))),
307
- };
308
- if (maxmodAr) archives["libmm.a"] = maxmodAr;
309
- const targetLibs = await readTargetArchives();
310
- Object.assign(archives, targetLibs);
311
-
312
- // Wrap the libs in --start-group / --end-group so the linker
313
- // re-scans them for cross-references between libc → libgcc → libc
314
- // (libc/strtol uses __aeabi_uidiv from libgcc; libgcc may call back
315
- // into libc helpers). Without re-scan we get undefined references.
316
- const ld = await runArmLd({
317
- objects,
318
- linkScript,
319
- archives,
320
- libraryPaths: ["/work"],
321
- libraries: [], // explicit -l flags moved into options for ordering control
322
- options: [
323
- "/work/crti.o",
324
- "/work/crtbegin.o",
325
- // libtonc/maxmod archives are packed from compiled-from-source objects.
326
- "--start-group",
327
- "-ltonc",
328
- ...(maxmod ? ["-lmm"] : []),
329
- "-lc",
330
- "-lgcc",
331
- "-lnosys",
332
- "--end-group",
333
- "/work/crtend.o",
334
- "/work/crtn.o",
335
- ],
336
- });
337
- log += `--- ld ---\n${ld.log || "(ok)"}\n`;
338
- if (ld.exitCode !== 0 || !ld.elf) {
339
- return { ok: false, binary: null, log, exitCode: ld.exitCode || 1, stage: "ld", runtime: "libtonc", ...(ld.crash ? { crash: ld.crash } : {}) };
340
- }
341
280
 
342
- // ── Stage D: objcopy ────────────────────────────────────────────
343
- const objcopy = await runArmObjcopy({ elf: ld.elf });
344
- log += `--- objcopy ---\n${objcopy.log || "(ok)"}\n`;
345
- if (objcopy.exitCode !== 0 || !objcopy.binary) {
346
- return { ok: false, binary: null, log, exitCode: objcopy.exitCode || 1, stage: "objcopy", runtime: "libtonc", ...(objcopy.crash ? { crash: objcopy.crash } : {}) };
281
+ // ── Stage C: link ───────────────────────────────────────────────
282
+ // crt*.o + libgcc/libc/libnosys are gcc/newlib toolchain runtime.
283
+ const archives = {
284
+ "libtonc.a": toncRes.archive,
285
+ "crti.o": new Uint8Array(await readFile(path.join(LIBTONC_DIR, "crti.o"))),
286
+ "crtn.o": new Uint8Array(await readFile(path.join(LIBTONC_DIR, "crtn.o"))),
287
+ "crtbegin.o": new Uint8Array(await readFile(path.join(LIBTONC_DIR, "crtbegin.o"))),
288
+ "crtend.o": new Uint8Array(await readFile(path.join(LIBTONC_DIR, "crtend.o"))),
289
+ };
290
+ if (maxmodAr) archives["libmm.a"] = maxmodAr;
291
+ const targetLibs = await readTargetArchives();
292
+ Object.assign(archives, targetLibs);
293
+
294
+ // Wrap the libs in --start-group / --end-group so the linker
295
+ // re-scans them for cross-references between libc → libgcc → libc
296
+ // (libc/strtol uses __aeabi_uidiv from libgcc; libgcc may call back
297
+ // into libc helpers). Without re-scan we get undefined references.
298
+ const ld = await cb.stage("ld", () => runArmLd({
299
+ objects,
300
+ linkScript,
301
+ archives,
302
+ libraryPaths: ["/work"],
303
+ libraries: [], // explicit -l flags moved into options for ordering control
304
+ options: [
305
+ "/work/crti.o",
306
+ "/work/crtbegin.o",
307
+ // libtonc/maxmod archives are packed from compiled-from-source objects.
308
+ "--start-group",
309
+ "-ltonc",
310
+ ...(maxmod ? ["-lmm"] : []),
311
+ "-lc",
312
+ "-lgcc",
313
+ "-lnosys",
314
+ "--end-group",
315
+ "/work/crtend.o",
316
+ "/work/crtn.o",
317
+ ],
318
+ }), (r) => r.elf);
319
+
320
+ // ── Stage D: objcopy ────────────────────────────────────────────
321
+ const objcopy = await cb.stage("objcopy", () => runArmObjcopy({ elf: ld.elf }), (r) => r.binary);
322
+
323
+ return { ok: true, binary: objcopy.binary, log: cb.log, exitCode: 0, stage: "done", runtime: "libtonc", ...(ld.map ? { symbols: ld.map } : {}), ...(sdkWarnings.length ? { sdkEditIgnored: sdkWarnings } : {}) };
324
+ } catch (e) {
325
+ if (e instanceof BuildError) return e.toResult({ runtime: "libtonc" });
326
+ throw e;
347
327
  }
348
-
349
- return { ok: true, binary: objcopy.binary, log, exitCode: 0, stage: "done", runtime: "libtonc", ...(ld.map ? { symbols: ld.map } : {}), ...(sdkWarnings.length ? { sdkEditIgnored: sdkWarnings } : {}) };
350
328
  }
351
329
 
352
330
  /**
@@ -360,7 +338,7 @@ async function buildWithLibtonc({ sources, headers, cc1Options, binaryIncludes =
360
338
  * 5. objcopy -O binary → final .gba ROM
361
339
  */
362
340
  async function buildWithLibgba({ sources, headers, cc1Options, rebuildSdk = false, writeSeed = false }) {
363
- let log = "";
341
+ const cb = new CBuild();
364
342
 
365
343
  // Read the libgba bundle once (crt0 + linker script; the SDK itself is
366
344
  // compiled from source below, not linked from libgba.a).
@@ -381,43 +359,32 @@ async function buildWithLibgba({ sources, headers, cc1Options, rebuildSdk = fals
381
359
  "-mthumb-interwork",
382
360
  ];
383
361
 
384
- // ── Stage A: compile each user .c → .s ─────────────────────────
385
- const objects = {};
386
- for (const [name, src] of Object.entries(sources)) {
387
- if (!name.endsWith(".c")) continue;
388
- const cc1 = await runCc1arm({
389
- source: src,
390
- headers: { ...sysHeaders, ...libgbaHeaders, ...headers },
391
- options: libgbaCc1Options,
392
- });
393
- log += `--- cc1 (${name}) ---\n${cc1.log || "(ok)"}\n`;
394
- if (cc1.exitCode !== 0 || !cc1.asmSource) {
395
- return { ok: false, binary: null, log, exitCode: cc1.exitCode || 1, stage: `cc1 (${name})`, runtime: "libgba", ...(cc1.crash ? { crash: cc1.crash } : {}) };
396
- }
397
- const asm = await runArmAs({ source: cc1.asmSource });
398
- log += `--- as (${name}) ---\n${asm.log || "(ok)"}\n`;
399
- if (asm.exitCode !== 0 || !asm.object) {
400
- return { ok: false, binary: null, log, exitCode: asm.exitCode || 1, stage: `as (${name})`, runtime: "libgba", ...(asm.crash ? { crash: asm.crash } : {}) };
362
+ try {
363
+ // ── Stage A: compile each user .c → .s ─────────────────────────
364
+ const objects = {};
365
+ for (const [name, src] of Object.entries(sources)) {
366
+ if (!name.endsWith(".c")) continue;
367
+ const cc1 = await cb.stage(`cc1 (${name})`, () => runCc1arm({
368
+ source: src,
369
+ headers: { ...sysHeaders, ...libgbaHeaders, ...headers },
370
+ options: libgbaCc1Options,
371
+ }), (r) => r.asmSource);
372
+ const asm = await cb.stage(`as (${name})`, () => runArmAs({ source: cc1.asmSource }), (r) => r.object);
373
+ objects[name.replace(/\.c$/, ".o")] = asm.object;
401
374
  }
402
- objects[name.replace(/\.c$/, ".o")] = asm.object;
403
- }
404
375
 
405
- // ── Stage B: assemble gba_crt0.s ─────────────────────────────────
406
- const crt0As = await runArmAs({ source: crt0Src });
407
- log += `--- as (gba_crt0.s) ---\n${crt0As.log || "(ok)"}\n`;
408
- if (crt0As.exitCode !== 0 || !crt0As.object) {
409
- return { ok: false, binary: null, log, exitCode: crt0As.exitCode || 1, stage: "as (gba_crt0.s)", runtime: "libgba", ...(crt0As.crash ? { crash: crt0As.crash } : {}) };
410
- }
411
- objects["gba_crt0.o"] = crt0As.object;
412
-
413
- // ── Stage B2: assemble fake_heap_end + `end` stubs ──────────────
414
- // devkitARM's libsysbase normally provides both `fake_heap_end`
415
- // (used by sbrk's heap tracking) and the linker script defines
416
- // `end` (start of heap). We excluded libsysbase so we provide
417
- // both as tiny data labels — gba_cart.ld defines `__end__` but not
418
- // bare `end` which newlib sbrk needs.
419
- const fakeHeapEndStub = await runArmAs({
420
- source: `
376
+ // ── Stage B: assemble gba_crt0.s ─────────────────────────────────
377
+ const crt0As = await cb.stage("as (gba_crt0.s)", () => runArmAs({ source: crt0Src }), (r) => r.object);
378
+ objects["gba_crt0.o"] = crt0As.object;
379
+
380
+ // ── Stage B2: assemble fake_heap_end + `end` stubs ──────────────
381
+ // devkitARM's libsysbase normally provides both `fake_heap_end`
382
+ // (used by sbrk's heap tracking) and the linker script defines
383
+ // `end` (start of heap). We excluded libsysbase so we provide
384
+ // both as tiny data labels gba_cart.ld defines `__end__` but not
385
+ // bare `end` which newlib sbrk needs.
386
+ const fakeHeapEndStub = await cb.stage("as (fake_heap_end stub)", () => runArmAs({
387
+ source: `
421
388
  .section .data
422
389
  .global fake_heap_end
423
390
  .global end
@@ -427,98 +394,90 @@ async function buildWithLibgba({ sources, headers, cc1Options, rebuildSdk = fals
427
394
  end:
428
395
  .word 0x02000000 /* start of EWRAM — sbrk grows from here */
429
396
  `,
430
- });
431
- log += `--- as (fake_heap_end stub) ---\n${fakeHeapEndStub.log || "(ok)"}\n`;
432
- if (fakeHeapEndStub.exitCode !== 0 || !fakeHeapEndStub.object) {
433
- return { ok: false, binary: null, log, exitCode: fakeHeapEndStub.exitCode || 1, stage: "as (fake_heap_end stub)", runtime: "libgba", ...(fakeHeapEndStub.crash ? { crash: fakeHeapEndStub.crash } : {}) };
434
- }
435
- objects["fake_heap_end.o"] = fakeHeapEndStub.object;
436
-
437
- // ── Stage B3: compile libgba (+ libsysbase) FROM SOURCE ─────────
438
- // Link libgba's own compiled objects, NOT a prebuilt libgba.a. Edit any
439
- // libgba source and it takes effect. libsysbase (gba_iosupport.c) gives the
440
- // devoptab routing so libgba's consoleInit() + iprintf work.
441
- const sdkWarnings = [];
442
- const gbaRes = await sdkArchive({
443
- name: "libgba",
444
- srcDirs: [path.join(LIBGBA_DIR, "src"), SYSBASE_DIR],
445
- seedBase: path.join(LIBGBA_DIR, "libgba"),
446
- rebuild: rebuildSdk, writeSeed,
447
- compile: async () => {
448
- const r = await compileSdkObjects({
449
- key: "libgba",
450
- srcDirs: [path.join(LIBGBA_DIR, "src"), SYSBASE_DIR],
451
- headers: { ...sysHeaders, ...libgbaHeaders, ...headers },
452
- });
453
- return r.ok ? { ok: true, archive: packAr(r.objects) } : r;
454
- },
455
- });
456
- if (!gbaRes.ok) {
457
- return { ok: false, binary: null, log: log + (gbaRes.log || ""), exitCode: 1, stage: gbaRes.stage, runtime: "libgba" };
458
- }
459
- if (gbaRes.sdkEditIgnored) sdkWarnings.push(gbaRes.sdkEditIgnored);
460
- log += `--- libgba ${gbaRes.fromSource ? "compiled from source" : "from prebuilt seed"} ---\n`;
461
-
462
- // ── Stage C: link ───────────────────────────────────────────────
463
- // libgba archive from seed/source; crt*.o + libgcc/libc/libnosys are toolchain.
464
- const archives = {
465
- "libgba.a": gbaRes.archive,
466
- "crti.o": new Uint8Array(await readFile(path.join(LIBGBA_DIR, "crti.o"))),
467
- "crtn.o": new Uint8Array(await readFile(path.join(LIBGBA_DIR, "crtn.o"))),
468
- "crtbegin.o": new Uint8Array(await readFile(path.join(LIBGBA_DIR, "crtbegin.o"))),
469
- "crtend.o": new Uint8Array(await readFile(path.join(LIBGBA_DIR, "crtend.o"))),
470
- };
471
- const targetLibs = await readTargetArchives();
472
- Object.assign(archives, targetLibs);
473
-
474
- // Link order matters for `-l` archives and crt*.o files. Standard
475
- // gcc-driver order: crti.o + crtbegin.o user objects → libgba →
476
- // libc + libgcc + libnosys crtend.o + crtn.o.
477
- // --start-group / --end-group forces ld to re-scan the libs so
478
- // libc → libgcc → libc cross-refs resolve (libc/strtol calls
479
- // __aeabi_uidiv in libgcc; libgcc may call back into libc).
480
- const ld = await runArmLd({
481
- objects,
482
- linkScript,
483
- archives,
484
- libraryPaths: ["/work"],
485
- libraries: [],
486
- options: [
487
- "/work/crti.o",
488
- "/work/crtbegin.o",
489
- // libgba archive packed from compiled-from-source objects.
490
- "--start-group",
491
- "-lgba",
492
- "-lc",
493
- "-lgcc",
494
- "-lnosys",
495
- "--end-group",
496
- "/work/crtend.o",
497
- "/work/crtn.o",
498
- ],
499
- });
500
- log += `--- ld ---\n${ld.log || "(ok)"}\n`;
501
- if (ld.exitCode !== 0 || !ld.elf) {
502
- return { ok: false, binary: null, log, exitCode: ld.exitCode || 1, stage: "ld", runtime: "libgba", ...(ld.crash ? { crash: ld.crash } : {}) };
503
- }
504
-
505
- // ── Stage D: objcopy ELF → raw .gba ─────────────────────────────
506
- const objcopy = await runArmObjcopy({ elf: ld.elf });
507
- log += `--- objcopy ---\n${objcopy.log || "(ok)"}\n`;
508
- if (objcopy.exitCode !== 0 || !objcopy.binary) {
509
- return { ok: false, binary: null, log, exitCode: objcopy.exitCode || 1, stage: "objcopy", runtime: "libgba", ...(objcopy.crash ? { crash: objcopy.crash } : {}) };
397
+ }), (r) => r.object);
398
+ objects["fake_heap_end.o"] = fakeHeapEndStub.object;
399
+
400
+ // ── Stage B3: compile libgba (+ libsysbase) FROM SOURCE ─────────
401
+ // Link libgba's own compiled objects, NOT a prebuilt libgba.a. Edit any
402
+ // libgba source and it takes effect. libsysbase (gba_iosupport.c) gives the
403
+ // devoptab routing so libgba's consoleInit() + iprintf work.
404
+ const sdkWarnings = [];
405
+ const gbaRes = await sdkArchive({
406
+ name: "libgba",
407
+ srcDirs: [path.join(LIBGBA_DIR, "src"), SYSBASE_DIR],
408
+ seedBase: path.join(LIBGBA_DIR, "libgba"),
409
+ rebuild: rebuildSdk, writeSeed,
410
+ compile: async () => {
411
+ const r = await compileSdkObjects({
412
+ key: "libgba",
413
+ srcDirs: [path.join(LIBGBA_DIR, "src"), SYSBASE_DIR],
414
+ headers: { ...sysHeaders, ...libgbaHeaders, ...headers },
415
+ });
416
+ return r.ok ? { ok: true, archive: packAr(r.objects) } : r;
417
+ },
418
+ });
419
+ if (!gbaRes.ok) {
420
+ return { ok: false, binary: null, log: cb.log + (gbaRes.log || ""), exitCode: 1, stage: gbaRes.stage, runtime: "libgba" };
421
+ }
422
+ if (gbaRes.sdkEditIgnored) sdkWarnings.push(gbaRes.sdkEditIgnored);
423
+ cb.note(`--- libgba ${gbaRes.fromSource ? "compiled from source" : "from prebuilt seed"} ---`);
424
+
425
+ // ── Stage C: link ───────────────────────────────────────────────
426
+ // libgba archive from seed/source; crt*.o + libgcc/libc/libnosys are toolchain.
427
+ const archives = {
428
+ "libgba.a": gbaRes.archive,
429
+ "crti.o": new Uint8Array(await readFile(path.join(LIBGBA_DIR, "crti.o"))),
430
+ "crtn.o": new Uint8Array(await readFile(path.join(LIBGBA_DIR, "crtn.o"))),
431
+ "crtbegin.o": new Uint8Array(await readFile(path.join(LIBGBA_DIR, "crtbegin.o"))),
432
+ "crtend.o": new Uint8Array(await readFile(path.join(LIBGBA_DIR, "crtend.o"))),
433
+ };
434
+ const targetLibs = await readTargetArchives();
435
+ Object.assign(archives, targetLibs);
436
+
437
+ // Link order matters for `-l` archives and crt*.o files. Standard
438
+ // gcc-driver order: crti.o + crtbegin.o → user objects → libgba →
439
+ // libc + libgcc + libnosys → crtend.o + crtn.o.
440
+ // --start-group / --end-group forces ld to re-scan the libs so
441
+ // libc libgcc libc cross-refs resolve (libc/strtol calls
442
+ // __aeabi_uidiv in libgcc; libgcc may call back into libc).
443
+ const ld = await cb.stage("ld", () => runArmLd({
444
+ objects,
445
+ linkScript,
446
+ archives,
447
+ libraryPaths: ["/work"],
448
+ libraries: [],
449
+ options: [
450
+ "/work/crti.o",
451
+ "/work/crtbegin.o",
452
+ // libgba archive packed from compiled-from-source objects.
453
+ "--start-group",
454
+ "-lgba",
455
+ "-lc",
456
+ "-lgcc",
457
+ "-lnosys",
458
+ "--end-group",
459
+ "/work/crtend.o",
460
+ "/work/crtn.o",
461
+ ],
462
+ }), (r) => r.elf);
463
+
464
+ // ── Stage D: objcopy ELF → raw .gba ─────────────────────────────
465
+ const objcopy = await cb.stage("objcopy", () => runArmObjcopy({ elf: ld.elf }), (r) => r.binary);
466
+
467
+ return {
468
+ ok: true,
469
+ binary: objcopy.binary,
470
+ log: cb.log,
471
+ exitCode: 0,
472
+ stage: "done",
473
+ runtime: "libgba",
474
+ ...(ld.map ? { symbols: ld.map } : {}),
475
+ ...(sdkWarnings.length ? { sdkEditIgnored: sdkWarnings } : {}),
476
+ };
477
+ } catch (e) {
478
+ if (e instanceof BuildError) return e.toResult({ runtime: "libgba" });
479
+ throw e;
510
480
  }
511
-
512
- return {
513
- ok: true,
514
- binary: objcopy.binary,
515
- log,
516
- exitCode: 0,
517
- stage: "done",
518
- runtime: "libgba",
519
- ...(ld.map ? { symbols: ld.map } : {}),
520
- ...(sdkWarnings.length ? { sdkEditIgnored: sdkWarnings } : {}),
521
- };
522
481
  }
523
482
 
524
483
  /**
@@ -530,44 +489,33 @@ async function buildWithLibgba({ sources, headers, cc1Options, rebuildSdk = fals
530
489
  * must provide their own _start / main entry point.
531
490
  */
532
491
  async function buildMinimal({ sources, headers, cc1Options }) {
533
- let log = "";
534
- const objects = {};
535
- for (const [name, src] of Object.entries(sources)) {
536
- if (!name.endsWith(".c")) continue;
537
- const cc1 = await runCc1arm({ source: src, headers, options: cc1Options });
538
- log += `--- cc1 (${name}) ---\n${cc1.log || "(ok)"}\n`;
539
- if (cc1.exitCode !== 0 || !cc1.asmSource) {
540
- return { ok: false, binary: null, log, exitCode: cc1.exitCode || 1, stage: `cc1 (${name})`, runtime: "minimal", ...(cc1.crash ? { crash: cc1.crash } : {}) };
541
- }
542
- const asm = await runArmAs({ source: cc1.asmSource });
543
- log += `--- as (${name}) ---\n${asm.log || "(ok)"}\n`;
544
- if (asm.exitCode !== 0 || !asm.object) {
545
- return { ok: false, binary: null, log, exitCode: asm.exitCode || 1, stage: `as (${name})`, runtime: "minimal", ...(asm.crash ? { crash: asm.crash } : {}) };
492
+ const cb = new CBuild();
493
+ try {
494
+ const objects = {};
495
+ for (const [name, src] of Object.entries(sources)) {
496
+ if (!name.endsWith(".c")) continue;
497
+ const cc1 = await cb.stage(`cc1 (${name})`, () => runCc1arm({ source: src, headers, options: cc1Options }), (r) => r.asmSource);
498
+ const asm = await cb.stage(`as (${name})`, () => runArmAs({ source: cc1.asmSource }), (r) => r.object);
499
+ objects[name.replace(/\.c$/, ".o")] = asm.object;
546
500
  }
547
- objects[name.replace(/\.c$/, ".o")] = asm.object;
548
- }
549
501
 
550
- // Minimum-viable linker script — single .text region at GBA cart base.
551
- const ld = await runArmLd({
552
- objects,
553
- linkScript: `OUTPUT_FORMAT("elf32-littlearm")
502
+ // Minimum-viable linker script — single .text region at GBA cart base.
503
+ const ld = await cb.stage("ld", () => runArmLd({
504
+ objects,
505
+ linkScript: `OUTPUT_FORMAT("elf32-littlearm")
554
506
  ENTRY(main)
555
507
  MEMORY { ROM (rx) : ORIGIN = 0x08000000, LENGTH = 32M }
556
508
  SECTIONS { .text : { *(.text*) *(.rodata*) } > ROM }
557
509
  `,
558
- });
559
- log += `--- ld ---\n${ld.log || "(ok)"}\n`;
560
- if (ld.exitCode !== 0 || !ld.elf) {
561
- return { ok: false, binary: null, log, exitCode: ld.exitCode || 1, stage: "ld", runtime: "minimal", ...(ld.crash ? { crash: ld.crash } : {}) };
562
- }
510
+ }), (r) => r.elf);
563
511
 
564
- const objcopy = await runArmObjcopy({ elf: ld.elf });
565
- log += `--- objcopy ---\n${objcopy.log || "(ok)"}\n`;
566
- if (objcopy.exitCode !== 0 || !objcopy.binary) {
567
- return { ok: false, binary: null, log, exitCode: objcopy.exitCode || 1, stage: "objcopy", runtime: "minimal", ...(objcopy.crash ? { crash: objcopy.crash } : {}) };
568
- }
512
+ const objcopy = await cb.stage("objcopy", () => runArmObjcopy({ elf: ld.elf }), (r) => r.binary);
569
513
 
570
- return { ok: true, binary: objcopy.binary, log, exitCode: 0, stage: "done", runtime: "minimal", ...(ld.map ? { symbols: ld.map } : {}) };
514
+ return { ok: true, binary: objcopy.binary, log: cb.log, exitCode: 0, stage: "done", runtime: "minimal", ...(ld.map ? { symbols: ld.map } : {}) };
515
+ } catch (e) {
516
+ if (e instanceof BuildError) return e.toResult({ runtime: "minimal" });
517
+ throw e;
518
+ }
571
519
  }
572
520
 
573
521
  /**