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
@@ -0,0 +1,80 @@
1
+ /*
2
+ * GameTank-specific defines
3
+ */
4
+
5
+ #ifndef GAMETANK_H
6
+ #define GAMETANK_H
7
+
8
+ typedef char bool;
9
+ #define true 1
10
+ #define false 0
11
+
12
+ #define audio_reset ((char *) 0x2000)
13
+ #define audio_nmi ((char *) 0x2001)
14
+ #define audio_rate ((char *) 0x2006)
15
+ #define dma_flags ((char *) 0x2007)
16
+ #define bank_reg ((char *) 0x2005)
17
+ #define via ((char*) 0x2800)
18
+ #define aram ((unsigned char *) 0x3000)
19
+ #define vram ((unsigned char *) 0x4000)
20
+ #define vram_VX ((char *) 0x4000)
21
+ #define vram_VY ((char *) 0x4001)
22
+ #define vram_GX ((char *) 0x4002)
23
+ #define vram_GY ((char *) 0x4003)
24
+ #define vram_WIDTH ((char *) 0x4004)
25
+ #define vram_HEIGHT ((char *) 0x4005)
26
+ #define vram_COLOR ((char *) 0x4007)
27
+ #define vram_START ((char *) 0x4006)
28
+
29
+ #define SCREEN_WIDTH 128
30
+ #define SCREEN_HEIGHT 128
31
+
32
+ #define DMA_ENABLE 1
33
+ #define DMA_PAGE_OUT 2
34
+ #define DMA_NMI 4
35
+ #define DMA_COLORFILL_ENABLE 8
36
+ #define DMA_GCARRY 16
37
+ #define DMA_CPU_TO_VRAM 32
38
+ #define DMA_IRQ 64
39
+ #define DMA_OPAQUE 128
40
+
41
+ #define BANK_GRAM_MASK 7
42
+ #define BANK_SECOND_FRAMEBUFFER 8
43
+ #define BANK_CLIP_X 16
44
+ #define BANK_CLIP_Y 32
45
+ #define BANK_RAM_MASK 192
46
+
47
+ #define GRAM_PAGE(x) x
48
+
49
+ #define VX 0
50
+ #define VY 1
51
+ #define GX 2
52
+ #define GY 3
53
+ #define WIDTH 4
54
+ #define HEIGHT 5
55
+ #define START 6
56
+ #define COLOR 7
57
+
58
+ #define gamepad_1 ((volatile char *) 0x2008)
59
+ #define gamepad_2 ((volatile char *) 0x2009)
60
+
61
+ #define ORB 0
62
+ #define ORA 1
63
+ #define DDRB 2
64
+ #define DDRA 3
65
+ #define T1C 5
66
+ #define ACR 11
67
+ #define PCR 12
68
+ #define IFR 13
69
+ #define IER 14
70
+
71
+ extern char frameflag, frameflip, flagsMirror, banksMirror, bankflip;
72
+
73
+ void wait();
74
+ void nop5();
75
+ void nop10();
76
+
77
+ #define PROFILER_START(x) via[ORB] = 0x80; via[ORB] = x;
78
+ #define PROFILER_END(x) via[ORB] = 0x80; via[ORB] = x | 0x40;
79
+
80
+ #endif
@@ -0,0 +1,32 @@
1
+ # GameTank SDK-runtime single-bank (EEPROM32K) ld65 config. Like single-bank.cfg
2
+ # but adds the segments the bundled SDK runtime uses (LOADERS / COMMON / SAVE /
3
+ # WAVES) — in the multi-bank SDK build these live in dedicated flash banks; for the
4
+ # single 32 KB cart they all fold into the one ROM region (SAVE → RAM). Flat 32 KB
5
+ # image = a valid .gtr (EEPROM32K). VECTORS pinned to $FFFA.
6
+ MEMORY {
7
+ ZP: start = $0000, size = $0100, type = rw, define = yes;
8
+ RAM: start = $0200, size = $1D00, define = yes;
9
+ ROM: start = $8000, size = $8000, file = %O, fill = yes, define = yes;
10
+ }
11
+ SEGMENTS {
12
+ ZEROPAGE: load = ZP, type = zp, define = yes;
13
+ DATA: load = ROM, type = rw, define = yes, run = RAM;
14
+ BSS: load = RAM, type = bss, define = yes;
15
+ HEAP: load = RAM, type = bss, optional = yes;
16
+ STARTUP: load = ROM, type = ro;
17
+ ONCE: load = ROM, type = ro, optional = yes;
18
+ CODE: load = ROM, type = ro;
19
+ RODATA: load = ROM, type = ro;
20
+ LOADERS: load = ROM, type = ro, optional = yes; # SDK sprite/blit loaders
21
+ COMMON: load = ROM, type = ro, optional = yes; # SDK shared code/data
22
+ WAVES: load = ROM, type = ro, optional = yes; # SDK audio wave tables
23
+ SAVE: load = RAM, type = bss, optional = yes; # persist (battery SRAM) — RAM-backed here
24
+ VECTORS: load = ROM, type = ro, start = $FFFA;
25
+ }
26
+ FEATURES {
27
+ CONDES: type = constructor, label = __CONSTRUCTOR_TABLE__, count = __CONSTRUCTOR_COUNT__, segment = ONCE;
28
+ CONDES: type = destructor, label = __DESTRUCTOR_TABLE__, count = __DESTRUCTOR_COUNT__, segment = RODATA;
29
+ }
30
+ SYMBOLS {
31
+ __STACKSIZE__: type = weak, value = $0200;
32
+ }
@@ -0,0 +1,61 @@
1
+ ; ---------------------------------------------------------------------------
2
+ ; GameTank SDK-runtime crt0 (single-bank EEPROM32K, with the bundled SDK gfx/
3
+ ; input/random/ACP runtime). Adapted from gametank_sdk src/gt/crt0.s, MIT — but
4
+ ; STRIPPED of flash banking + the audio-FW asset: a single 32 KB cart maps at
5
+ ; boot, so there's no flash bank to shift in, and the bare runtime has no
6
+ ; asset-pipeline audio firmware. Calls _sdk_init (init_graphics + ACP) then
7
+ ; _main. The SDK's interrupt.s provides _nmi_int / _irq_int, so this crt0 does
8
+ ; NOT define them (that was the duplicate-symbol clash with the bare crt0).
9
+ ; ---------------------------------------------------------------------------
10
+ .export _init, _exit
11
+ .import _main, _sdk_init
12
+ .export __STARTUP__ : absolute = 1
13
+ .import __RAM_START__, __RAM_SIZE__
14
+ .import copydata, zerobss, initlib, donelib
15
+
16
+ .PC02
17
+
18
+ BankReg = $2005
19
+ VIA = $2800
20
+ DDRA = 3
21
+ ORAr = 1
22
+
23
+ .include "zeropage.inc"
24
+
25
+ .segment "STARTUP"
26
+
27
+ _init: LDX #$FF
28
+ TXS
29
+ CLD
30
+
31
+ LDX #0
32
+ viaWakeup:
33
+ INX
34
+ BNE viaWakeup
35
+
36
+ STZ BankReg ; single 32K cart: fixed bank, nothing to shift
37
+ STZ $1FFF
38
+
39
+ LDA #%00000111 ; VIA DDRA: banking pins as output
40
+ STA VIA+DDRA
41
+ LDA #$FF
42
+ STA VIA+ORAr
43
+
44
+ ; cc65 C argument-stack pointer = top of work RAM
45
+ LDA #<(__RAM_START__ + __RAM_SIZE__)
46
+ STA c_sp
47
+ LDA #>(__RAM_START__ + __RAM_SIZE__)
48
+ STA c_sp+1
49
+
50
+ JSR zerobss
51
+ JSR copydata
52
+ JSR initlib
53
+
54
+ JSR _sdk_init ; init_graphics + ACP (the bundled runtime)
55
+ CLI ; enable IRQs — the draw queue + ACP are
56
+ ; INTERRUPT-DRIVEN (blit-done IRQ processes
57
+ ; queue_draw_box; without CLI nothing draws)
58
+ JSR _main
59
+
60
+ _exit: JSR donelib
61
+ BRK
@@ -0,0 +1,11 @@
1
+ ; GameTank SDK-runtime vector table at $FFFA. The interrupt handlers come from the
2
+ ; bundled SDK runtime (interrupt.s exports _nmi_int / _irq_int), so this just
3
+ ; imports + points the table at them. (The bare single-bank.vectors.s defines its
4
+ ; own rti handlers; the SDK preset uses the SDK's real ones.)
5
+ .import _init, _nmi_int, _irq_int
6
+
7
+ .segment "VECTORS"
8
+
9
+ .addr _nmi_int ; $FFFA NMI
10
+ .addr _init ; $FFFC RESET
11
+ .addr _irq_int ; $FFFE IRQ / BRK
@@ -0,0 +1,28 @@
1
+ # GameTank single-bank (EEPROM32K) ld65 config — the Tier-A "bare" path.
2
+ # Collapses the SDK's multi-bank 2 MB linkerConfig.js to ONE 32 KB ROM bank at
3
+ # $8000-$FFFF (no flash SPI, no Node bank-cat, no zopfli). ZP/RAM match the SDK
4
+ # (ZP $0, stack $0100, work RAM $0200-$1EFF). VECTORS at $FFFA. Output is a flat
5
+ # 32 KB image that IS a valid .gtr (the core size-detects the EEPROM32K mapper).
6
+ MEMORY {
7
+ ZP: start = $0000, size = $0100, type = rw, define = yes;
8
+ RAM: start = $0200, size = $1D00, define = yes;
9
+ ROM: start = $8000, size = $8000, file = %O, fill = yes, define = yes;
10
+ }
11
+ SEGMENTS {
12
+ ZEROPAGE: load = ZP, type = zp, define = yes;
13
+ DATA: load = ROM, type = rw, define = yes, run = RAM;
14
+ BSS: load = RAM, type = bss, define = yes;
15
+ HEAP: load = RAM, type = bss, optional = yes;
16
+ STARTUP: load = ROM, type = ro;
17
+ ONCE: load = ROM, type = ro, optional = yes;
18
+ CODE: load = ROM, type = ro;
19
+ RODATA: load = ROM, type = ro;
20
+ VECTORS: load = ROM, type = ro, start = $FFFA;
21
+ }
22
+ FEATURES {
23
+ CONDES: type = constructor, label = __CONSTRUCTOR_TABLE__, count = __CONSTRUCTOR_COUNT__, segment = ONCE;
24
+ CONDES: type = destructor, label = __DESTRUCTOR_TABLE__, count = __DESTRUCTOR_COUNT__, segment = RODATA;
25
+ }
26
+ SYMBOLS {
27
+ __STACKSIZE__: type = weak, value = $0200;
28
+ }
@@ -0,0 +1,71 @@
1
+ ; ---------------------------------------------------------------------------
2
+ ; GameTank Tier-A crt0 (single-bank EEPROM32K, no SDK runtime).
3
+ ; Adapted from gametank_sdk src/gt/crt0.s, MIT (Clyde Shaffer). STRIPPED for the
4
+ ; bare 32 KB path: NO flash bank shift-out (the whole game is in one bank that
5
+ ; maps at boot), NO _sdk_init, NO _AudioFWPkg .incbin. Just: stack init, VIA
6
+ ; wakeup, zerobss/copydata/constructors, set the cc65 C arg-stack pointer, call
7
+ ; main(). A default NMI/IRQ handler (rti) lives here so a bare game links without
8
+ ; defining its own.
9
+ ; ---------------------------------------------------------------------------
10
+ .export _init, _exit
11
+ .export _nmi_int, _irq_int
12
+ .import _main
13
+ .export __STARTUP__ : absolute = 1
14
+ .import __RAM_START__, __RAM_SIZE__
15
+ .import copydata, zerobss, initlib, donelib
16
+
17
+ .PC02 ; W65C02 opcode set (stz/bra/phx/…)
18
+
19
+ BankReg = $2005
20
+ VIA = $2800
21
+ DDRA = 3
22
+ ORAr = 1
23
+
24
+ .include "zeropage.inc"
25
+
26
+ .segment "STARTUP"
27
+
28
+ _init: LDX #$FF ; init stack pointer to $01FF
29
+ TXS
30
+ CLD
31
+
32
+ LDX #0 ; brief VIA wakeup delay
33
+ viaWakeup:
34
+ INX
35
+ BNE viaWakeup
36
+
37
+ ; Park the banking register at a known state. With a single 32 KB cart
38
+ ; the active bank is fixed at boot, so we don't shift a flash bank in.
39
+ STZ BankReg
40
+ STZ $1FFF
41
+
42
+ LDA #%00000111 ; VIA DDRA: low 3 bits output (banking pins)
43
+ STA VIA+DDRA
44
+ LDA #$FF
45
+ STA VIA+ORAr
46
+
47
+ ; ---------------------------------------------------------------------------
48
+ ; cc65 C argument-stack pointer = top of work RAM
49
+ LDA #<(__RAM_START__ + __RAM_SIZE__)
50
+ STA c_sp
51
+ LDA #>(__RAM_START__ + __RAM_SIZE__)
52
+ STA c_sp+1
53
+
54
+ ; ---------------------------------------------------------------------------
55
+ JSR zerobss ; clear BSS
56
+ JSR copydata ; copy initialized DATA to RAM
57
+ JSR initlib ; run constructors
58
+
59
+ JSR _main
60
+
61
+ _exit: JSR donelib ; run destructors
62
+ BRK
63
+
64
+ ; ---------------------------------------------------------------------------
65
+ ; Default interrupt handlers — a bare game that doesn't define its own still
66
+ ; links. (A game CAN provide _nmi_int/_irq_int; cc65's linker lets a strong
67
+ ; symbol from the game override these .export'd ones... so keep them weak by
68
+ ; only defining if absent — here they're plain rti fallbacks.)
69
+ _nmi_int:
70
+ _irq_int:
71
+ RTI
@@ -0,0 +1,12 @@
1
+ ; ---------------------------------------------------------------------------
2
+ ; GameTank 6502 vector table at $FFFA (NMI / RESET / IRQ-BRK).
3
+ ; Adapted from gametank_sdk src/gt/vectors.s, MIT. The handlers are exported by
4
+ ; the Tier-A crt0 (rti defaults) — a game can override _nmi_int/_irq_int.
5
+ ; ---------------------------------------------------------------------------
6
+ .import _init, _nmi_int, _irq_int
7
+
8
+ .segment "VECTORS"
9
+
10
+ .addr _nmi_int ; $FFFA NMI
11
+ .addr _init ; $FFFC RESET
12
+ .addr _irq_int ; $FFFE IRQ / BRK
@@ -0,0 +1,109 @@
1
+ // Shared C-SDK build-pipeline helper (0.81.0). The gba-c / genesis-c / mips-c / sh-c
2
+ // builders all walk the SAME spine — compile each .c (cc1 → as), assemble raw .s,
3
+ // assemble a crt0, link, optionally objcopy, finalize — and at EVERY stage repeat the
4
+ // identical 5-part dance:
5
+ //
6
+ // const r = await runX(...);
7
+ // log += `--- stage ---\n${r.log || "(ok)"}\n`;
8
+ // if (r.exitCode !== 0 || !r.<output>)
9
+ // return { ok: false, binary: null, log, exitCode: r.exitCode || 1, stage, ...(r.crash ? { crash } : {}) };
10
+ //
11
+ // That `if (...) return { ok:false, ... }` line appeared ~73 times across the 5 builders.
12
+ // CBuild owns it: each builder makes one `new CBuild()`, calls `.stage(name, () => runX(...), pick)`,
13
+ // and the helper accumulates the log + THROWS a BuildError on any failure. The builder
14
+ // wraps its body in try/catch and turns a BuildError into the same `{ ok:false, ... }` shape
15
+ // it returned before — so the public contract is byte-for-byte unchanged, the boilerplate
16
+ // is gone, and each builder keeps only its genuinely-per-platform wiring (which crt0, which
17
+ // libs, finalize). No behavior change.
18
+
19
+ /**
20
+ * Thrown by CBuild.stage() when a stage fails. Carries everything the builder's
21
+ * error return needs. The builder catches it and maps to its result object.
22
+ */
23
+ export class BuildError extends Error {
24
+ /** @param {{stage:string, exitCode:number, log:string, crash?:any}} info */
25
+ constructor(info) {
26
+ super(`build failed at stage: ${info.stage}`);
27
+ this.name = "BuildError";
28
+ this.stage = info.stage;
29
+ this.exitCode = info.exitCode;
30
+ this.log = info.log;
31
+ if (info.crash) this.crash = info.crash;
32
+ }
33
+ /**
34
+ * The canonical failure result the GCC/tcc C-SDK builders return (includes
35
+ * `ok: false`). Pass extra fields (e.g. a platform-specific `{ runtime }`) to merge.
36
+ * @param {Record<string, any>} [extra]
37
+ */
38
+ toResult(extra = {}) {
39
+ return { ok: false, ...this.fields(extra) };
40
+ }
41
+
42
+ /**
43
+ * The failure fields WITHOUT a forced `ok` — for builders (cc65/sdcc) whose
44
+ * lower layer returns `{ binary, log, exitCode, stage }` and lets the caller
45
+ * (index.js) add `ok`. Same { binary:null, log, exitCode, stage, crash? } core
46
+ * + any `extra` (e.g. sdcc's `failedTU` / `compiledOK`). Note: crash is only
47
+ * spread when present (cc65/sdcc historically never carried it — pass nothing).
48
+ * @param {Record<string, any>} [extra]
49
+ */
50
+ fields(extra = {}) {
51
+ return {
52
+ binary: null,
53
+ log: this.log,
54
+ exitCode: this.exitCode,
55
+ stage: this.stage,
56
+ ...(this.crash ? { crash: this.crash } : {}),
57
+ ...extra,
58
+ };
59
+ }
60
+ }
61
+
62
+ /**
63
+ * A build-log accumulator + stage runner. One per build.
64
+ *
65
+ * Usage:
66
+ * const cb = new CBuild();
67
+ * const cc = await cb.stage(`cc1 (${name})`, () => runCc1x({...}), r => r.asmSource);
68
+ * // cc is the full run result; cb.log has the accumulated log.
69
+ * // a failure THROWS BuildError — wrap the build body in try/catch.
70
+ */
71
+ export class CBuild {
72
+ constructor() {
73
+ /** @type {string} accumulated build log */
74
+ this.log = "";
75
+ }
76
+
77
+ /**
78
+ * Run one toolchain stage, append its log, and throw BuildError on failure.
79
+ *
80
+ * @template T
81
+ * @param {string} name stage label — the failure `stage` field AND, by default,
82
+ * the log header. Most builders use one string for both.
83
+ * @param {() => Promise<T>} run the toolchain call (returns {log, exitCode, crash?, ...output})
84
+ * @param {(r: T) => any} pick selects the stage's required output (e.g. r => r.object);
85
+ * a falsy result is treated as failure even if exitCode is 0
86
+ * @param {{logName?:string}} [opts] `logName` overrides ONLY the log header when a
87
+ * builder historically logged a label that differs from the
88
+ * failure `stage` string (e.g. log "as (x → .o)" / stage "as (x)").
89
+ * @returns {Promise<T>} the full run result (on success)
90
+ */
91
+ async stage(name, run, pick, opts = {}) {
92
+ const r = await run();
93
+ this.log += `--- ${opts.logName ?? name} ---\n${r.log || "(ok)"}\n`;
94
+ if (r.exitCode !== 0 || !pick(r)) {
95
+ throw new BuildError({
96
+ stage: name,
97
+ exitCode: r.exitCode || 1,
98
+ log: this.log,
99
+ crash: r.crash,
100
+ });
101
+ }
102
+ return r;
103
+ }
104
+
105
+ /** Append a freeform note to the log (e.g. a sub-step that isn't a toolchain stage). */
106
+ note(text) {
107
+ this.log += text.endsWith("\n") ? text : text + "\n";
108
+ }
109
+ }
@@ -0,0 +1,164 @@
1
+ // Shared GCC-family toolchain factory (0.81.0). The arm/m68k/mips/sh "gcc" wrappers
2
+ // were ~73% identical: each exported runCc1X / runXAs / runXLd / runXObjcopy that did
3
+ // the same input-marshalling + runIsolated() + output-decoding, differing only in:
4
+ // - the npm package the glue ships in + the glue filenames
5
+ // - the per-stage arch flags (cc1 / as / ld), which for MIPS depend on endian
6
+ // - the linker-script filename + the objcopy output filename
7
+ // makeGccToolchain(config) returns the 4 run functions, so each arch shrinks to a
8
+ // small config object. Glue resolution is lazy+memoized (booting never loads a
9
+ // toolchain package until something is actually built with it).
10
+ //
11
+ // All four stages keep the exact pre-refactor argv, /work paths, output encodings,
12
+ // and return shapes (including `...(r.crash ? { crash, stage:"crash" } : {})`), so
13
+ // this is a pure de-duplication — no behavior change.
14
+ import { runIsolated, getOutputBytes, getOutputText } from "../_worker/run.js";
15
+ import { makeGlueResolver, marshalInputs } from "./wasm-tool.js";
16
+
17
+ /**
18
+ * @typedef {Object} GccArchConfig
19
+ * @property {string} pkg npm package the glue ships in
20
+ * @property {string} localDir the wrapper's __dirname (dev fallback base)
21
+ * @property {string} label tool label for "not found" errors
22
+ * @property {Object} glue glue filenames per stage
23
+ * @property {string} glue.cc1 e.g. "cc1-m68k.mjs"
24
+ * @property {string} glue.as e.g. "m68k-elf-as.mjs"
25
+ * @property {string} glue.ld e.g. "m68k-elf-ld.mjs"
26
+ * @property {string} glue.objcopy e.g. "m68k-elf-objcopy.mjs"
27
+ * @property {string[]|((endian:string)=>string[])} cc1Flags arch flags for cc1 (before -iquote/-I)
28
+ * @property {string[]|((endian:string)=>string[])} asFlags arch flags for as (before -I)
29
+ * @property {string[]|((endian:string)=>string[])} [ldFlags] arch flags for ld (before -T); default []
30
+ * @property {string} ldScriptName name the link script is mounted as (e.g. "genesis.ld")
31
+ * @property {string} outputName objcopy output filename (e.g. "main.bin", "main.gba")
32
+ * @property {string} [defaultEndian] "big"|"little" — only meaningful when flags are fns
33
+ */
34
+
35
+ /** Resolve a flags entry that may be a constant array or an endian-dependent fn. */
36
+ function flags(entry, endian) {
37
+ return typeof entry === "function" ? entry(endian) : (entry ?? []);
38
+ }
39
+
40
+ /**
41
+ * Build the 4 GCC-stage run functions for one architecture.
42
+ * @param {GccArchConfig} config
43
+ * @returns {{ runCc1:Function, runAs:Function, runLd:Function, runObjcopy:Function }}
44
+ */
45
+ export function makeGccToolchain(config) {
46
+ const glue = makeGlueResolver({ pkg: config.pkg, localDir: config.localDir, label: config.label });
47
+ const endianOf = (args) => args.endian ?? config.defaultEndian ?? "big";
48
+
49
+ /** cc1: C source → assembly (.s). */
50
+ async function runCc1(args) {
51
+ const { source, options = [] } = args;
52
+ const inputFiles = marshalInputs({ primary: { name: "main.c", text: source }, text: args.headers ?? {} });
53
+ const argv = [
54
+ ...flags(config.cc1Flags, endianOf(args)),
55
+ "-iquote", "/work",
56
+ "-I", "/work",
57
+ ...options,
58
+ "/work/main.c",
59
+ "-o", "/work/main.s",
60
+ ];
61
+ const r = await runIsolated({
62
+ gluePath: glue(config.glue.cc1),
63
+ argv,
64
+ inputFiles,
65
+ outputFiles: [{ vfsPath: "/work/main.s", encoding: "utf8" }],
66
+ });
67
+ return {
68
+ log: r.log,
69
+ exitCode: r.exitCode,
70
+ asmSource: getOutputText(r, "/work/main.s") || null,
71
+ ...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
72
+ };
73
+ }
74
+
75
+ /** as: assembly (.s) → object (.o). */
76
+ async function runAs(args) {
77
+ const { source, options = [] } = args;
78
+ const inputFiles = marshalInputs({
79
+ primary: { name: "main.s", text: source },
80
+ text: args.includes ?? {},
81
+ binary: args.binaryIncludes ?? {},
82
+ });
83
+ const argv = [
84
+ ...flags(config.asFlags, endianOf(args)),
85
+ "-I", "/work",
86
+ ...options,
87
+ "/work/main.s",
88
+ "-o", "/work/main.o",
89
+ ];
90
+ const r = await runIsolated({
91
+ gluePath: glue(config.glue.as),
92
+ argv,
93
+ inputFiles,
94
+ outputFiles: [{ vfsPath: "/work/main.o", encoding: "base64" }],
95
+ });
96
+ return {
97
+ log: r.log,
98
+ exitCode: r.exitCode,
99
+ object: getOutputBytes(r, "/work/main.o"),
100
+ ...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
101
+ };
102
+ }
103
+
104
+ /** ld: objects + link script → ELF (+ map). */
105
+ async function runLd(args) {
106
+ const { objects, linkScript, libraries = [], libraryPaths = [], options = [] } = args;
107
+ const inputFiles = marshalInputs({
108
+ text: { [config.ldScriptName]: linkScript },
109
+ binary: { ...objects, ...(args.archives ?? {}) },
110
+ });
111
+ const argv = [
112
+ ...flags(config.ldFlags, endianOf(args)),
113
+ "-T", "/work/" + config.ldScriptName,
114
+ "-o", "/work/main.elf",
115
+ "-Map=/work/main.map",
116
+ ...libraryPaths.flatMap((p) => ["-L", p]),
117
+ ...Object.keys(objects).map((n) => "/work/" + n),
118
+ ...libraries.map((l) => `-l${l}`),
119
+ ...options,
120
+ ];
121
+ const r = await runIsolated({
122
+ gluePath: glue(config.glue.ld),
123
+ argv,
124
+ inputFiles,
125
+ outputFiles: [
126
+ { vfsPath: "/work/main.elf", encoding: "base64" },
127
+ { vfsPath: "/work/main.map", encoding: "utf8" },
128
+ ],
129
+ });
130
+ return {
131
+ log: r.log,
132
+ exitCode: r.exitCode,
133
+ elf: getOutputBytes(r, "/work/main.elf"),
134
+ map: getOutputText(r, "/work/main.map") || null,
135
+ ...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
136
+ };
137
+ }
138
+
139
+ /** objcopy: ELF → raw binary. */
140
+ async function runObjcopy(args) {
141
+ const { elf, options = [] } = args;
142
+ const inputFiles = marshalInputs({ binary: { "main.elf": elf } });
143
+ const argv = [
144
+ "-O", "binary",
145
+ ...options,
146
+ "/work/main.elf",
147
+ "/work/" + config.outputName,
148
+ ];
149
+ const r = await runIsolated({
150
+ gluePath: glue(config.glue.objcopy),
151
+ argv,
152
+ inputFiles,
153
+ outputFiles: [{ vfsPath: "/work/" + config.outputName, encoding: "base64" }],
154
+ });
155
+ return {
156
+ log: r.log,
157
+ exitCode: r.exitCode,
158
+ binary: getOutputBytes(r, "/work/" + config.outputName),
159
+ ...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
160
+ };
161
+ }
162
+
163
+ return { runCc1, runAs, runLd, runObjcopy };
164
+ }
@@ -0,0 +1,101 @@
1
+ // Shared WASM-toolchain plumbing (0.81.0). Every toolchain wrapper used to
2
+ // re-implement the SAME two things:
3
+ // 1. Resolve a tool's emscripten glue from its npm package, with a local
4
+ // src/ fallback for dev, throwing an install hint if neither exists.
5
+ // 2. Marshal { name: text|bytes } maps into the runIsolated() inputFiles[]
6
+ // shape (textFile / binaryFile per entry).
7
+ // This module owns both so the per-tool wrappers carry only what's genuinely
8
+ // tool-specific (package name, glue filename, argv, output decoding).
9
+ //
10
+ // Resolution stays LAZY at the call site (the wrappers memoize) so booting the
11
+ // server never touches a 100MB+ toolchain package unless that toolchain is
12
+ // actually used to build something.
13
+ import { existsSync } from "node:fs";
14
+ import { fileURLToPath } from "node:url";
15
+ import path from "node:path";
16
+ import { textFile, binaryFile } from "../_worker/run.js";
17
+
18
+ /**
19
+ * Resolve a single tool glue FILE from its package, falling back to a local
20
+ * in-tree copy. This is the arm/m68k/mips/sh-gcc + asar + wladx + rgbds + dasm +
21
+ * vasm68k + sjasm + tcc816 shape (one .js/.mjs glue per tool).
22
+ *
23
+ * @param {Object} a
24
+ * @param {string} a.pkg npm package the glue ships in (e.g. "romdev-platform-gba")
25
+ * @param {string} a.file glue filename inside the package's wasm/ dir (e.g. "cc1-arm.mjs")
26
+ * @param {string} a.localDir the wrapper's __dirname (its wasm/ holds the dev fallback)
27
+ * @param {string} [a.label] tool name for the "not found" error (defaults to file)
28
+ * @returns {string} absolute path to the glue file
29
+ */
30
+ export function resolveGlueFile({ pkg, file, localDir, label }) {
31
+ try {
32
+ const u = import.meta.resolve(pkg);
33
+ const p = path.join(path.dirname(fileURLToPath(u)), "wasm", file);
34
+ if (existsSync(p)) return p;
35
+ } catch { /* package not resolvable — fall through to local */ }
36
+ const local = path.join(localDir, "wasm", file);
37
+ if (existsSync(local)) return local;
38
+ throw new Error(`${label ?? file} WASM not found — install ${pkg}`);
39
+ }
40
+
41
+ /**
42
+ * Resolve a tool's BASE directory (the caller derives wasm/ + share/ from it).
43
+ * This is the cc65 + sdcc shape, where one package holds several tools + shared
44
+ * data and the wrapper wants the root, validated by a sentinel file's presence.
45
+ *
46
+ * @param {Object} a
47
+ * @param {string} a.pkg npm package (e.g. "romdev-toolchain-cc65")
48
+ * @param {string} a.sentinel a path under the base that must exist (e.g. "wasm/cc65.js")
49
+ * @param {string} a.localDir the wrapper's __dirname (the dev fallback base)
50
+ * @param {string} [a.label] tool name for the "not found" error
51
+ * @returns {string} absolute path to the base dir
52
+ */
53
+ export function resolveToolBaseDir({ pkg, sentinel, localDir, label }) {
54
+ try {
55
+ const u = import.meta.resolve(pkg);
56
+ const dir = path.dirname(fileURLToPath(u));
57
+ if (existsSync(path.join(dir, sentinel))) return dir;
58
+ } catch { /* package not resolvable — fall through to local */ }
59
+ if (existsSync(path.join(localDir, sentinel))) return localDir;
60
+ throw new Error(`${label ?? pkg} WASM not found — install ${pkg}`);
61
+ }
62
+
63
+ /**
64
+ * A lazy, memoized glue resolver bound to a package + local dir. Returns a
65
+ * function `glue(file)` that resolves+caches each glue path on first use.
66
+ * Replaces the `const _glue = {}; const xGlue = (f) => (_glue[f] ??= ...)`
67
+ * boilerplate every multi-tool wrapper repeats.
68
+ *
69
+ * @param {Object} a
70
+ * @param {string} a.pkg
71
+ * @param {string} a.localDir
72
+ * @param {string} [a.label]
73
+ * @returns {(file: string) => string}
74
+ */
75
+ export function makeGlueResolver({ pkg, localDir, label }) {
76
+ const cache = {};
77
+ return (file) =>
78
+ (cache[file] ??= resolveGlueFile({ pkg, file, localDir, label }));
79
+ }
80
+
81
+ /**
82
+ * Marshal text + binary virtual-file maps into the runIsolated() inputFiles[]
83
+ * array, all rooted at /work. Order: the primary source first (if given), then
84
+ * text entries, then binary entries — matching what the wrappers built by hand.
85
+ *
86
+ * @param {Object} a
87
+ * @param {{name: string, text: string}} [a.primary] the main source (e.g. main.c → /work/main.c)
88
+ * @param {Record<string,string>} [a.text] virtual text files (headers, includes, link scripts)
89
+ * @param {Record<string,Uint8Array>} [a.binary] virtual binary files (objects, archives, binary includes)
90
+ * @returns {import("../_worker/run.js").InputFile[]}
91
+ */
92
+ export function marshalInputs({ primary, text = {}, binary = {} } = {}) {
93
+ /** @type {import("../_worker/run.js").InputFile[]} */
94
+ const files = [];
95
+ if (primary) files.push(textFile("/work/" + primary.name, primary.text));
96
+ for (const [name, content] of Object.entries(text))
97
+ files.push(textFile("/work/" + name, content));
98
+ for (const [name, bytes] of Object.entries(binary))
99
+ files.push(binaryFile("/work/" + name, bytes));
100
+ return files;
101
+ }