feat(cli): install in one command, and call the CLI ludic

Getting started meant cloning the repository, bootstrapping a compiler and
learning a task runner called `x`. That is a contributor's workflow handed to
everyone who wants to try the language.

Installing is now one command:

    curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh

install.sh puts a complete toolchain — compiler, CLI, engine runtime, bundled
ludic.* packages, formatter, language server — in ~/.ludic and adds it to PATH.
Prebuilt artifacts are checksum-verified; where a platform has none, or the
release predates this layout, it bootstraps from the compiler's own IR seed with
clang. The docs site publishes the script beside the pages that quote it, so the
page and the script can never come from different releases.

`x` becomes `ludic`, and the surface splits by audience. A user of the language
sees `new`, `run`, `build`, `test`, `add`, `fmt`, `lsp`, `doctor`, `upgrade`;
`ludic new` scaffolds a project that builds and plays as it stands. Everything
the toolchain repo needs moved under `ludic dev` — build, test, reseed,
bootstrap-cfree, docs-gen, release — unchanged apart from the namespace. Those
tasks read arguments one position further along, so dispatch_dev sets a shift
and commands use arg_n()/arg_total() rather than each knowing its own depth.

Release artifacts become complete install roots (bin/ beside runtime/, packages/
and VERSION) rather than bare binaries, which is what the installer unpacks.
`ludic dev test` asserts the whole shape: it stages an install, puts it on PATH
with no LUDIC_HOME, and runs new -> build -> test through it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-05 22:01:52 +03:00
parent 005cc39394
commit aca263642d
54 changed files with 1802 additions and 670 deletions

View file

