# pkg_test.ludic — the package-manager suite (issue #63), hermetic and offline. # # It stands up throwaway git repositories under /tmp and points the package # manager at them with LUDIC_PKG_PROXY, so the whole fetch → MVS resolve → # content-addressed store → project link → compile → run → verify pipeline runs # with no network. Every check drives a fresh `bin/ludic` / `bin/ludicc` process the # way a real project would. # write `body` to `path`, creating parent directories function pt_write(path: pointer, body: pointer) -> void { shell(`mkdir -p "$(dirname {path})"`) write_file(path, body) } # commit the current tree of `dir` and tag it `tag` function pt_commit_tag(dir: pointer, tag: pointer) -> void { shell(`git -C {dir} add -A`) shell(`git -C {dir} -c user.email=t@t.test -c user.name=tester commit -q -m {tag}`) shell(`git -C {dir} tag {tag}`) } function cmd_test_pkg() -> int { PASS = 0 FAIL = 0 print("== package manager (issue #63): fetch + MVS resolve + store + link + build ==") if not shq("command -v git >/dev/null 2>&1") { skip("git unavailable — package-manager suite needs git") return report() } let root = capture_line("pwd") let work = `{tmp_dir()}/pkg_test` let proxy = `{work}/proxy` let store = `{work}/store` let proj = `{work}/proj` shell(`rm -rf {work}`) shell(`mkdir -p {proxy} {store} {proj}`) # ---- fixture packages ----------------------------------------------------- # example.test/util: a leaf source package, two published versions. let util = `{proxy}/example.test/util` shell(`git -C {util} init -q 2>/dev/null || ( mkdir -p {util} && git -C {util} init -q )`) pt_write(`{util}/package.ludic`, `package "example.test/util"` + nl() + `version "1.0.0"` + nl() + "kind source" + nl() + `provides "Util"` + nl()) pt_write(`{util}/util.ludic`, "function util_tag() -> pointer { return \"u1\" }\n") pt_commit_tag(util, "v1.0.0") pt_write(`{util}/package.ludic`, `package "example.test/util"` + nl() + `version "1.2.0"` + nl() + "kind source" + nl() + `provides "Util"` + nl()) pt_write(`{util}/util.ludic`, "function util_tag() -> pointer { return \"u12\" }\n") pt_commit_tag(util, "v1.2.0") # example.test/greeter: requires util >=1.0.0, provides Greet. let greeter = `{proxy}/example.test/greeter` shell(`mkdir -p {greeter} && git -C {greeter} init -q`) pt_write(`{greeter}/package.ludic`, `package "example.test/greeter"` + nl() + `version "1.0.0"` + nl() + "kind source" + nl() + `provides "Greet"` + nl() + `require "example.test/util" "1.0.0"` + nl()) pt_write(`{greeter}/greet.ludic`, "function greet_hello() -> pointer { return \"hi\" }\n") pt_commit_tag(greeter, "v1.0.0") # ---- the consumer project ------------------------------------------------- # requires util 1.2.0 directly; greeter (which requires util 1.0.0) is added # too. MVS must pick util 1.2.0 — the greatest required minimum. pt_write(`{proj}/package.ludic`, `package "app"` + nl() + `version "0.0.0"` + nl() + `require "example.test/greeter" "1.0.0"` + nl() + `require "example.test/util" "1.2.0"` + nl()) pt_write(`{proj}/app.ludic`, "program App {\n import \"example.test/greeter/greet.ludic\"\n import \"example.test/util/util.ludic\"\n entry { print(greet_hello()); print(util_tag()) }\n}\n") let envp = `LUDIC_PKG_PROXY={proxy} LUDIC_STORE={store}` # ---- ludic get: resolve, fetch, store, link, lock ----------------------------- if shq(`( cd {proj} && {envp} {root}/bin/ludic get > {work}/get.out 2>&1 )`) { ok("ludic get resolves + fetches the dependency graph") } else { bad2("ludic get failed", capture_line(`tail -2 {work}/get.out`)) } var lock = read_file(`{proj}/package.lock.ludic`) if lock == null { lock = "" } if s_contains(lock, "example.test/greeter") and s_contains(lock, "example.test/util") { ok("package.lock.ludic pins the whole build list") } else { bad("lockfile is missing a resolved package") } # MVS: util resolves to 1.2.0 (max of the 1.0.0 and 1.2.0 minimums), not 1.0.0 if s_contains(lock, `"example.test/util" version "1.2.0"`) { ok("MVS selects util 1.2.0 (greatest required minimum)") } else { bad2("MVS picked the wrong util version", capture_line(`grep util {proj}/package.lock.ludic`)) } if s_contains(lock, "sha256:") { ok("lockfile records content hashes") } else { bad("lockfile has no content hash") } # the project view links into the content-addressed store if file_exists(`{proj}/ludic_modules/example.test/greeter/greet.ludic`) and file_exists(`{proj}/ludic_modules/example.test/util/util.ludic`) { ok("ludic_modules/ links the store entries into the project") } else { bad("ludic_modules view was not linked") } # ---- compile + run the consumer against the fetched packages -------------- if shq(`( cd {proj} && LUDIC_MODULES=ludic_modules {root}/bin/ludicc app.ludic -o app > {work}/build.out 2>&1 )`) { ok("consumer compiles against the linked packages (do_import module-root fallback)") let got = capture_line(`( cd {proj} && ./app )`) if got == "hi u12" { ok("the running program uses the MVS-selected package code (\"hi u12\")") } else { bad2("program output mismatch", `got [{got}] want [hi u12]`) } } else { bad2("consumer build failed", capture_line(`tail -2 {work}/build.out`)) } # ---- ludic verify: content integrity against the store ------------------------ if shq(`( cd {proj} && {envp} {root}/bin/ludic verify > {work}/verify.out 2>&1 )`) { ok("ludic verify passes on an untouched store") } else { bad2("ludic verify failed unexpectedly", capture_line(`tail -2 {work}/verify.out`)) } # tamper with a stored file — verify must now flag it let uhash = capture_line(`grep 'example.test/util' {proj}/package.lock.ludic | sed 's/.*sha256://; s/\".*//'`) shell(`echo tampered >> {store}/{uhash}/util.ludic`) if not shq(`( cd {proj} && {envp} {root}/bin/ludic verify > {work}/verify2.out 2>&1 )`) { ok("ludic verify detects a tampered store entry") } else { bad("ludic verify missed a tampered store entry") } # heal the store: a content-addressed entry is keyed by its hash-named dir, so # `ludic get` trusts an existing one — drop it first, then re-fetch a clean copy. shell(`rm -rf {store}/{uhash}`) shell(`( cd {proj} && {envp} {root}/bin/ludic get > /dev/null 2>&1 )`) if shq(`( cd {proj} && {envp} {root}/bin/ludic verify > {work}/verify3.out 2>&1 )`) { ok("re-fetching heals a tampered store entry (ludic verify green again)") } else { bad2("store did not heal after re-fetch", capture_line(`tail -1 {work}/verify3.out`)) } # ---- a package that exports ECS: a component, a model and a system -------- # Ludic is AOT, so a source package's declarations compile straight into the # consumer's compile-time ECS — no ABI seam. This proves a consumer *game* can # use a package's property (component), model, @OnSpawn handler (system) and a # plain function together. The engine runtime (runtime/native/*.ludic) resolves # relative to the compile CWD, so — exactly like `ludic build` — the game is compiled # from the repo root with the package linked in via an absolute LUDIC_MODULES. let epkg = `{proxy}/example.test/enginepkg` shell(`mkdir -p {epkg} && git -C {epkg} init -q`) pt_write(`{epkg}/package.ludic`, `package "example.test/enginepkg"` + nl() + `version "1.0.0"` + nl() + "kind source" + nl() + `provides "EnginePkg"` + nl()) pt_write(`{epkg}/lib.ludic`, "property Health { hp: int = 0, max: int = 0 }\nmodel Unit { Health }\n@Public @OnSpawn(Unit) handler Init { Health.hp = Health.max }\nfunction bonus() -> int { return 7 }\n") pt_commit_tag(epkg, "v1.0.0") let proj2 = `{work}/proj2` pt_write(`{proj2}/package.ludic`, `package "game"` + nl() + `version "0.0.0"` + nl() + `require "example.test/enginepkg" "1.0.0"` + nl()) pt_write(`{proj2}/game.ludic`, "program G {\n import \"example.test/enginepkg/lib.ludic\"\n entry {\n spawn Unit { Health { max: 50 } }\n let H = world_prop_id(\"Health\")\n let hp = world_field_id(H, \"hp\")\n let e = world_query_next(H, 0)\n print(world_get(e, H, hp))\n print(bonus())\n }\n}\n") if shq(`( cd {proj2} && {envp} {root}/bin/ludic get > {work}/get2.out 2>&1 )`) { if shq(`( cd {root} && LUDIC_MODULES={proj2}/ludic_modules {root}/bin/ludicc --headless {proj2}/game.ludic -o {proj2}/game > {work}/gbuild.out 2>&1 )`) { let got = capture_line(`{proj2}/game`) if got == "50 7" { ok("a consumer game uses the package's component + model + system + function (\"50 7\")") } else { bad2("imported ECS produced wrong output", `got [{got}] want [50 7]`) } } else { bad2("game consuming an ECS package failed to build", capture_line(`tail -2 {work}/gbuild.out`)) } } else { bad2("ludic get for the ECS package failed", capture_line(`tail -1 {work}/get2.out`)) } # ---- namespace collision policy (v1: hard error) -------------------------- let dupe = `{proxy}/example.test/dupe` shell(`mkdir -p {dupe} && git -C {dupe} init -q`) pt_write(`{dupe}/package.ludic`, `package "example.test/dupe"` + nl() + `version "1.0.0"` + nl() + "kind source" + nl() + `provides "Util"` + nl()) pt_write(`{dupe}/dupe.ludic`, "function dupe_tag() -> pointer { return \"d\" }\n") pt_commit_tag(dupe, "v1.0.0") if not shq(`( cd {proj} && {envp} {root}/bin/ludic add example.test/dupe@1.0.0 > {work}/dupe.out 2>&1 )`) and shq(`grep -q collision {work}/dupe.out`) { ok("two packages claiming the same Foo.* namespace is a hard error") } else { bad2("collision not rejected", capture_line(`tail -1 {work}/dupe.out`)) } # drop the bad dep again so the manifest is clean pt_write(`{proj}/package.ludic`, `package "app"` + nl() + `version "0.0.0"` + nl() + `require "example.test/greeter" "1.0.0"` + nl() + `require "example.test/util" "1.2.0"` + nl()) # ---- prebuilt target matrix ---------------------------------------------- let pb = `{proxy}/example.test/prebuilt` shell(`mkdir -p {pb} && git -C {pb} init -q`) pt_write(`{pb}/package.ludic`, `package "example.test/prebuilt"` + nl() + `version "1.0.0"` + nl() + "kind prebuilt" + nl() + `provides "Blob"` + nl() + `targets "made-up-target"` + nl()) pt_write(`{pb}/README`, "a prebuilt lib shipping no artifact for this host\n") pt_commit_tag(pb, "v1.0.0") if not shq(`( cd {proj} && {envp} {root}/bin/ludic add example.test/prebuilt@1.0.0 > {work}/pb.out 2>&1 )`) and shq(`grep -q 'prebuilt lib' {work}/pb.out`) { ok("a prebuilt lib with no artifact for the build target is a hard error") } else { bad2("prebuilt target matrix not enforced", capture_line(`tail -1 {work}/pb.out`)) } pt_write(`{proj}/package.ludic`, `package "app"` + nl() + `version "0.0.0"` + nl() + `require "example.test/greeter" "1.0.0"` + nl() + `require "example.test/util" "1.2.0"` + nl()) # ---- ludic vendor: hermetic copy --------------------------------------------- shell(`( cd {proj} && {envp} {root}/bin/ludic get > /dev/null 2>&1 )`) if shq(`( cd {proj} && {envp} {root}/bin/ludic vendor > {work}/vendor.out 2>&1 )`) and file_exists(`{proj}/vendor/example.test/util/util.ludic`) { ok("ludic vendor copies the resolved packages into ./vendor") } else { bad2("ludic vendor failed", capture_line(`tail -1 {work}/vendor.out`)) } # ---- package-declarable engine-system + namespace (issue #62) ------------- # A source package registers a compile-time engine system (@EngineSystem) and a # Foo.* namespace (@Namespace) with no compiler edit; a consumer game imports it # and both take effect. Source path → runs everywhere (not dylib/macOS-gated). let cp = `{proxy}/example.test/coachpack` shell(`mkdir -p {cp} && git -C {cp} init -q`) pt_write(`{cp}/package.ludic`, `package "example.test/coachpack"` + nl() + `version "1.0.0"` + nl() + "kind source" + nl() + `provides "Coach"` + nl()) pt_write(`{cp}/coach.ludic`, "property Score { value: int = 0 }\n@EngineSystem(Score, Update) function esys_score() -> void {\n let P = world_prop_id(\"Score\")\n let e = world_query_next(P, 0)\n if e >= 0 { world_set(e, P, 0, world_get(e, P, 0) + 1) }\n}\n@Namespace(Coach) function coach_bonus() -> int { return 99 }\n") pt_commit_tag(cp, "v1.0.0") let proj4 = `{work}/proj4` pt_write(`{proj4}/package.ludic`, `package "game"` + nl() + `version "0.0.0"` + nl() + `require "example.test/coachpack" "1.0.0"` + nl()) pt_write(`{proj4}/game.ludic`, "program Game {\n import \"example.test/coachpack/coach.ludic\"\n model Player { Score }\n @OnSpawn(Player) handler Init { }\n var frames: int = 0\n handler Boot phase Start { let e = world_spawn(world_model_id(\"Player\")) }\n handler Run phase Update {\n frames = frames + 1\n if frames >= 5 {\n let P = world_prop_id(\"Score\")\n let e = world_query_next(P, 0)\n print(world_get(e, P, 0))\n print(Coach.bonus())\n quit()\n }\n }\n}\n") if shq(`( cd {proj4} && {envp} {root}/bin/ludic get > {work}/get4.out 2>&1 )`) { shell(`LUDIC_HOME={root} LUDIC_MODULES={proj4}/ludic_modules {root}/bin/ludicc --headless {proj4}/game.ludic -o {proj4}/game 2>{work}/gc4.err`) if file_exists(`{proj4}/game`) { let got = capture_line(`printf 'aaaaa' | {proj4}/game`) if got == "4 99" { ok("a package registers an engine-system (@EngineSystem) + a namespace (@Namespace) with no compiler edit (\"4 99\")") } else { bad2("package hooks produced wrong output", `got [{got}] want [4 99]`) } } else { bad2("consumer of a @EngineSystem/@Namespace package failed to build", capture_line(`tail -1 {work}/gc4.err`)) } } else { bad2("ludic get for the coach package failed", capture_line(`tail -1 {work}/get4.out`)) } # ---- prebuilt binary package (issue #64): exported function + system + component # A module is compiled to a dylib (ludic 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` shell(`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/ludic build-lib manapack.ludic > {work}/bl.out 2>&1 )`) and file_exists(`{mp}/lib/{tgt}/libmanapack.dylib`) { ok("ludic build-lib compiles a package module to a per-target dylib") } else { bad2("ludic 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/ludic get > {work}/get3.out 2>&1 )`) and file_exists(`{proj3}/ludic_modules/example.test/manapack/lib/{tgt}/libmanapack.dylib`) { ok("ludic get fetches + links a prebuilt (kind prebuilt) package for the target") } else { bad2("prebuilt ludic 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 `ludic link-flags` (a real external build's link step). shell(`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/ludic 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() }