native-compat: remove [api] and [dependencies] before horizon-mixins, as the launcher does (#364)

The launcher strips these launcher-only tables before horizon-mixins reads a
bundle's manifest (native_mods/api.rs). The check passed them through, so a
mod with a trait package (cpm, #317) failed with E_MANIFEST "unknown field api"
while players' launchers apply it. Mods with an API package are applied but
not compiled, as health already leaves them out.

Co-authored-by: Horizon agent <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-11-07 09:35:45 +01:00
1 parent 93a328a007
commit e349124fc4
2 files changed
+77 -1

No files matched your search

+25 -1
View File
@@ -79,7 +79,27 @@ class Bundle:
# As the launcher does for a mod built on another Horizon than it pins.
toml = out / "horizon-mixin.toml"
text = re.sub(r'horizon_revision\s*=\s*"[0-9a-f]{40}"', f'horizon_revision = "{revision}"', toml.read_text())
toml.write_text(text)
toml.write_text(strip_launcher_tables(text))
# The tables only the launcher reads; it removes them before horizon-mixins sees the manifest (native_mods/api.rs).
LAUNCHER_TABLES = ("api", "dependencies")
def strip_launcher_tables(text):
"""`text` without its [api] and [dependencies] tables (and their sub-tables), the rest unchanged."""
kept, skipping = [], False
for line in text.splitlines(keepends=True):
header = re.match(r'\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$', line)
if header:
skipping = header.group(1).split(".")[0].strip().strip('"\'') in LAUNCHER_TABLES
if not skipping:
kept.append(line)
stripped = "".join(kept)
want = {k: v for k, v in tomllib.loads(text).items() if k not in LAUNCHER_TABLES}
if tomllib.loads(stripped) != want:
sys.exit("horizon-mixin.toml: its [api] or [dependencies] table couldn't be removed line by line")
return stripped
def download(url, sha256):
@@ -321,6 +341,10 @@ def check(args):
failed.append(mod.id)
if args.no_compile:
continue
if mod.api:
# Its API package is staged by the launcher only (Bundle.api), as `health` leaves these out too.
print("- applies; it has a mod API package, which only the launcher builds, so it isn't compiled here.")
continue
if compiles(stage):
print(f"- applies and compiles with {len(popular) - len(refused)} popular mods.")
continue
+52
View File
@@ -0,0 +1,52 @@
"""python3 -m unittest discover -s scripts -p 'test_*.py'"""
import tomllib
import unittest
from native_compat import strip_launcher_tables
MANIFEST = """[mod]
id = "example"
version = "1.0.0"
[compatibility]
horizon_revision = "0123456789abcdef0123456789abcdef01234567"
# A trait-mod package.
[api]
path = "api"
[dependencies]
jei = { optional = true }
[dependencies.ae2]
optional = false
[[module]]
id = "runtime"
file = "crates/horizon-client/src/game.rs"
name = "example_mod"
source = "runtime.rs"
[[injection]]
id = "frame"
target = "horizon_client::game::native_hooks::Game<'_>::native_frame"
helper = "frame.rs"
"""
class StripLauncherTables(unittest.TestCase):
def test_removes_only_the_launcher_tables(self):
stripped = tomllib.loads(strip_launcher_tables(MANIFEST))
original = tomllib.loads(MANIFEST)
self.assertNotIn("api", stripped)
self.assertNotIn("dependencies", stripped)
for key in ("mod", "compatibility", "module", "injection"):
self.assertEqual(stripped[key], original[key])
def test_a_manifest_without_them_is_unchanged(self):
plain = MANIFEST.split("# A trait-mod package.")[0] + MANIFEST.split('optional = false\n')[1]
self.assertEqual(strip_launcher_tables(plain), plain)
if __name__ == "__main__":
unittest.main()