@ -64,7 +64,7 @@ controller.
The genre packages build on the engine's `Body`/`Collider` + `esys_move` (swept-AABB
tile collision, in `runtime/native/systems_move.ludic`) and on `ludic.gameplay`. In a
real project you `x add` them; in this repo the packages live under `packages/` and a
real project you `ludic add` them; in this repo the packages live under `packages/` and a
game compiles against them with `LUDIC_MODULES`:
```
@ -74,7 +74,7 @@ LUDIC_MODULES=packages ludicc examples/games/platformer_demo.ludic -o platformer
Worked, self-checking examples for every controller live in `examples/games/`
(`platformer_demo`, `platformer_scaffolding`, `shooter_demo`, `npcai_demo`, `rpg_demo`)
and `examples/library/gameplay_foundation.ludic`; each is a deterministic regression
case in `x test`.
case in `ludic dev test`.
## What we deliberately do NOT do

View file

@ -1,6 +1,6 @@
# Ludic packages
Ludic has a package manager built into the task runner (`bin/x`). It fetches,
Ludic has a package manager built into the task runner (`bin/ludic`). It fetches,
resolves, stores and links third-party packages with no new infrastructure to
run — it drives plain `git` and rides on the Forgejo host and its release tags.
@ -18,14 +18,14 @@ This is the v1 implementation of the direction decided in issue #63.
## Commands
```
x add <module>[@version] add a dependency to package.ludic, then resolve + fetch + link
x get resolve every dependency in package.ludic, link them, write the lock
x update [module] bump a dependency (or all) to its latest published version, then relock
x verify check every locked package against the store by content hash
x vendor copy the resolved packages into ./vendor for hermetic/offline builds
ludic add <module>[@version] add a dependency to package.ludic, then resolve + fetch + link
ludic get resolve every dependency in package.ludic, link them, write the lock
ludic update [module] bump a dependency (or all) to its latest published version, then relock
ludic verify check every locked package against the store by content hash
ludic vendor copy the resolved packages into ./vendor for hermetic/offline builds
```
`x add` with no `@version` picks the latest published tag and records it as the
`ludic add` with no `@version` picks the latest published tag and records it as the
minimum. All the install commands print the resolved build list and write
`package.lock.ludic`.
@ -52,17 +52,17 @@ optional for a leaf application).
## The lockfile — `package.lock.ludic`
Generated by `x get`; do not edit by hand. One line per resolved module, pinning
Generated by `ludic get`; do not edit by hand. One line per resolved module, pinning
its selected version, content hash, kind and provided namespaces:
```
# package.lock.ludic — generated by `x get`. Do not edit by hand.
# package.lock.ludic — generated by `ludic get`. Do not edit by hand.
lock 1
module "git.workshopsoft.io/orkun/greeter" version "1.2.0" hash "sha256:…" kind "source" provides "Greet"
module "git.workshopsoft.io/orkun/util" version "1.0.0" hash "sha256:…" kind "source" provides "Util"
```
`x verify` rehashes each store entry and confirms the project links to it, so a
`ludic verify` rehashes each store entry and confirms the project links to it, so a
tampered or missing dependency is caught before it reaches a build.
## The store and the project view
@ -176,31 +176,31 @@ always emitted (unused parts dead-strip).
**Publishing.** In the package repo:
```
x build-lib module.ludic # -> lib/<target>/lib<name>.dylib
ludic build-lib module.ludic # -> lib/<target>/lib<name>.dylib
# add `kind prebuilt` and `targets "<target>"` to package.ludic, commit lib/, git tag
```
**Consuming.** In the game project:
```
x add git.host/user/module # kind prebuilt is resolved + the dylib linked into the view
x get # links the artifact for the build target (hard error if the target is missing)
ludic add git.host/user/module # kind prebuilt is resolved + the dylib linked into the view
ludic get # links the artifact for the build target (hard error if the target is missing)
```
then link the module dylibs into the game build. `x link-flags` prints the exact
then link the module dylibs into the game build. `ludic link-flags` prints the exact
clang flags (the dylib, an rpath to the store, `-export_dynamic`) for any build
system to splice into its link step:
```
clang -O2 game.ll $(x link-flags) -o game
clang -O2 game.ll $(ludic link-flags) -o game
```
(`x app` links them automatically when building in-repo.) If the package does
not ship the build target, `x get` fails — build from source instead where the
(`ludic build` links them automatically when building in-repo.) If the package does
not ship the build target, `ludic get` fails — build from source instead where the
package offers it.
## Offline / hermetic builds
`x vendor` copies the resolved packages out of the store into `./vendor`. Build
`ludic vendor` copies the resolved packages out of the store into `./vendor`. Build
against the copy with `LUDIC_MODULES=vendor`, so the build needs neither the
network nor the global store.

View file

@ -4,4 +4,4 @@ title: Testing
order: 34
---
A built-in testing framework in the spirit of Go's <code>go test</code>: tests live next to the code, run with one command, and report pass/fail — no harness to wire up. A <a href="kw-test"><code>test "name" { … }</code></a> block is discovered automatically and run by a synthetic entry point that prints <code>ok - name</code> or <code>FAIL - name</code> for each, a <code>== N passed, M failed ==</code> summary, and exits non-zero if anything failed (so CI and the <code>bin/x</code> runner catch it). Inside a test, the <code>expect</code>, <code>expect_eq</code> and <code>expect_near</code> assertions check a condition and, on failure, print <code>file:line: … failed (got …, want …)</code> and mark the test failed — without aborting, so one run reports every failure. <code>expect_near</code> takes a tolerance, which is what fixed-point and accumulated-integer game math need. Everything is deterministic and compiles to a native binary, so a suite runs in the same C-free toolchain as the rest of Ludic. Coverage instrumentation is a planned follow-up. Related: <a href="kw-function"><code>function</code></a>, <a href="fn-print"><code>print</code></a>.
A built-in testing framework in the spirit of Go's <code>go test</code>: tests live next to the code, run with one command, and report pass/fail — no harness to wire up. A <a href="kw-test"><code>test "name" { … }</code></a> block is discovered automatically and run by a synthetic entry point that prints <code>ok - name</code> or <code>FAIL - name</code> for each, a <code>== N passed, M failed ==</code> summary, and exits non-zero if anything failed (so CI and the <code>bin/ludic</code> runner catch it). Inside a test, the <code>expect</code>, <code>expect_eq</code> and <code>expect_near</code> assertions check a condition and, on failure, print <code>file:line: … failed (got …, want …)</code> and mark the test failed — without aborting, so one run reports every failure. <code>expect_near</code> takes a tolerance, which is what fixed-point and accumulated-integer game math need. Everything is deterministic and compiles to a native binary, so a suite runs in the same C-free toolchain as the rest of Ludic. Coverage instrumentation is a planned follow-up. Related: <a href="kw-function"><code>function</code></a>, <a href="fn-print"><code>print</code></a>.

View file

@ -17,12 +17,12 @@ Assertions (each records a failure and prints <code>file:line: … failed</code>
- `expect_eq(a, b)` — `a` must equal `b`; on failure prints `(got a, want b)`.
- `expect_near(a, b, tol)` — `a` must be within `tol` of `b` (absolute). Use it for `fixed`-point results and accumulated integer math, where an exact match is too brittle.
Run a spec file directly with the compiler-runner — `ludic mymath_test.ludic` compiles it to a native binary, runs it, and forwards the pass/fail exit code — so it drops straight into `bin/x` and CI.
Run a spec file directly with the compiler-runner — `ludic mymath_test.ludic` compiles it to a native binary, runs it, and forwards the pass/fail exit code — so it drops straight into `bin/ludic` and CI.
<strong>Line coverage.</strong> Compile with the <code>--coverage</code> flag and the compiler instruments every statement with a per-source-line hit counter; at exit the counts are written to the file named by <code>$LUDIC_COVERAGE</code> (default <code>ludic.cov</code>) as a <code>FILE &lt;name&gt;</code> header followed by one <code>&lt;line&gt; &lt;hits&gt;</code> row per instrumented line. The instrumentation is flag-gated and additive, so an ordinary build — and the compiler's own self-compile — stays byte-identical. <code>bin/x test --coverage</code> compiles the test specs this way, runs them, and aggregates the dumps into a per-file report that names the lines your tests never reached:
<strong>Line coverage.</strong> Compile with the <code>--coverage</code> flag and the compiler instruments every statement with a per-source-line hit counter; at exit the counts are written to the file named by <code>$LUDIC_COVERAGE</code> (default <code>ludic.cov</code>) as a <code>FILE &lt;name&gt;</code> header followed by one <code>&lt;line&gt; &lt;hits&gt;</code> row per instrumented line. The instrumentation is flag-gated and additive, so an ordinary build — and the compiler's own self-compile — stays byte-identical. <code>bin/ludic dev test --coverage</code> compiles the test specs this way, runs them, and aggregates the dumps into a per-file report that names the lines your tests never reached:
```
== line coverage (bin/x test --coverage) ==
== line coverage (bin/ludic dev test --coverage) ==
examples/library/coverage.ludic 16/17 lines 94% uncovered: 33
examples/library/testing.ludic 17/17 lines 100%
----

