feat(pkg): x build-lib / prebuilt consumption — binary packages end-to-end (#64)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 19s
ci / build-and-test (push) Successful in 1m37s
commit-lint / conventional-commits (push) Successful in 1s
docs / build-and-deploy (push) Successful in 26s

The package-manager half of prebuilt binary packages, on top of the compiler
foundation (dynamic system registration + --emit-module).

- x build-lib [module.ludic]: compile a package's module to a per-target native
  dylib under lib/<target>/, with an @rpath install name so a consumer resolves
  it from the content-addressed store.
- x link-flags: print the clang flags (the dylib, an rpath to its store dir,
  -export_dynamic) so any build system links a project's prebuilt module dylibs;
  x app splices them automatically for in-repo builds.
- kind prebuilt is resolved + linked like any dependency; a missing build target
  stays a hard error.

Proven hermetically (macOS-gated, since dylibs are native): a module exporting a
component + an @System(Update) + a function is built with x build-lib, fetched
as a prebuilt dep, and linked into a consumer game that never saw its source —
the module's system mutates the shared world and its function is callable
("3 42"). Package suite 17/0; full suite 87/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-01 07:52:06 +03:00
parent e1537d2d45
commit 075a7b1430
6 changed files with 173 additions and 11 deletions

View file

@ -170,5 +170,41 @@ function cmd_test_pkg() -> int {
ok("x vendor copies the resolved packages into ./vendor")
} else { bad2("x vendor failed", capture_line(`tail -1 {work}/vendor.out`)) }
# ---- prebuilt binary package (issue #64): exported function + system + component
# A module is compiled to a dylib (x build-lib), fetched as a `kind prebuilt`
# dependency, and linked into a consumer game that never saw its source. At load
# the module registers a dynamic component and an @System(Update); the consumer
# also calls an exported function via `extern`. Native/dylib only → macOS-gated.
if not is_darwin() {
skip("prebuilt binary packages (dylib) — native/macOS only")
return report()
}
let tgt = `native-{capture_line("uname -m")}`
let mp = `{proxy}/example.test/manapack`
run(`mkdir -p {mp} && git -C {mp} init -q`)
pt_write(`{mp}/package.ludic`, `package "example.test/manapack"` + nl() + `version "1.0.0"` + nl() + "kind prebuilt" + nl() + `provides "Mana"` + nl() + `targets "{tgt}"` + nl())
pt_write(`{mp}/manapack.ludic`, "program ManaPack {\n function module_init() -> void { world_register_prop(\"Mana\", 2) }\n @System(Update) function mana_regen() -> void {\n let M = world_prop_id(\"Mana\")\n let e = world_query_next(M, 0)\n if e >= 0 { let v = world_get(e, M, 0); world_set(e, M, 0, v + 1) }\n }\n function mana_bonus() -> int { return 42 }\n}\n")
if shq(`( cd {mp} && LUDICC={root}/bin/ludicc {root}/bin/x build-lib manapack.ludic > {work}/bl.out 2>&1 )`) and file_exists(`{mp}/lib/{tgt}/libmanapack.dylib`) {
ok("x build-lib compiles a package module to a per-target dylib")
} else { bad2("x build-lib failed", capture_line(`tail -1 {work}/bl.out`)) }
pt_commit_tag(mp, "v1.0.0")
let proj3 = `{work}/proj3`
pt_write(`{proj3}/package.ludic`, `package "game"` + nl() + `version "0.0.0"` + nl() + `require "example.test/manapack" "1.0.0"` + nl())
pt_write(`{proj3}/game.ludic`, "program Game {\n property Dummy { x: int = 0 }\n model Ent { Dummy }\n @OnSpawn(Ent) handler Init { }\n extern function mana_bonus() -> int = \"fn_mana_bonus\"\n var frames: int = 0\n handler Boot phase Start {\n let e = world_spawn(world_model_id(\"Ent\"))\n let M = world_prop_id(\"Mana\")\n world_attach_dyn(e, M)\n world_set(e, M, 0, 0)\n }\n handler Run phase Update {\n frames = frames + 1\n if frames >= 4 {\n let M = world_prop_id(\"Mana\")\n let e = world_query_next(M, 0)\n print(world_get(e, M, 0))\n print(mana_bonus())\n quit()\n }\n }\n}\n")
if shq(`( cd {proj3} && {envp} {root}/bin/x get > {work}/get3.out 2>&1 )`) and file_exists(`{proj3}/ludic_modules/example.test/manapack/lib/{tgt}/libmanapack.dylib`) {
ok("x get fetches + links a prebuilt (kind prebuilt) package for the target")
} else { bad2("prebuilt x get failed", capture_line(`tail -1 {work}/get3.out`)) }
# compile the ECS consumer from the repo root (so runtime/native resolves), then
# link the module dylib via `x link-flags` (a real external build's link step).
run(`LUDIC_HOME={root} {root}/bin/ludicc --headless {proj3}/game.ludic --emit-llvm -o {proj3}/game.ll 2>{work}/gc.err`)
let flags = capture_line(`( cd {proj3} && LUDIC_STORE={store} {root}/bin/x link-flags )`)
if shq(`{cc()} -O2 -Wno-override-module {proj3}/game.ll {flags} -o {proj3}/game 2>{work}/gl.err`) {
let got = capture_line(`printf 'aaaaaa' | {proj3}/game`)
if got == "3 42" {
ok("a consumer links the prebuilt module: its system mutates the world + its function is callable (\"3 42\")")
} else { bad2("prebuilt consumer output mismatch", `got [{got}] want [3 42]`) }
} else { bad2("prebuilt consumer link failed", capture_line(`tail -1 {work}/gl.err`)) }
return report()
}