From 950e04ed87180b9bb0cc6672454d460b25458065 Mon Sep 17 00:00:00 2001 From: repeat98 Date: Thu, 5 Nov 2026 09:57:09 +0100 Subject: [PATCH 1/2] Fix: publish a C package with no event subscriptions as ABI 8 with its name build.py makes a C module that subscribes to no event an unnamed ABI 4 package, which the publication builder refuses ("Not a supported published runtime ABI"). The host wrapper now repacks it as the ABI 8 package build.py makes for a subscribing one (trailing empty hook words dropped, its stem as its unit name). The loader on the committed base already reads that form; build.py itself stays unchanged, since it is a base input and pinned by the stock recipes. Every published package rebuilds byte for byte. Co-Authored-By: Claude Opus 5.5 --- .../2026-11-05-c-package-no-events-abi8.md | 1 + docs/ADD_A_MODULE.md | 3 +++ scripts/octatrack-module-packages.py | 26 ++++++++++++++++--- sdk/tests/test_module_packages_cli.py | 21 +++++++++++++++ 4 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 changelog/2026-11-05-c-package-no-events-abi8.md diff --git a/changelog/2026-11-05-c-package-no-events-abi8.md b/changelog/2026-11-05-c-package-no-events-abi8.md new file mode 100644 index 000000000..93fe83e91 --- /dev/null +++ b/changelog/2026-11-05-c-package-no-events-abi8.md @@ -0,0 +1 @@ +Fix: an Octatrack C module that subscribes to no event, using only the core tick, draw, key and encoder hooks, can now be published for USB. `npm run octatrack:module-packages` used to stop on its package with "Not a supported published runtime ABI", because the builder made it ABI 4 with no name. It now goes out as ABI 8 with its id as its name on the unit, the form an event-subscribing C module already has, so the unit's inventory names it too. The published base's loader already reads that form, so no base update is needed, and every package already published rebuilds byte for byte. diff --git a/docs/ADD_A_MODULE.md b/docs/ADD_A_MODULE.md index 7fe558c2d..a7ab7c297 100644 --- a/docs/ADD_A_MODULE.md +++ b/docs/ADD_A_MODULE.md @@ -231,6 +231,9 @@ the package is rebuilt. The standalone and table recipes in asset JSON, the remix tools, `module_packages.py`, the Elekloader checkout), so any import of rebuilt packages can stop the full run on them. Re-pin only the asset hashes that moved; `--check` must then call every package identical. +`sdk/runtime/loader/build.py` and `module_packages.py` are base inputs too +(`src/engine/assets/octatrack-base-sources.json`): a change to how packages are +published goes in the host wrapper `scripts/octatrack-module-packages.py`. In the toolchain image set `ELEKLOADER_DSP_ASM`; restore old versions it deletes. A newly developed module selectable in beta module sets must have a working diff --git a/scripts/octatrack-module-packages.py b/scripts/octatrack-module-packages.py index f30a8a90f..c015549d4 100644 --- a/scripts/octatrack-module-packages.py +++ b/scripts/octatrack-module-packages.py @@ -124,6 +124,26 @@ def engine_words(data): return engine_capabilities, engine_words +def titled(data, name): + """A C package that subscribes to no event: build.py makes it ABI 4, with no name, and the site publishes only ABI 6 + and later. Repacked as the ABI 8 package build.py makes for a subscribing one (build_c: its trailing empty hook + words dropped, its stem as its unit name), which every base's loader reads, so the unit's inventory names it too. + Any other package comes back as it is. Here, not in the builder, whose source the base pins.""" + if struct.unpack_from('>H', data, 4)[0] != 4: + return data + spec = importlib.util.spec_from_file_location('octatrack_runtime_builder', APP / 'sdk/runtime/loader/build.py') + runtime = importlib.util.module_from_spec(spec); spec.loader.exec_module(runtime) + flags, image, bss, count, hooks, sites, ident = struct.unpack_from('>HIIIIII', data, 6) + at = 32 + 4 * hooks + if flags or sites or ident != runtime.module_id(name) or len(data) != at + image + 4 * count: + raise ValueError('%s: not the ABI 4 package build.py makes from C sources.' % name) + words = list(struct.unpack_from('>%dI' % hooks, data, 32)) + while words and words[-1] == runtime.NONE: + words.pop() + return runtime.package(data[at:at + image], bss, words, list(struct.unpack_from('>%dI' % count, data, at + image)), + name=name, abi=8, hashed=True, title=name[:15]) + + def main(): spec = importlib.util.spec_from_file_location( 'octatrack_module_packages', APP / 'sdk/machines/octatrack/elekloader/module_packages.py') @@ -146,8 +166,8 @@ def named_build(entry, *args, **kwargs): if build: builder.build = named_build # A C runtime module: its row's "sources", "subscribe" and "raise" go to build.py as they are. It has no card form - # (src/catalog/usb-only.ts). build.py takes its id and unit name from the output's stem, so build at .mwrm. - # Here, not in the builder, whose source the base pins. + # (src/catalog/usb-only.ts). build.py takes its id and unit name from the output's stem, so build at .mwrm; one + # with no events goes out as ABI 8 too (titled()). Here, not in the builder, whose source the base pins. sourced, tools = getattr(builder, 'build', None), getattr(builder, 'tools', None) def c_build(entry, out, *args, **kwargs): if not entry.get('sources'): @@ -157,7 +177,7 @@ def c_build(entry, out, *args, **kwargs): *[arg for hook in entry.get('subscribe', []) for arg in ('--subscribe', hook)], *[arg for event in entry.get('raise', []) for arg in ('--raise', event)], '-o', str(named)], check=True, capture_output=True, text=True, cwd=APP) - out.write_bytes(named.read_bytes()) + out.write_bytes(titled(named.read_bytes(), entry['id'])) return out.read_bytes() if sourced and tools: builder.build = c_build diff --git a/sdk/tests/test_module_packages_cli.py b/sdk/tests/test_module_packages_cli.py index 96047e055..9d32a9206 100644 --- a/sdk/tests/test_module_packages_cli.py +++ b/sdk/tests/test_module_packages_cli.py @@ -104,5 +104,26 @@ def action(): cli.keep_published(builder, rebuild(True)) self.assertEqual((old.read_bytes(), current.read_bytes()), (b'old', b'new')) + def test_a_c_package_with_no_events_goes_out_as_abi_8_with_its_name(self): + modules = {} + for name, path in (('cli', 'scripts/octatrack-module-packages.py'), ('runtime', 'sdk/runtime/loader/build.py'), + ('publisher', 'sdk/machines/octatrack/elekloader/module_packages.py')): + spec = importlib.util.spec_from_file_location('module_packages_titled_' + name, ROOT / path) + modules[name] = importlib.util.module_from_spec(spec) + spec.loader.exec_module(modules[name]) + cli, runtime, publisher = modules['cli'], modules['runtime'], modules['publisher'] + image, hooks = b'\x4e\x75\0\0\0\0\0\0', [0, runtime.NONE, 2, runtime.NONE] # tick and key; no draw or enc + four = runtime.package(image, 8, hooks, [4], name='tiempos') # what build.py makes with no --subscribe + self.assertEqual(four[5], 4) + made = cli.titled(four, 'tiempos') + # build_c's form for a subscribing package, with no subscription: subscribe() drops the trailing empty words + self.assertEqual(made, runtime.package(image, 8, runtime.subscribe(hooks, [], {}), [4], name='tiempos', abi=8, + hashed=True, title='tiempos')) + self.assertEqual(publisher.capabilities(made), dict(abi=8, features=0, sites=0, copies=0, fills=0)) + self.assertEqual((made[28:32], made[50:66].rstrip(b'\0')), (four[28:32], b'tiempos')) # same module id, named + self.assertIs(cli.titled(made, 'tiempos'), made) # a subscribing (ABI 8) one stays as it is + with self.assertRaises(ValueError): + cli.titled(four, 'beat-slicer') # the name must be the one the package's id was made from + if __name__ == '__main__': unittest.main() From 8aec32d1d6fbeb883380ff8f323fb15f354071d6 Mon Sep 17 00:00:00 2001 From: repeat98 Date: Thu, 5 Nov 2026 10:07:59 +0100 Subject: [PATCH 2/2] Fix: the repacking note names the base's loader, not every base's Co-Authored-By: Claude Opus 5.5 --- scripts/octatrack-module-packages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/octatrack-module-packages.py b/scripts/octatrack-module-packages.py index c015549d4..f70cc190e 100644 --- a/scripts/octatrack-module-packages.py +++ b/scripts/octatrack-module-packages.py @@ -127,7 +127,7 @@ def engine_words(data): def titled(data, name): """A C package that subscribes to no event: build.py makes it ABI 4, with no name, and the site publishes only ABI 6 and later. Repacked as the ABI 8 package build.py makes for a subscribing one (build_c: its trailing empty hook - words dropped, its stem as its unit name), which every base's loader reads, so the unit's inventory names it too. + words dropped, its stem as its unit name), which the base's loader reads, so the unit's inventory names it too. Any other package comes back as it is. Here, not in the builder, whose source the base pins.""" if struct.unpack_from('>H', data, 4)[0] != 4: return data