View file

@ -152,63 +152,69 @@
},
"start": {
"kicker": "Get started",
"title": "From clone to a native window.",
"intro": "Bootstrap the task runner once from the checked-in IR seed, then build the toolchain and your game. Everything runs from the repo root.",
"title": "One command to install. One to play.",
"intro": "Install the toolchain with a single command — it brings the compiler, the <code>ludic</code> CLI, the engine runtime and the editor tooling, and needs nothing else on your machine but a C toolchain to link with. Then create a project and run it.",
"steps": [
{
"title": "Bootstrap from the seed",
"html": "<code>bin/</code> is not checked in, so it is created first; then <code>clang</code> assembles the compiler's own checked-in LLVM IR seed, and that compiler builds <code>bin/x</code>, the task runner. This is the only step Ludic cannot do for itself."
"title": "Install",
"html": "The installer downloads a verified toolchain for your platform into <code>~/.ludic</code> and puts it on your <code>PATH</code>. Nothing else is touched; uninstalling is <code>rm -rf ~/.ludic</code>. On a platform with no prebuilt toolchain it bootstraps from the compiler's own IR seed instead — same result, a few seconds longer."
},
{
"title": "Build the toolchain",
"html": "<code>bin/x build</code> produces <code>ludicc</code>, <code>ludic</code>, <code>ludic-fmt</code> and <code>ludic-lsp</code> — all compiled by Ludic, from Ludic."
"title": "Create a project",
"html": "<code>ludic new mygame</code> writes a manifest, a program that already moves something on screen, a test, and an <code>assets/</code> directory. There is no scaffolding to choose and no build file to write."
},
{
"title": "Compile &amp; run an example",
"html": "<code>bin/x app examples/games/snake.ludic</code> turns a <code>.ludic</code> file into a native binary. Run it to open a real window."
"title": "Run it",
"html": "<code>ludic run</code> compiles <code>src/main.ludic</code> to a native binary and launches it. <code>ludic build</code> stops at the binary — one self-contained executable, with nothing to ship beside it."
},
{
"title": "Go headless for tests",
"html": "<code>--headless</code> renders frames to a <code>.ppm</code> from piped input — deterministic output you can diff in CI."
"title": "Test it, headlessly",
"html": "<code>ludic test</code> compiles and runs every <code>test</code> block in the project. <code>--headless</code> renders frames to a <code>.ppm</code> from piped input, so a game is deterministic enough to diff in CI."
}
],
"terminal_name": "zsh — ludic",
"terminal": [
{
"comment": "bootstrap the task runner (clang + the IR seed, once)"
"comment": "install the toolchain (macOS, Linux)"
},
{
"cmd": "mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc"
"cmd": "curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh"
},
{
"cmd": "bin/ludicc tools/x/main.ludic -o bin/x"
"out": "→ installed ludic 0.4.0 → ~/.ludic"
},
{
"blank": true
},
{
"comment": "build the toolchain, then an example (opens a window)"
"comment": "a project that builds and plays as it stands"
},
{
"cmd": "bin/x build"
"cmd": "ludic new mygame"
},
{
"cmd": "bin/x app examples/games/snake.ludic"
"cmd": "cd mygame"
},
{
"cmd": "./build/snake"
"cmd": "ludic run"
},
{
"out": "→ a native window, running your game"
},
{
"blank": true
},
{
"comment": "deterministic headless render for tests"
"comment": "tests, and a deterministic headless render for CI"
},
{
"cmd": "bin/x app examples/games/snake.ludic --headless"
"cmd": "ludic test"
},
{
"cmd": "printf 'ddddwww' | ./build/snake_headless"
"cmd": "ludic build --headless"
},
{
"cmd": "printf 'ddddwww' | ./build/mygame_headless"
},
{
"out": "→ writes build/out.ppm"
@ -228,6 +234,6 @@
"Sublime Text",
"Zed"
],
"note": "<code>bin/x tools</code> builds <span class=\"mono\">ludic-fmt</span> and <span class=\"mono\">ludic-lsp</span> — the same formatter runs as a CLI for pre-commit hooks and CI."
"note": "Editors spawn <code>ludic lsp</code> — it ships with the toolchain, so there is nothing extra to build or install. The same formatter runs as <code>ludic fmt</code> for pre-commit hooks and CI."
}
}