RFC: package manager for Ludic (fetch + resolve + namespace registration) #63
Labels
No labels
area:ci
area:docs
area:input
area:net
area:rendering
area:repo
area:stdlib
area:tooling
area:types
cleanup
dx
priority:high
priority:low
priority:medium
proposal
status:in-progress
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: workshopsoft/ludic#63
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Ludic has no package manager yet. As the stdlib grows and external packages (e.g. the gameplay controllers roadmap) come online, we need a way to declare, fetch, resolve, and compile third-party Ludic packages. This issue started as an open RFC; the v1 direction is now decided (see below). The axis discussion is kept as rationale.
Two properties frame Ludic's situation:
git.workshopsoft.io) and drive everything through one Ludic task runner (bin/x). The design slots into that — no new infra to run.Foo.*namespaces. The prebuilt-lib kind reintroduces an ABI/target concern that a source-only design would not have — called out explicitly in Open Questions.Decisions (v1 direction)
git.workshopsoft.io/user/pkg).git tagis publishing — no account, no publish step, no registry to run.package.ludic(declared deps + provided namespaces) andpackage.lock.ludic(resolved versions + content hashes).Foo.*namespaces.Rationale — the decision axes
The design splits into four independent axes; the Go model is a bundle of specific choices we adopted for two of them.
Axis 1 — Where source comes from → URL-as-identity (Go)
Import path is the location. Zero infra given our Forgejo hosting. Accepted costs: no global namespace/search, name tied to host, URL typosquatting, dead host = dead package without a proxy. Rejected: central registry (must build+run infra, heavy at v0.2), hybrid index.
Axis 2 — Version resolution → MVS (Go)
Lowest-satisfying version, deterministic, no SAT solver — and critically, the resolver has to be written in Ludic, so a few-hundred-line MVS beats a full backtracking solver. Rejected: SemVer+backtracking (Cargo/npm machinery), pinned-commit-only (no diamond dedup).
Axis 3 — Where deps live → pnpm-style store + links
Global content-addressed store, projects link into it. Avoids the per-project duplication of a vendored/
node_modulesmodel while keeping the shared-immutable benefit of a global cache. Vendoring can still be offered as an opt-in for hermetic/offline builds.Axis 4 — Manifest + lockfile →
package.ludic+package.lock.ludicManifest lists deps and the namespace(s) the package provides; lockfile pins resolved versions + content hashes for reproducibility and integrity.
Package kinds & namespace registration (
Foo.*)A package must be able to register a stdlib-style
Foo.*namespace (Regex., Grid., Math.* … are compiler-internal today; third parties should ship the same shape). Two kinds are in scope:dylib; wasm has its own target) shipping a namespace over a stable surface.package.ludictherefore declares, per package, the namespace(s) it provides, its kind, and (for prebuilt) which targets it ships.Open sub-questions:
Foo.*.Open questions
package.ludic(declared deps, provided namespaces, kind, targets) and the exact lock schema forpackage.lock.ludic.bin/xverify step; proxy/mirror for host-death resilience — day one or later?bin/x:x add/x get/x vendor/x update/x verify.Non-goals (v1)
Implemented and shipped to
mainin2c44bae.The v1 direction from this RFC is now a working package manager — a set of
xsubcommands plus one small, contained compiler change. All four axes landed as decided:URL-as-identity, no registry. A dependency is named by its git import path; a
git tag vX.Y.Zpublishes a version. Fetching drives plaingit(a local$LUDIC_PKG_PROXYtree serves as a mirror/offline source and is what the test suite uses).Minimum Version Selection. A
requireis a minimum; the resolver picks the greatest required minimum per module, then keeps the reachable closure at those versions. Deterministic, no SAT solver — a few hundred lines of Ludic intools/x/pkg.ludic.Content-addressed global store + per-project links (pnpm-style). Packages live once under
~/.ludic/store/<sha256>(keyed by a metadata-independent content hash); each project gets a symlinked view underludic_modules/.$LUDIC_STOREoverrides the location.package.ludic+package.lock.ludic. The manifest declares deps, provided namespaces, kind (source/prebuilt) and prebuilt targets; the lock pins resolved versions + content hashes.x verifyrehashes each store entry and confirms the project links to it.Namespace registration.
do_importnow resolves a non-local, non-absolute import under$LUDIC_MODULES(defaultludic_modules/), so a fetched source package's Ludic is spliced into the consumer's AOT build exactly the way the built-in stdlib namespaces are — no ABI seam, whole-program determinism preserved. Two packages claiming the sameFoo.*is a hard error. Prebuilt libs declare shippedtargets; a missing target for the build target is a hard error (falls back to source when the package also ships it) — the documented escape hatch over the engine C-ABI.Command surface (all on
bin/x):add,get,update,verify,vendor.Open questions from the issue — resolved for v1:
$LUDIC_TARGETelsenative-<arch>; a package keys artifacts by target and errors when the needed one is absent (source fallback if present). Full cross-target artifact builds remain future work; the resolution/verification contract is in place.docs/PACKAGES.md.x vendorgives the hermetic/offline copy (LUDIC_MODULES=vendor).x verify. A proxy/mirror rides on$LUDIC_PKG_PROXY(host-death resilience without standing up a CDN).Verification. New hermetic suite
x test-pkg(throwaway git repos, fully offline) covers fetch → MVS → store → link → compile → run → verify → collision → prebuilt-target → vendor — 12/12 green — and is gated insidex test. The full regression suite is 87/87; the existing golden renders are byte-identical and the C-free bootstrap fixpoint holds (the import fallback only fires when the local path is absent, so existing programs compile unchanged; the seed was regenerated).Docs:
docs/PACKAGES.md(design + manifest/lock/commands), README quick-start, and a changeset for the next release.This also satisfies the packaging prerequisite that the controllers roadmap (#57–#62) depends on: external, versioned source packages that compile into the consumer. Registry-izing the engine-system hooks (the other half of #62) is separate follow-up.