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:
parent
005cc39394
commit
aca263642d
54 changed files with 1802 additions and 670 deletions
|
|
@ -10,7 +10,7 @@ docs/
|
|||
language/<category>/<id>.md one file per symbol — keyword, type, phase,
|
||||
builtin, namespace method, operator, annotation
|
||||
language/<category>/_section.md section title + blurb + order
|
||||
language/colors/palette.json the 221 named colors (generated by `x docs-palette`)
|
||||
language/colors/palette.json the 221 named colors (generated by `ludic dev docs-palette`)
|
||||
site/site.json landing-page messaging (hero, features, …)
|
||||
site/snippets/*.ludic the code shown on the landing page (real programs)
|
||||
tools/docgen/inventory.json the authoritative symbol set the coverage guard checks
|
||||
|
|
@ -66,26 +66,26 @@ no Python in the pipeline. `assets/` (the CSS/HTML/JS templates) and
|
|||
`inventory.json` are the only inputs the tools here still read directly.
|
||||
|
||||
```bash
|
||||
bin/x docs-gen --out build/pages # generate the whole site
|
||||
bin/x docs-check build/pages # coverage + duplicate-token + link guard
|
||||
bin/x check-docs # parse every ```ludic doc fence
|
||||
bin/ludic dev docs-gen --out build/pages # generate the whole site
|
||||
bin/ludic dev docs-check build/pages # coverage + duplicate-token + link guard
|
||||
bin/ludic dev check-docs # parse every ```ludic doc fence
|
||||
```
|
||||
|
||||
`docs-check` fails CI if any symbol in `inventory.json` lacks a page, if a token
|
||||
is documented on two pages, or if a highlighter link points at a missing page —
|
||||
so "every symbol is documented, autogenerated each time" is enforced. It needs a
|
||||
built `bin/x` (bootstrapped from the IR seed with clang alone).
|
||||
built `bin/ludic` (bootstrapped from the IR seed with clang alone).
|
||||
|
||||
## Publish
|
||||
|
||||
`.forgejo/workflows/docs.yml` bootstraps the toolchain from the IR seed and runs
|
||||
`x docs-gen` + `x docs-check` on every push to `main` that touches `docs/**`,
|
||||
`tools/docgen/**` or `tools/x/**`, and publishes the result to the `pages` branch
|
||||
`ludic dev docs-gen` + `ludic dev docs-check` on every push to `main` that touches `docs/**`,
|
||||
`tools/docgen/**` or `tools/ludic-cli/**`, and publishes the result to the `pages` branch
|
||||
root. `index.html` + `.nojekyll` always stay at the root.
|
||||
|
||||
## Colors
|
||||
|
||||
`palette.json` and `selfhost/backend/stdlib/emit_color.ludic` are both generated
|
||||
by `bin/x docs-palette` from a single palette table — the `pal_add(...)` rows in
|
||||
`tools/x/docgen.ludic`. Edit the table there and regenerate; do not hand-edit the
|
||||
by `bin/ludic dev docs-palette` from a single palette table — the `pal_add(...)` rows in
|
||||
`tools/ludic-cli/docgen.ludic`. Edit the table there and regenerate; do not hand-edit the
|
||||
generated files.
|
||||
|
|
|
|||
|
|
@ -21,14 +21,21 @@ tools/editors/
|
|||
neovim/ helix/ emacs/ sublime/ zed/ configuration, no plugin needed
|
||||
```
|
||||
|
||||
## Build it
|
||||
## Getting the server
|
||||
|
||||
An installed toolchain already has it: `curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh`
|
||||
puts `ludic-lsp` and `ludic-fmt` in `~/.ludic/bin`, on your `PATH`, and
|
||||
`ludic lsp` runs the server on stdio — that is what an editor should spawn, since
|
||||
it needs no path configuration.
|
||||
|
||||
From a checkout, build them with:
|
||||
|
||||
```bash
|
||||
bin/x tools
|
||||
bin/ludic dev tools
|
||||
```
|
||||
|
||||
Produces `bin/ludic-fmt` and `bin/ludic-lsp`. Add `--install` to symlink both
|
||||
into `~/.local/bin`, `--test` to run `bin/x test-tools` afterwards.
|
||||
into `~/.local/bin`, `--test` to run `bin/ludic dev test-tools` afterwards.
|
||||
|
||||
## What you get, in any editor
|
||||
|
||||
|
|
@ -63,11 +70,14 @@ and otherwise keywords, builtins, and everything in scope.
|
|||
- **Anything else** that speaks LSP:
|
||||
|
||||
```
|
||||
command: bin/ludic-lsp --stdio
|
||||
command: ludic lsp
|
||||
languages: ludic, markdown
|
||||
initializationOptions: { "compilerPath": "bin/ludicc", "compilerDiagnostics": true, "indentSize": 2 }
|
||||
initializationOptions: { "compilerDiagnostics": true, "indentSize": 2 }
|
||||
```
|
||||
|
||||
(`ludic lsp` is on `PATH` after an install; from a checkout it is
|
||||
`bin/ludic lsp`, and the server finds `ludicc` next to itself.)
|
||||
|
||||
## Markdown
|
||||
|
||||
Markdown fenced-code highlighting is not one standard — it is three mechanisms,
|
||||
|
|
@ -108,7 +118,7 @@ ln -sf ../../tools/git-hooks/pre-commit .git/hooks/pre-commit
|
|||
|
||||
The hook runs `ludic-fmt --check` over the staged `.ludic` and `.md` files only,
|
||||
and does nothing at all when `bin/ludic-fmt` has not been built — so it never
|
||||
blocks a commit on a machine that has not run `bin/x tools`.
|
||||
blocks a commit on a machine that has not run `bin/ludic dev tools`.
|
||||
|
||||
## Why the formatter is not `ludicc --fmt`
|
||||
|
||||
|
|
@ -130,7 +140,7 @@ both spellings are what the language documents and uses:
|
|||
(`const R_DIR: int = 0 # 0 up`) survive a save;
|
||||
- `id=Root` inside a `ui` block and `{Enemy}` inside a query stay tight.
|
||||
|
||||
`bin/x test-tools` checks the property that matters: formatting every file in
|
||||
`bin/ludic dev test-tools` checks the property that matters: formatting every file in
|
||||
the tree and re-running the *compiler's* canonical dump produces byte-identical
|
||||
output. The formatter cannot change what a program means.
|
||||
|
||||
|
|
@ -139,8 +149,8 @@ output. The formatter cannot change what a program means.
|
|||
The vocabulary is written down in five places that cannot include each other —
|
||||
the compiler's two tables, `ludic_syntax.h`, the TextMate grammar (JSON), and the
|
||||
JetBrains lexer (Kotlin). Adding a builtin and forgetting the rest is silent
|
||||
failure, so `bin/x check-vocabulary` (written in Ludic) compares all five, and
|
||||
`bin/x test-tools` runs it.
|
||||
failure, so `bin/ludic dev check-vocabulary` (written in Ludic) compares all five, and
|
||||
`bin/ludic dev test-tools` runs it.
|
||||
|
||||
When you add a keyword or builtin: put it in `ludic_syntax.h`, then run
|
||||
`bin/x test-tools` and let it tell you which copies still need it.
|
||||
`bin/ludic dev test-tools` and let it tell you which copies still need it.
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ The first build downloads an IntelliJ IDEA Community distribution (over 1 GB),
|
|||
so expect it to take a while; later builds take seconds. Once the cache is warm,
|
||||
|
||||
```bash
|
||||
LUDIC_TEST_JETBRAINS=1 bin/x test-tools
|
||||
LUDIC_TEST_JETBRAINS=1 bin/ludic dev test-tools
|
||||
```
|
||||
|
||||
includes the plugin build in the toolchain's own test run.
|
||||
|
|
@ -55,4 +55,4 @@ LSP4IJ is a required dependency; the IDE offers to install it for you.
|
|||
|
||||
**Settings -> Languages & Frameworks -> Ludic** — the path to `ludic-lsp` and to
|
||||
`ludicc`. Both default to `bin/` under the project root, which is where
|
||||
`bin/x tools` puts them.
|
||||
`bin/ludic dev tools` puts them.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import java.io.File
|
|||
|
||||
/**
|
||||
* Where to find the toolchain. Both paths may be left blank, in which case the
|
||||
* plugin looks under the project root for what `bin/x tools` produces —
|
||||
* plugin looks under the project root for what `bin/ludic dev tools` produces —
|
||||
* which is the case for anyone working in the Ludic repository itself.
|
||||
*/
|
||||
@Service(Service.Level.PROJECT)
|
||||
|
|
@ -60,13 +60,23 @@ internal fun findExecutable(root: String?, configured: String, names: List<Strin
|
|||
|
||||
/** Fall back to PATH, so a system-wide install works with no configuration. */
|
||||
private fun searchPath(names: List<String>): String? {
|
||||
val path = System.getenv("PATH") ?: return null
|
||||
for (dir in path.split(File.pathSeparator)) {
|
||||
for (name in names) {
|
||||
val f = File(dir, name)
|
||||
if (f.isFile && f.canExecute()) return f.absolutePath
|
||||
val path = System.getenv("PATH")
|
||||
if (path != null) {
|
||||
for (dir in path.split(File.pathSeparator)) {
|
||||
for (name in names) {
|
||||
val f = File(dir, name)
|
||||
if (f.isFile && f.canExecute()) return f.absolutePath
|
||||
}
|
||||
}
|
||||
}
|
||||
// The install root, last: an IDE launched from the desktop does not inherit
|
||||
// the PATH the installer wrote to the shell profile, so a working ~/.ludic
|
||||
// install would otherwise look missing.
|
||||
val home = System.getenv("LUDIC_HOME") ?: (System.getProperty("user.home") + "/.ludic")
|
||||
for (name in names) {
|
||||
val f = File(File(home, "bin"), name)
|
||||
if (f.isFile && f.canExecute()) return f.absolutePath
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -108,9 +118,10 @@ class LudicConnectionProvider(private val project: Project) : ProcessStreamConne
|
|||
append(if (root != null) File(File(root, d), "ludic-lsp").path else "<no project root>/$d/ludic-lsp")
|
||||
append('\n')
|
||||
}
|
||||
append(" and on PATH.\n")
|
||||
append(" on PATH, and in ~/.ludic/bin.\n")
|
||||
}
|
||||
append("\nBuild it with `bin/x tools` from the repository root, ")
|
||||
append("\nInstall the toolchain (curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh), ")
|
||||
append("build it with `bin/ludic dev tools` from a checkout, ")
|
||||
append("or set the path in Settings | Languages & Frameworks | Ludic. ")
|
||||
append("Syntax highlighting works without it; completion and diagnostics do not.")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@
|
|||
name="Ludic"
|
||||
factoryClass="io.ludic.ide.LudicLanguageServerFactory">
|
||||
<description><![CDATA[
|
||||
The Ludic language server. Built by <code>bin/x tools</code>.
|
||||
The Ludic language server. Built by <code>bin/ludic dev tools</code>.
|
||||
]]></description>
|
||||
</server>
|
||||
<languageMapping language="Ludic" serverId="ludic"/>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ for the language reference.
|
|||
Build the toolchain once, from the repository root:
|
||||
|
||||
```bash
|
||||
bin/x tools
|
||||
bin/ludic dev tools
|
||||
```
|
||||
|
||||
That produces `bin/ludic-lsp` and `bin/ludic-fmt`, which the extension finds
|
||||
|
|
|
|||
|
|
@ -38,9 +38,21 @@ function resolveServer(folders) {
|
|||
if (isExecutable(p)) return p;
|
||||
}
|
||||
}
|
||||
// The install root, checked last: an editor launched from Finder or a desktop
|
||||
// launcher does not inherit the PATH the installer wrote to the shell profile,
|
||||
// so a perfectly good ~/.ludic install would otherwise look missing.
|
||||
for (const name of names) {
|
||||
const p = path.join(installRoot(), 'bin', name);
|
||||
if (isExecutable(p)) return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The toolchain install root: $LUDIC_HOME, else ~/.ludic (what install.sh uses). */
|
||||
function installRoot() {
|
||||
return process.env.LUDIC_HOME || path.join(os.homedir(), '.ludic');
|
||||
}
|
||||
|
||||
function resolveCompiler(folders) {
|
||||
const configured = vscode.workspace.getConfiguration('ludic').get('compilerPath');
|
||||
if (configured) return expand(configured, folders);
|
||||
|
|
@ -54,6 +66,8 @@ function resolveCompiler(folders) {
|
|||
const p = path.join(dir, 'ludicc');
|
||||
if (isExecutable(p)) return p;
|
||||
}
|
||||
const installed = path.join(installRoot(), 'bin', 'ludicc');
|
||||
if (isExecutable(installed)) return installed;
|
||||
return '';
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +98,7 @@ async function start(context) {
|
|||
// Not an error worth a modal: the grammar still highlights, and plenty of
|
||||
// people open a .ludic file without having built the toolchain.
|
||||
output.appendLine(
|
||||
'ludic-lsp not found. Build it with `bin/x tools`, or set "ludic.server.path". ' +
|
||||
'ludic-lsp not found. Install the toolchain (curl -fsSL https://ludic.dev/install.sh | sh), build it from a checkout with `bin/ludic dev tools`, or set "ludic.server.path". ' +
|
||||
'Syntax highlighting works without it; completion and diagnostics do not.'
|
||||
);
|
||||
return;
|
||||
|
|
|
|||
100
tools/ludic-cli/build.ludic
Normal file
100
tools/ludic-cli/build.ludic
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# build.ludic — compiling: a user's program (ludic build / ludic run) and the
|
||||
# toolchain's own binaries (ludic dev build).
|
||||
#
|
||||
# compile_app is the one place a .ludic file becomes an executable, so a project
|
||||
# build and the repo's own example builds cannot drift apart. Everything the
|
||||
# toolchain owns — the compiler, the engine runtime it splices — is addressed
|
||||
# through ludic_home(), so the same code path works from a checkout and from an
|
||||
# installed ~/.ludic.
|
||||
|
||||
# compile a Ludic source to a native binary in bin/ via ludicc + clang (-O2).
|
||||
# Returns true on success. Used for x itself and the editor tools.
|
||||
function build_tool(name: pointer, src: pointer) -> bool {
|
||||
let ll = `build/{name}.ll`
|
||||
if not shq(`bin/ludicc {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false }
|
||||
# write to a temp then move, so a running bin/ludic can rebuild itself in place
|
||||
if not shq(`{cc()} -O2 {ll} -o bin/{name}.tmp`) { print(`build failed: {name} (link)`); return false }
|
||||
run(`mv -f bin/{name}.tmp bin/{name} && rm -f {ll}`)
|
||||
return true
|
||||
}
|
||||
|
||||
# ---- build-cli: the self-hosted front-end binaries ---------------------------
|
||||
# Both ludicc and ludic are the SAME multi-call binary assembled from the IR seed
|
||||
# with clang alone; invoked as `ludic` it compiles-and-runs, as `ludicc` it just
|
||||
# compiles.
|
||||
function cmd_dev_build_cli() -> int {
|
||||
run("mkdir -p bin build")
|
||||
print("cc: selfhost/ludicc.seed.ll -> bin/ludicc (from the IR seed, no C compiler)")
|
||||
if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) { err("ludic: the seed did not assemble\n"); return 1 }
|
||||
run("chmod +x bin/ludicc")
|
||||
print("done. built bin/ludicc")
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---- build: the whole toolchain (ludicc, ludic, ludic-fmt, ludic-lsp) -------
|
||||
function cmd_dev_build() -> int {
|
||||
if cmd_dev_build_cli() != 0 { return 1 }
|
||||
print("ludicc: tools/ludic-cli/main.ludic -> bin/ludic (the CLI rebuilds itself)")
|
||||
if not build_tool("ludic", "tools/ludic-cli/main.ludic") { return 1 }
|
||||
print("ludicc: tools/ludic-tools/fmt.ludic -> bin/ludic-fmt")
|
||||
if not build_tool("ludic-fmt", "tools/ludic-tools/fmt.ludic") { return 1 }
|
||||
print("ludicc: tools/ludic-tools/lsp.ludic -> bin/ludic-lsp")
|
||||
if not build_tool("ludic-lsp", "tools/ludic-tools/lsp.ludic") { return 1 }
|
||||
sync_vscode_grammar()
|
||||
print("done. toolchain in bin/ (ludicc, ludic, ludic-fmt, ludic-lsp)")
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---- compiling one program to a native binary -------------------------------
|
||||
#
|
||||
# mode 1 = windowed (a game opens a real window), 2 = headless (the last frame is
|
||||
# rendered to build/out.ppm from piped input — deterministic output for tests).
|
||||
# Returns true on success. `ludic build`, `ludic run` and `ludic dev app` all go
|
||||
# through here.
|
||||
function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool {
|
||||
ensure_ludicc() # in a checkout: assemble the seed if bin/ludicc is missing or stale
|
||||
let home = ludic_home()
|
||||
let pbf = prebuilt_link_flags() # #64: link resolved prebuilt module dylibs, if any
|
||||
let odir = dir_of_path(out)
|
||||
if len(odir) > 0 { run(`mkdir -p {odir}`) }
|
||||
run("mkdir -p build")
|
||||
let ll = `{out}.ll`
|
||||
|
||||
if mode == 2 {
|
||||
if not shq(`{ludicc()} --headless {src} --emit-llvm -o {ll}`) { return false }
|
||||
if not shq(`{cc()} -O2 {ll}{pbf} -o {out}`) { return false }
|
||||
if not save { run(`rm -f {ll}`) }
|
||||
return true
|
||||
}
|
||||
|
||||
if not shq(`{ludicc()} --windowed {src} --emit-llvm -o {ll}`) { return false }
|
||||
# audio.ll (#22) is always linked here — unused snd_* are dead-stripped; the
|
||||
# canonical `ludicc -o` path links it only when Audio.* is used.
|
||||
let cocoa = `{home}runtime/native/cocoa.ll`
|
||||
let audio = `{home}runtime/native/audio.ll`
|
||||
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{pbf} -o {out}`) { return false }
|
||||
if not save { run(`rm -f {ll}`) }
|
||||
return true
|
||||
}
|
||||
|
||||
# the directory part of a path, without the trailing '/' ("" when there is none)
|
||||
function dir_of_path(p: pointer) -> pointer {
|
||||
var last = -1
|
||||
var i = 0
|
||||
while p[i] != 0 { if p[i] == '/' { last = i }; i += 1 }
|
||||
if last < 0 { return "" }
|
||||
return p[0..last]
|
||||
}
|
||||
|
||||
# Remove every generated artifact and leave the tracked source untouched: the
|
||||
# whole build/ tree (IR, objects, compiled apps, the headless render at
|
||||
# build/out.ppm), and any stray *.tmp/*.ll left behind by a failed build. A
|
||||
# legacy out.ppm in the root (from an older toolchain) is swept too. Binaries in
|
||||
# bin/ are kept so the running CLI survives; in the toolchain repo, rebuild them
|
||||
# any time with `ludic dev build`.
|
||||
function cmd_clean() -> int {
|
||||
run("rm -rf build")
|
||||
run("rm -f out.ppm bin/*.tmp")
|
||||
print("cleaned: build/ (incl. build/out.ppm) and stray artifacts (bin/ is left alone)")
|
||||
return 0
|
||||
}
|
||||
|
|
@ -177,7 +177,7 @@ function count_nl(s: pointer, upto: int) -> int {
|
|||
return n
|
||||
}
|
||||
|
||||
# usage: x check-docs (scans the user-facing docs + docs/language/**)
|
||||
# usage: ludic dev check-docs (scans the user-facing docs + docs/language/**)
|
||||
function cmd_check_docs() -> int {
|
||||
ensure_ludicc()
|
||||
DC_OK = 0; DC_BAD = 0; DC_SKIP = 0; DC_FAILS = ""
|
||||
|
|
@ -361,7 +361,7 @@ var CI_PROB: pointer = ""
|
|||
var CI_NPROB: int = 0
|
||||
function ci_problem(msg: pointer) -> void { CI_PROB = CI_PROB + " - " + msg + "\n"; CI_NPROB += 1 }
|
||||
|
||||
# usage: x check-impl
|
||||
# usage: ludic dev check-impl
|
||||
function cmd_check_impl() -> int {
|
||||
CI_PROB = ""; CI_NPROB = 0
|
||||
let allsrc = read_all_selfhost()
|
||||
|
|
@ -568,7 +568,7 @@ function set_union(a: []pointer, b: []pointer) -> []pointer {
|
|||
return out
|
||||
}
|
||||
|
||||
# usage: x check-vocabulary
|
||||
# usage: ludic dev check-vocabulary
|
||||
function cmd_check_vocab() -> int {
|
||||
CV_PROB = ""; CV_N = 0
|
||||
let h = read_file("tools/ludic-tools/ludic_syntax.h")
|
||||
|
|
@ -702,10 +702,10 @@ function xml_valid(text: pointer) -> bool {
|
|||
return sp == 0
|
||||
}
|
||||
|
||||
# usage: x lint-asset <file> (validates one .json or .xml editor asset)
|
||||
# usage: ludic dev lint-asset <file> (validates one .json or .xml editor asset)
|
||||
function cmd_lint_asset() -> int {
|
||||
if arg_count() < 3 { err("usage: x lint-asset <file.json|file.xml>\n"); return 2 }
|
||||
let path = arg(2)
|
||||
if arg_total() < 3 { err("usage: ludic dev lint-asset <file.json|file.xml>\n"); return 2 }
|
||||
let path = arg_n(2)
|
||||
let t = read_file(path)
|
||||
if t == null { err("lint-asset: cannot read " + path + "\n"); return 1 }
|
||||
if s_index(path, ".json", 0) >= 0 {
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
# docgen.ludic — the Ludic documentation-site generator and its guards, ported
|
||||
# off Python (gen.py / check.py / palette.py). Three x subcommands:
|
||||
#
|
||||
# x docs-palette emit selfhost/backend/stdlib/emit_color.ludic +
|
||||
# ludic dev docs-palette emit selfhost/backend/stdlib/emit_color.ludic +
|
||||
# docs/language/colors/palette.json from the palette
|
||||
# table below (the named-colour source of truth).
|
||||
# x docs-gen [--out DIR] parse docs/language/** + docs/site/*, read the asset
|
||||
# ludic dev docs-gen [--out DIR] parse docs/language/** + docs/site/*, read the asset
|
||||
# templates, emit every page + symbols.json + the
|
||||
# highlighter + .nojekyll into DIR (default build/pages).
|
||||
# x docs-check [DIR] the coverage/integrity guard over a generated site.
|
||||
# ludic dev docs-check [DIR] the coverage/integrity guard over a generated site.
|
||||
#
|
||||
# String work is over NUL-terminated byte buffers; helpers here mirror gen.py's
|
||||
# html.escape / json.dumps / front-matter parsing closely enough to reproduce the
|
||||
# Python generator's output byte-for-byte (verified against the oracle in x test).
|
||||
# Python generator's output byte-for-byte (verified against the oracle in ludic dev test).
|
||||
|
||||
# ---- a growable byte buffer (avoids O(n^2) string building) ------------------
|
||||
property Sb { data: pointer = null, len: int = 0, cap: int = 0 }
|
||||
|
|
@ -563,8 +563,8 @@ function palette_emit_ludic(path: pointer) -> bool {
|
|||
sb_puts(b, "# no allocation, identical codegen to writing the hex by hand. Unknown names are\n")
|
||||
sb_puts(b, "# a compile error (color_lookup returns -1, which emit_expr reports).\n")
|
||||
sb_puts(b, "#\n")
|
||||
sb_puts(b, "# GENERATED by `x docs-palette` from the single source-of-truth palette table\n")
|
||||
sb_puts(b, "# in tools/x/docgen.ludic. Edit the palette there and regenerate; do not hand-edit.\n")
|
||||
sb_puts(b, "# GENERATED by `ludic dev docs-palette` from the single source-of-truth palette table\n")
|
||||
sb_puts(b, "# in tools/ludic-cli/docgen.ludic. Edit the palette there and regenerate; do not hand-edit.\n")
|
||||
sb_puts(b, "# ============================================================================\n")
|
||||
sb_puts(b, "\n")
|
||||
sb_puts(b, "function color_lookup(name: pointer) -> int {\n")
|
||||
|
|
@ -620,11 +620,11 @@ function palette_emit_json(path: pointer) -> bool {
|
|||
return write_file(path, sb_str(b))
|
||||
}
|
||||
|
||||
# `x docs-palette` rewrites the two tracked outputs; `x docs-palette --check`
|
||||
# `ludic dev docs-palette` rewrites the two tracked outputs; `ludic dev docs-palette --check`
|
||||
# regenerates into scratch files and compares them to the tracked ones instead,
|
||||
# so the drift guard judges the working tree, not git HEAD.
|
||||
function cmd_docs_palette() -> int {
|
||||
let check = arg_count() > 2 and arg(2) == "--check"
|
||||
let check = arg_total() > 2 and arg_n(2) == "--check"
|
||||
palette_init()
|
||||
# guard: duplicate names
|
||||
let seen = new []pointer
|
||||
|
|
@ -640,8 +640,8 @@ function cmd_docs_palette() -> int {
|
|||
if not palette_emit_ludic(ludic_out) { err("cannot write emit_color.ludic\n"); return 1 }
|
||||
if not palette_emit_json(json_out) { err("cannot write palette.json\n"); return 1 }
|
||||
if check {
|
||||
if not shq(`cmp -s {ludic_out} selfhost/backend/stdlib/emit_color.ludic`) { err("emit_color.ludic drifted from the palette table (run: x docs-palette)\n"); return 1 }
|
||||
if not shq(`cmp -s {json_out} docs/language/colors/palette.json`) { err("palette.json drifted from the palette table (run: x docs-palette)\n"); return 1 }
|
||||
if not shq(`cmp -s {ludic_out} selfhost/backend/stdlib/emit_color.ludic`) { err("emit_color.ludic drifted from the palette table (run: ludic dev docs-palette)\n"); return 1 }
|
||||
if not shq(`cmp -s {json_out} docs/language/colors/palette.json`) { err("palette.json drifted from the palette table (run: ludic dev docs-palette)\n"); return 1 }
|
||||
}
|
||||
print(`OK {string(len(pal_names))} colors`)
|
||||
return 0
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# docgen_check.ludic — `x docs-check [DIR]`: the coverage / integrity guard over
|
||||
# docgen_check.ludic — `ludic dev docs-check [DIR]`: the coverage / integrity guard over
|
||||
# a generated docs site, ported from tools/docgen/check.py. Fails (exit 1) if the
|
||||
# pages contract is broken, an inventory symbol lacks a source file or page, a
|
||||
# token is documented on two pages, a namespace/section is split across dirs, or
|
||||
|
|
@ -94,7 +94,7 @@ function drop_md(fn: pointer) -> pointer {
|
|||
|
||||
function cmd_docs_check() -> int {
|
||||
var site = "build/pages"
|
||||
if arg_count() >= 3 { site = arg(2) }
|
||||
if arg_total() >= 3 { site = arg_n(2) }
|
||||
let problems = new []pointer
|
||||
let warnings = new []pointer
|
||||
|
||||
|
|
@ -102,6 +102,9 @@ function cmd_docs_check() -> int {
|
|||
if not file_exists(`{site}/index.html`) { push(problems, "missing required file: index.html") }
|
||||
if not file_exists(`{site}/api.html`) { push(problems, "missing required file: api.html") }
|
||||
if not file_exists(`{site}/.nojekyll`) { push(problems, "missing required file: .nojekyll") }
|
||||
# the landing page tells people to curl this; a site without it publishes a
|
||||
# broken install command
|
||||
if not file_exists(`{site}/install.sh`) { push(problems, "missing required file: install.sh") }
|
||||
|
||||
# 2) coverage + thin detection over docs/language/**
|
||||
let id2src = meta_new()
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
# docgen_gen.ludic — `x docs-gen`: the documentation-site generator, ported from
|
||||
# docgen_gen.ludic — `ludic dev docs-gen`: the documentation-site generator, ported from
|
||||
# tools/docgen/gen.py. Parses docs/language/** (front-matter + bodies) and
|
||||
# docs/site/*, reads the asset templates in tools/docgen/assets, and emits the
|
||||
# whole pages-branch payload (per-symbol pages, namespace/color/api pages, the
|
||||
# landing page, ludic-highlight.js, symbols.json, .nojekyll) into --out. Output
|
||||
# is byte-for-byte identical to the Python generator (gated in x test).
|
||||
# is byte-for-byte identical to the Python generator (gated in ludic dev test).
|
||||
#
|
||||
# Shared helpers (Sb, hesc/hescattr, the JSON writer, codeify, string utils)
|
||||
# live in docgen.ludic; the JSON reader (json_parse/j_get) in json.ludic.
|
||||
|
|
@ -951,11 +951,11 @@ function render_highlighter(sym: JVal) -> pointer {
|
|||
function cmd_docs_gen() -> int {
|
||||
var out = "build/pages"
|
||||
var i = 2
|
||||
while i < arg_count() {
|
||||
let a = arg(i)
|
||||
while i < arg_total() {
|
||||
let a = arg_n(i)
|
||||
if a == "--out" {
|
||||
i += 1
|
||||
if i < arg_count() { out = arg(i) }
|
||||
if i < arg_total() { out = arg_n(i) }
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
|
@ -1013,6 +1013,12 @@ function cmd_docs_gen() -> int {
|
|||
jvput(root, "highlight", syms.sym)
|
||||
write_file(`{out}/symbols.json`, json_dumps(root, true, false))
|
||||
write_file(`{out}/.nojekyll`, "")
|
||||
# The install one-liner on the landing page points at this site, so the
|
||||
# installer is published with it: the page and the script that page tells you
|
||||
# to pipe into sh can then never be from different releases.
|
||||
let inst = read_file("install.sh")
|
||||
if inst == null { err("docs-gen: install.sh is missing (the landing page's one-liner serves it)\n"); return 1 }
|
||||
write_file(`{out}/install.sh`, inst)
|
||||
print(`docs generated -> {out}`)
|
||||
print(` pages: {string(npages)}`)
|
||||
return 0
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
# lsp_test.ludic — drive bin/ludic-lsp over real LSP traffic against the real
|
||||
# source tree (`x test-lsp`; replaces tools/test-lsp.py). Every assertion is
|
||||
# source tree (`ludic dev test-lsp`; replaces tools/test-lsp.py). Every assertion is
|
||||
# about a feature an editor actually calls: if this passes, VS Code, JetBrains,
|
||||
# Neovim and the rest are exercising code paths that have been checked, because
|
||||
# they all speak to this one binary.
|
||||
230
tools/ludic-cli/main.ludic
Normal file
230
tools/ludic-cli/main.ludic
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
# ludic — the command-line interface to the Ludic toolchain. One native binary
|
||||
# (bin/ludic) written in Ludic and compiled by Ludic; the unix tools, clang and
|
||||
# the compiler it drives are invoked through the `run` intrinsic.
|
||||
#
|
||||
# Two audiences, one binary. A user of the language sees a small surface —
|
||||
# new, build, run, test, add, fmt — that works from any directory against an
|
||||
# installed toolchain. A contributor to the toolchain itself gets everything
|
||||
# else under `ludic dev`: bootstrapping the compiler from its IR seed, the
|
||||
# regression suites, the docs site, releases. Splitting them by namespace keeps
|
||||
# `ludic help` about the language rather than about the repository.
|
||||
#
|
||||
# Installing (macOS, Linux):
|
||||
#
|
||||
# curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh
|
||||
#
|
||||
# Building from a checkout — the one step Ludic cannot do for itself, since
|
||||
# compiling Ludic needs a compiler:
|
||||
#
|
||||
# mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc
|
||||
# bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic
|
||||
# bin/ludic dev build
|
||||
#
|
||||
# Thereafter `ludic dev build` rebuilds the whole toolchain — including the CLI
|
||||
# itself. The `dev` tasks run from the repository root.
|
||||
program Ludic {
|
||||
import "prelude.ludic"
|
||||
import "selfhost.ludic"
|
||||
import "build.ludic"
|
||||
import "project.ludic"
|
||||
import "tools.ludic"
|
||||
import "selfhost_test.ludic"
|
||||
import "test.ludic"
|
||||
import "json.ludic"
|
||||
import "lsp_test.ludic"
|
||||
import "forgejo.ludic"
|
||||
import "checks.ludic"
|
||||
import "docgen.ludic"
|
||||
import "docgen_gen.ludic"
|
||||
import "docgen_check.ludic"
|
||||
import "release.ludic"
|
||||
import "pkg.ludic"
|
||||
import "pkg_test.ludic"
|
||||
|
||||
function usage() -> void {
|
||||
print("ludic — the toolchain for the Ludic language")
|
||||
print("")
|
||||
print("usage: ludic <command> [arguments]")
|
||||
print("")
|
||||
print("start a game:")
|
||||
print(" new <name> create a project that builds and runs as it stands")
|
||||
print(" run [file] [--headless] build the project and run it")
|
||||
print(" build [file] [--headless] [-o out]")
|
||||
print(" compile to build/<name> (a native binary, nothing to ship beside it)")
|
||||
print(" test [file...] compile and run the project's tests")
|
||||
print(" clean remove build/")
|
||||
print("")
|
||||
print("packages:")
|
||||
print(" add <module>[@version] add a dependency, then resolve + fetch + link")
|
||||
print(" get resolve every package.ludic dependency and write the lock")
|
||||
print(" update [module] bump a dependency (or all) to its latest published version")
|
||||
print(" verify check every locked package against the store by content hash")
|
||||
print(" vendor copy the resolved packages into ./vendor for offline builds")
|
||||
print(" build-lib <module.ludic> compile a package's module to a prebuilt dylib in lib/<target>/")
|
||||
print(" link-flags print the clang flags to link this project's prebuilt module dylibs")
|
||||
print("")
|
||||
print("the toolchain:")
|
||||
print(" version print the toolchain version")
|
||||
print(" upgrade [version] reinstall from the docs site (the same script that installed it)")
|
||||
print(" doctor check that the install is complete and usable")
|
||||
print(" fmt [paths...] format Ludic source in place")
|
||||
print(" lsp run the language server on stdio (what editors spawn)")
|
||||
print(" help this message")
|
||||
print("")
|
||||
print("contributing to the toolchain itself:")
|
||||
print(" dev <task> build, test, bootstrap, docs, release (ludic dev help)")
|
||||
print("")
|
||||
print("A file argument runs it: ludic mygame.ludic")
|
||||
}
|
||||
|
||||
function dev_usage() -> void {
|
||||
print("ludic dev — tasks for a checkout of the Ludic toolchain (run from the repository root)")
|
||||
print("")
|
||||
print("build:")
|
||||
print(" dev build build the whole toolchain into bin/ (ludicc, ludic, ludic-fmt, ludic-lsp)")
|
||||
print(" dev build-cli build just bin/ludicc from the IR seed")
|
||||
print(" dev tools [--install] [--test] build the editor toolchain (ludic-fmt, ludic-lsp)")
|
||||
print("")
|
||||
print("test:")
|
||||
print(" dev test the full regression suite")
|
||||
print(" dev test --coverage per-file line coverage over the test specs")
|
||||
print(" dev test-pkg the package-manager suite (hermetic git repos, offline)")
|
||||
print(" dev selfhost-test the self-hosting suite (correctness + bootstrap fixpoints)")
|
||||
print(" dev test-tools the editor-toolchain suite")
|
||||
print(" dev test-lsp drive bin/ludic-lsp over real LSP traffic (exit 2 = no server)")
|
||||
print(" dev golden regenerate selfhost/golden/renders.sha256 (review with git diff)")
|
||||
print("")
|
||||
print("doc / lint checks (Ludic, no Python):")
|
||||
print(" dev check-docs every ```ludic doc fence parses (or is marked skip/expect-error)")
|
||||
print(" dev check-impl every implemented feature has a docs/language page")
|
||||
print(" dev check-vocabulary the vocabulary is in sync across grammar / lexer / header / parser")
|
||||
print(" dev lint-asset <file> validate one editor .json / .xml asset")
|
||||
print(" dev docs-gen [--out DIR] generate the documentation site (default build/pages)")
|
||||
print(" dev docs-check [DIR] coverage/integrity guard over a generated docs site")
|
||||
print(" dev docs-palette [--check] regenerate emit_color.ludic + palette.json from the palette table")
|
||||
print("")
|
||||
print("release:")
|
||||
print(" dev release [major|minor|patch] [--dry-run] [--publish]")
|
||||
print(" cut a release: CHANGELOG + VERSION bump + tag (+ Forgejo release)")
|
||||
print(" dev publish [vX.Y.Z] publish an already-tagged release; what CI runs on a tag push")
|
||||
print(" dev changelog-section <ver> print that release's CHANGELOG.md section")
|
||||
print(" dev changelog-render <ver> <date> <dir>")
|
||||
print(" render a CHANGELOG section from a directory of changesets")
|
||||
print("")
|
||||
print("self-host internals:")
|
||||
print(" dev selfhost-build [ludicc] [out] assemble + compile the self-host compiler")
|
||||
print(" dev bootstrap the self-hosting fixpoint proof (seeded from bin/ludicc)")
|
||||
print(" dev bootstrap-cfree rebuild the compiler from the seed with no C compiler")
|
||||
print(" dev reseed regenerate selfhost/ludicc.seed.ll after a compiler change")
|
||||
print(" dev game-build <ludicc> <game.ludic> <out> compile a Ludic game (headless)")
|
||||
print(" dev sh-compile <ludicc> <in.ludic> <out> compile + link one .ludic file")
|
||||
}
|
||||
|
||||
# a positional argument, or a default when absent
|
||||
function argn(i: int, dflt: pointer) -> pointer {
|
||||
if (i < arg_count()) { return arg(i) }
|
||||
return dflt
|
||||
}
|
||||
|
||||
# does `s` end in ".ludic"? — `ludic mygame.ludic` runs that file, the shape
|
||||
# the compiler-runner had before the CLI grew subcommands.
|
||||
function is_ludic_file(s: pointer) -> bool {
|
||||
let n = slen(s)
|
||||
if n <= 6 { return false }
|
||||
return sslice(s, n - 6, n) == ".ludic"
|
||||
}
|
||||
|
||||
# ---- ludic dev <task> — the toolchain repo's own tasks ---------------------
|
||||
# Every task here reads its arguments one position further along than a
|
||||
# top-level command, so the shift is set once, here.
|
||||
function dispatch_dev(cmd: pointer) -> int {
|
||||
g_shift = 1
|
||||
if (cmd == "build") { return cmd_dev_build() }
|
||||
if (cmd == "build-cli") { return cmd_dev_build_cli() }
|
||||
if (cmd == "tools") { return cmd_tools() }
|
||||
if (cmd == "clean") { return cmd_clean() }
|
||||
if (cmd == "test") {
|
||||
if (argn(3, "") == "--coverage") { return cmd_test_coverage() }
|
||||
return cmd_dev_test()
|
||||
}
|
||||
if (cmd == "selfhost-test") { return cmd_selfhost_test() }
|
||||
if (cmd == "test-tools") { return cmd_test_tools() }
|
||||
if (cmd == "test-lsp") { return cmd_test_lsp() }
|
||||
if (cmd == "test-pkg") { return cmd_test_pkg() }
|
||||
if (cmd == "check-docs") { return cmd_check_docs() }
|
||||
if (cmd == "check-impl") { return cmd_check_impl() }
|
||||
if (cmd == "check-vocabulary") { return cmd_check_vocab() }
|
||||
if (cmd == "lint-asset") { return cmd_lint_asset() }
|
||||
if (cmd == "docs-palette") { return cmd_docs_palette() }
|
||||
if (cmd == "docs-gen") { return cmd_docs_gen() }
|
||||
if (cmd == "docs-check") { return cmd_docs_check() }
|
||||
if (cmd == "golden") { return cmd_golden() }
|
||||
if (cmd == "bootstrap") { return cmd_bootstrap() }
|
||||
if (cmd == "bootstrap-cfree") { return cmd_bootstrap_cfree() }
|
||||
if (cmd == "reseed") { return cmd_reseed() }
|
||||
if (cmd == "selfhost-build") { return cmd_selfhost_build(argn(3, "bin/ludicc"), argn(4, "build/selfhost")) }
|
||||
if (cmd == "sh-compile") {
|
||||
if (arg_count() < 6) { err("usage: ludic dev sh-compile <ludicc> <in.ludic> <out>\n"); exit(1) }
|
||||
return cmd_sh_compile(arg(3), arg(4), arg(5))
|
||||
}
|
||||
if (cmd == "game-build") {
|
||||
if (arg_count() < 6) { err("usage: ludic dev game-build <ludicc> <game.ludic> <out>\n"); exit(1) }
|
||||
return cmd_game_build(arg(3), arg(4), arg(5))
|
||||
}
|
||||
if (cmd == "release") { return cmd_release() }
|
||||
if (cmd == "changelog-render") { return cmd_changelog_render() }
|
||||
if (cmd == "changelog-section") { return cmd_changelog_section() }
|
||||
if (cmd == "publish") { return cmd_publish() }
|
||||
if (cmd == "help") or (cmd == "--help") or (cmd == "-h") or (cmd == "") { dev_usage(); return 0 }
|
||||
# reported here rather than by entry, so the message names the dev task the
|
||||
# user actually typed and the usage shown is the dev one
|
||||
err(`ludic dev: unknown task '{cmd}'\n`)
|
||||
dev_usage()
|
||||
return 1
|
||||
}
|
||||
|
||||
# route one subcommand to its implementation and return its exit code; -1 when
|
||||
# the name is unknown. Returning (rather than exiting in place) lets entry tear
|
||||
# down the per-process scratch directory before the process ends.
|
||||
function dispatch(cmd: pointer) -> int {
|
||||
if (cmd == "new") { return cmd_new() }
|
||||
if (cmd == "build") { return cmd_build() }
|
||||
if (cmd == "run") { return cmd_run() }
|
||||
if (cmd == "test") { return cmd_test() }
|
||||
if (cmd == "clean") { return cmd_clean() }
|
||||
if (cmd == "fmt") { return cmd_fmt() }
|
||||
if (cmd == "lsp") { return cmd_lsp() }
|
||||
if (cmd == "doctor") { return cmd_doctor() }
|
||||
if (cmd == "upgrade") { return cmd_upgrade() }
|
||||
|
||||
if (cmd == "add") { return cmd_pkg_add() }
|
||||
if (cmd == "get") { return cmd_pkg_get() }
|
||||
if (cmd == "update") { return cmd_pkg_update() }
|
||||
if (cmd == "verify") { return cmd_pkg_verify() }
|
||||
if (cmd == "vendor") { return cmd_pkg_vendor() }
|
||||
if (cmd == "build-lib") { return cmd_pkg_build_lib() }
|
||||
if (cmd == "link-flags") { return cmd_pkg_link_flags() }
|
||||
|
||||
if (cmd == "version") or (cmd == "--version") or (cmd == "-v") { return cmd_version() }
|
||||
if (cmd == "help") or (cmd == "--help") or (cmd == "-h") { usage(); return 0 }
|
||||
if (cmd == "dev") { return dispatch_dev(argn(2, "")) }
|
||||
return -1
|
||||
}
|
||||
|
||||
entry {
|
||||
if (arg_count() < 2) { usage(); exit(1) }
|
||||
let cmd = arg(1)
|
||||
# `ludic mygame.ludic` — build and run that file
|
||||
if is_ludic_file(cmd) {
|
||||
let rc = cmd_run_file()
|
||||
tmp_cleanup()
|
||||
exit(rc)
|
||||
}
|
||||
let code = dispatch(cmd)
|
||||
tmp_cleanup()
|
||||
if code >= 0 { exit(code) }
|
||||
err(`ludic: unknown command '{cmd}'\n`)
|
||||
usage()
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
# infrastructure. A source package's Ludic is compiled together with the
|
||||
# consumer via the module-root import fallback in the compiler (do_import); a
|
||||
# prebuilt lib declares its shipped targets and is resolved against the build
|
||||
# target. Everything is content-verified against the lockfile by `x verify`.
|
||||
# target. Everything is content-verified against the lockfile by `ludic verify`.
|
||||
|
||||
# ---- the manifest / lock model ----------------------------------------------
|
||||
|
||||
|
|
@ -419,7 +419,7 @@ function collision(sels: []Manifest) -> pointer {
|
|||
# ---- lockfile writing --------------------------------------------------------
|
||||
|
||||
function write_lock(sels: []Manifest) -> bool {
|
||||
var body = "# package.lock.ludic — generated by `x get`. Do not edit by hand." + nl()
|
||||
var body = "# package.lock.ludic — generated by `ludic get`. Do not edit by hand." + nl()
|
||||
body = body + "lock 1" + nl()
|
||||
let sorted = sort_mans(sels)
|
||||
var i = 0
|
||||
|
|
@ -537,30 +537,30 @@ function set_require(module: pointer, ver: pointer) -> void {
|
|||
|
||||
# ---- the commands ------------------------------------------------------------
|
||||
|
||||
# x add <module>[@version] — add/update a dependency then install
|
||||
# ludic add <module>[@version] — add/update a dependency then install
|
||||
function cmd_pkg_add() -> int {
|
||||
if arg_count() < 3 { err("usage: x add <module>[@version]\n"); return 1 }
|
||||
let spec = split_spec(arg(2))
|
||||
if arg_total() < 3 { err("usage: ludic add <module>[@version]\n"); return 1 }
|
||||
let spec = split_spec(arg_n(2))
|
||||
let module = spec[0]
|
||||
var ver = spec[1]
|
||||
if slen(ver) == 0 {
|
||||
ver = latest_version(module)
|
||||
if slen(ver) == 0 { err(`x: {module} has no published version tags (git tag vX.Y.Z to publish)\n`); return 1 }
|
||||
print(`x add: {module} -> latest v{ver}`)
|
||||
print(`ludic add: {module} -> latest v{ver}`)
|
||||
}
|
||||
set_require(module, ver)
|
||||
return do_install(read_root_manifest())
|
||||
}
|
||||
|
||||
# x get — resolve + fetch + link every dependency in package.ludic, write lock
|
||||
# ludic get — resolve + fetch + link every dependency in package.ludic, write lock
|
||||
function cmd_pkg_get() -> int {
|
||||
let txt = read_file("package.ludic")
|
||||
if txt == null { err("x: no package.ludic in the current directory (x add <module> to start one)\n"); return 1 }
|
||||
if txt == null { err("x: no package.ludic in the current directory (ludic add <module> to start one)\n"); return 1 }
|
||||
print("resolving dependencies (MVS)…")
|
||||
return do_install(parse_manifest(txt))
|
||||
}
|
||||
|
||||
# x update [module] — bump a dep (or all) to its latest published version, relock
|
||||
# ludic update [module] — bump a dep (or all) to its latest published version, relock
|
||||
function cmd_pkg_update() -> int {
|
||||
let root = read_root_manifest()
|
||||
if len(root.deps) == 0 { err("x: package.ludic declares no dependencies\n"); return 1 }
|
||||
|
|
@ -571,7 +571,7 @@ function cmd_pkg_update() -> int {
|
|||
if only == "" or only == d.module {
|
||||
let latest = latest_version(d.module)
|
||||
if slen(latest) > 0 and ver_gt(latest, d.ver) {
|
||||
print(`x update: {d.module} {d.ver} -> {latest}`)
|
||||
print(`ludic update: {d.module} {d.ver} -> {latest}`)
|
||||
set_require(d.module, latest)
|
||||
}
|
||||
}
|
||||
|
|
@ -580,11 +580,11 @@ function cmd_pkg_update() -> int {
|
|||
return do_install(read_root_manifest())
|
||||
}
|
||||
|
||||
# x verify — check every locked package against the store by content hash and
|
||||
# ludic verify — check every locked package against the store by content hash and
|
||||
# confirm the project view links to it
|
||||
function cmd_pkg_verify() -> int {
|
||||
let txt = read_file("package.lock.ludic")
|
||||
if txt == null { err("x: no package.lock.ludic (run x get first)\n"); return 1 }
|
||||
if txt == null { err("x: no package.lock.ludic (run ludic get first)\n"); return 1 }
|
||||
let locked = parse_lock(txt)
|
||||
if len(locked) == 0 { print("lockfile lists no packages"); return 0 }
|
||||
var bad_count = 0
|
||||
|
|
@ -602,7 +602,7 @@ function cmd_pkg_verify() -> int {
|
|||
if got == raw {
|
||||
let link = `ludic_modules/{m.module}`
|
||||
if file_exists(link) { print(` ok {m.module}@{m.ver}`) }
|
||||
else { print(` UNLINKED {m.module}@{m.ver} (ludic_modules view missing — run x get)`); bad_count += 1 }
|
||||
else { print(` UNLINKED {m.module}@{m.ver} (ludic_modules view missing — run ludic get)`); bad_count += 1 }
|
||||
} else {
|
||||
print(` TAMPERED {m.module}@{m.ver} (want sha256:{sslice(raw, 0, 12)}… got sha256:{sslice(got, 0, 12)}…)`)
|
||||
bad_count += 1
|
||||
|
|
@ -615,11 +615,11 @@ function cmd_pkg_verify() -> int {
|
|||
return 1
|
||||
}
|
||||
|
||||
# x vendor — copy the resolved packages into ./vendor for hermetic/offline
|
||||
# ludic vendor — copy the resolved packages into ./vendor for hermetic/offline
|
||||
# builds. Build against them with LUDIC_MODULES=vendor.
|
||||
function cmd_pkg_vendor() -> int {
|
||||
let txt = read_file("package.lock.ludic")
|
||||
if txt == null { err("x: no package.lock.ludic (run x get first)\n"); return 1 }
|
||||
if txt == null { err("x: no package.lock.ludic (run ludic get first)\n"); return 1 }
|
||||
let locked = parse_lock(txt)
|
||||
run("rm -rf vendor")
|
||||
var i = 0
|
||||
|
|
@ -627,7 +627,7 @@ function cmd_pkg_vendor() -> int {
|
|||
let m = locked[i]
|
||||
let raw = strip_prefix(m.hash, "sha256:")
|
||||
let dest = `{store_root()}{raw}`
|
||||
if not file_exists(dest) { err(`x: {m.module}@{m.ver} not in the store — run x get\n`); return 1 }
|
||||
if not file_exists(dest) { err(`x: {m.module}@{m.ver} not in the store — run ludic get\n`); return 1 }
|
||||
let vdir = `vendor/{m.module}`
|
||||
run(`mkdir -p "$(dirname {vdir})"`)
|
||||
run(`cp -R {dest} {vdir}`)
|
||||
|
|
@ -640,7 +640,7 @@ function cmd_pkg_vendor() -> int {
|
|||
|
||||
# ---- prebuilt binary packages (issue #64) -----------------------------------
|
||||
|
||||
# x build-lib [module.ludic] — compile a package's module to a prebuilt dylib for
|
||||
# ludic build-lib [module.ludic] — compile a package's module to a prebuilt dylib for
|
||||
# the current target, under lib/<target>/. The developer runs this in the package
|
||||
# repo, commits the artifact, and `git tag`s — consumers fetch and link it.
|
||||
# The compiler is $LUDICC (default bin/ludicc) so this works both in-repo and,
|
||||
|
|
@ -648,7 +648,7 @@ function cmd_pkg_vendor() -> int {
|
|||
function cmd_pkg_build_lib() -> int {
|
||||
var src = argn(2, "")
|
||||
if src == "" { src = capture_line("ls *.ludic 2>/dev/null | grep -v package.ludic | head -1") }
|
||||
if src == "" or not file_exists(src) { err("usage: x build-lib <module.ludic> (run in the package directory)\n"); return 1 }
|
||||
if src == "" or not file_exists(src) { err("usage: ludic build-lib <module.ludic> (run in the package directory)\n"); return 1 }
|
||||
let man = read_root_manifest()
|
||||
var name = ""
|
||||
if man.module != "" { name = capture_line(`basename {man.module}`) }
|
||||
|
|
@ -671,9 +671,9 @@ function cmd_pkg_build_lib() -> int {
|
|||
return 0
|
||||
}
|
||||
|
||||
# x link-flags — print the clang link flags a consumer build needs to pull in the
|
||||
# ludic link-flags — print the clang link flags a consumer build needs to pull in the
|
||||
# project's prebuilt module dylibs (for the current target). Portable escape from
|
||||
# `x app` (which is in-repo only): any build system can splice `$(x link-flags)`
|
||||
# `ludic build` (which is in-repo only): any build system can splice `$(ludic link-flags)`
|
||||
# into its link step.
|
||||
function cmd_pkg_link_flags() -> int {
|
||||
let f = prebuilt_link_flags()
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
# 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/x` / `bin/ludicc` process the
|
||||
# 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
|
||||
|
|
@ -63,11 +63,11 @@ function cmd_test_pkg() -> int {
|
|||
|
||||
let envp = `LUDIC_PKG_PROXY={proxy} LUDIC_STORE={store}`
|
||||
|
||||
# ---- x get: resolve, fetch, store, link, lock -----------------------------
|
||||
if shq(`( cd {proj} && {envp} {root}/bin/x get > {work}/get.out 2>&1 )`) {
|
||||
ok("x get resolves + fetches the dependency graph")
|
||||
# ---- 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("x get failed", capture_line(`tail -2 {work}/get.out`))
|
||||
bad2("ludic get failed", capture_line(`tail -2 {work}/get.out`))
|
||||
}
|
||||
|
||||
var lock = read_file(`{proj}/package.lock.ludic`)
|
||||
|
|
@ -98,23 +98,23 @@ function cmd_test_pkg() -> int {
|
|||
bad2("consumer build failed", capture_line(`tail -2 {work}/build.out`))
|
||||
}
|
||||
|
||||
# ---- x verify: content integrity against the store ------------------------
|
||||
if shq(`( cd {proj} && {envp} {root}/bin/x verify > {work}/verify.out 2>&1 )`) {
|
||||
ok("x verify passes on an untouched store")
|
||||
} else { bad2("x verify failed unexpectedly", capture_line(`tail -2 {work}/verify.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/\".*//'`)
|
||||
run(`echo tampered >> {store}/{uhash}/util.ludic`)
|
||||
if not shq(`( cd {proj} && {envp} {root}/bin/x verify > {work}/verify2.out 2>&1 )`) {
|
||||
ok("x verify detects a tampered store entry")
|
||||
} else { bad("x verify missed a tampered store entry") }
|
||||
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
|
||||
# `x get` trusts an existing one — drop it first, then re-fetch a clean copy.
|
||||
# `ludic get` trusts an existing one — drop it first, then re-fetch a clean copy.
|
||||
run(`rm -rf {store}/{uhash}`)
|
||||
run(`( cd {proj} && {envp} {root}/bin/x get > /dev/null 2>&1 )`)
|
||||
if shq(`( cd {proj} && {envp} {root}/bin/x verify > {work}/verify3.out 2>&1 )`) {
|
||||
ok("re-fetching heals a tampered store entry (x verify green again)")
|
||||
run(`( 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 --------
|
||||
|
|
@ -122,7 +122,7 @@ function cmd_test_pkg() -> int {
|
|||
# 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 `x app` — the game is compiled
|
||||
# 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`
|
||||
run(`mkdir -p {epkg} && git -C {epkg} init -q`)
|
||||
|
|
@ -132,14 +132,14 @@ function cmd_test_pkg() -> int {
|
|||
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/x get > {work}/get2.out 2>&1 )`) {
|
||||
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("x get for the ECS package failed", capture_line(`tail -1 {work}/get2.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`
|
||||
|
|
@ -147,7 +147,7 @@ function cmd_test_pkg() -> int {
|
|||
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/x add example.test/dupe@1.0.0 > {work}/dupe.out 2>&1 )`) and shq(`grep -q collision {work}/dupe.out`) {
|
||||
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
|
||||
|
|
@ -159,16 +159,16 @@ function cmd_test_pkg() -> int {
|
|||
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/x add example.test/prebuilt@1.0.0 > {work}/pb.out 2>&1 )`) and shq(`grep -q 'prebuilt lib' {work}/pb.out`) {
|
||||
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())
|
||||
|
||||
# ---- x vendor: hermetic copy ---------------------------------------------
|
||||
run(`( cd {proj} && {envp} {root}/bin/x get > /dev/null 2>&1 )`)
|
||||
if shq(`( cd {proj} && {envp} {root}/bin/x vendor > {work}/vendor.out 2>&1 )`) and file_exists(`{proj}/vendor/example.test/util/util.ludic`) {
|
||||
ok("x vendor copies the resolved packages into ./vendor")
|
||||
} else { bad2("x vendor failed", capture_line(`tail -1 {work}/vendor.out`)) }
|
||||
# ---- ludic vendor: hermetic copy ---------------------------------------------
|
||||
run(`( 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
|
||||
|
|
@ -182,7 +182,7 @@ function cmd_test_pkg() -> int {
|
|||
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/x get > {work}/get4.out 2>&1 )`) {
|
||||
if shq(`( cd {proj4} && {envp} {root}/bin/ludic get > {work}/get4.out 2>&1 )`) {
|
||||
run(`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`)
|
||||
|
|
@ -190,10 +190,10 @@ function cmd_test_pkg() -> int {
|
|||
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("x get for the coach package failed", capture_line(`tail -1 {work}/get4.out`)) }
|
||||
} 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 (x build-lib), fetched as a `kind prebuilt`
|
||||
# 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.
|
||||
|
|
@ -206,21 +206,21 @@ function cmd_test_pkg() -> int {
|
|||
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`)) }
|
||||
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/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`)) }
|
||||
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 `x link-flags` (a real external build's link step).
|
||||
# link the module dylib via `ludic 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 )`)
|
||||
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" {
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
# prelude.ludic — the shared runtime for `x`, the Ludic task runner.
|
||||
# prelude.ludic — the shared runtime for `ludic`, the command-line interface.
|
||||
#
|
||||
# `x` replaces every build/test/bootstrap shell script in the repo: it is a
|
||||
# single native binary (bin/x) that drives clang, the self-host compiler and the
|
||||
# unix tools the same way the old *.sh files did — only now it is written in
|
||||
# Ludic and compiled by Ludic. This fragment is the tiny standard library the
|
||||
# commands lean on: process control, file IO, string trimming and a colored
|
||||
# PASS/FAIL test harness. It carries no ECS, so it links as a plain CLI program.
|
||||
# `ludic` is the one tool a user of the language ever runs: it creates projects,
|
||||
# compiles and runs them, resolves packages, formats, tests, and (under
|
||||
# `ludic dev`) drives every build/bootstrap/release task of the toolchain repo
|
||||
# itself. It is a single native binary written in Ludic and compiled by Ludic,
|
||||
# driving clang, the compiler and the unix tools through `run`. This fragment is
|
||||
# the tiny standard library the commands lean on: process control, file IO,
|
||||
# string trimming and a colored PASS/FAIL test harness. It carries no ECS, so it
|
||||
# links as a plain CLI program.
|
||||
#
|
||||
# Everything runs relative to the current directory, so `x` must be invoked from
|
||||
# the repository root (the one-line bootstrap in README.md does exactly that).
|
||||
# The user-facing commands work from any directory. The `ludic dev` tasks run
|
||||
# relative to the current directory and expect the toolchain repo root.
|
||||
|
||||
# ---- file IO ----------------------------------------------------------------
|
||||
|
||||
|
|
@ -51,7 +53,7 @@ function shq(cmd: pointer) -> bool { return exit_code(run(cmd)) == 0 }
|
|||
|
||||
# ---- scratch files ------------------------------------------------------------
|
||||
# Every scratch file the runner writes lives under one per-process directory
|
||||
# (`$TMPDIR/x_<pid>`), so `x test` and an `x check-*` can run side by side without
|
||||
# (`$TMPDIR/x_<pid>`), so `ludic dev test` and an `x check-*` can run side by side without
|
||||
# clobbering each other's captures. main removes it on the way out.
|
||||
var x_tmp: pointer = null
|
||||
|
||||
|
|
@ -59,7 +61,7 @@ function tmp_dir() -> pointer {
|
|||
if x_tmp == null {
|
||||
var base = Os.temp_dir()
|
||||
if len(base) > 1 and base[len(base) - 1] == '/' { base = base[0..len(base) - 1] } # macOS: TMPDIR ends in '/'
|
||||
x_tmp = `{base}/x_{Os.pid()}`
|
||||
x_tmp = `{base}/ludic_{Os.pid()}`
|
||||
run(`mkdir -p {x_tmp}`)
|
||||
}
|
||||
return x_tmp
|
||||
|
|
@ -69,10 +71,10 @@ function tmp_dir() -> pointer {
|
|||
function tmp_path(name: pointer) -> pointer { return `{tmp_dir()}/{name}` }
|
||||
|
||||
# remove the scratch directory (idempotent; a no-op when nothing was written).
|
||||
# X_KEEP_TMP=1 leaves it in place, and says where, for debugging a failing case.
|
||||
# LUDIC_KEEP_TMP=1 leaves it in place, and says where, for debugging a failing case.
|
||||
function tmp_cleanup() -> void {
|
||||
if x_tmp == null { return }
|
||||
if getenv_or("X_KEEP_TMP", "0") == "1" { print(`scratch kept: {x_tmp}`) }
|
||||
if getenv_or("LUDIC_KEEP_TMP", "0") == "1" { print(`scratch kept: {x_tmp}`) }
|
||||
else { run(`rm -rf {x_tmp}`) }
|
||||
x_tmp = null
|
||||
}
|
||||
|
|
@ -172,3 +174,83 @@ function report() -> int {
|
|||
if (FAIL == 0) { return 0 }
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---- command arguments -------------------------------------------------------
|
||||
#
|
||||
# A command reads its own arguments as arg_n(1), arg_n(2)… whether it was
|
||||
# reached as `ludic get` or as `ludic dev docs-check DIR`. main sets the shift
|
||||
# once per dispatch, so no command has to know how deep its namespace is —
|
||||
# getting that wrong is how `docs-check DIR` silently checked the wrong
|
||||
# directory.
|
||||
var g_shift: int = 0
|
||||
|
||||
function arg_n(i: int) -> pointer { return arg(i + g_shift) }
|
||||
function arg_total() -> int { return arg_count() - g_shift }
|
||||
|
||||
# ---- the toolchain install ---------------------------------------------------
|
||||
#
|
||||
# The user-facing commands run anywhere, so they cannot assume a `bin/` in the
|
||||
# current directory the way the old repo-root-only task runner did. They ask
|
||||
# here instead, and get the same answer in both layouts that exist: an install
|
||||
# (~/.ludic/bin/ludic, root ~/.ludic) and a checkout of the toolchain repo
|
||||
# (bin/ludic, root the repo). The compiler derives its own root by the identical
|
||||
# rule (see ludic_home() in selfhost/frontend/parse.ludic), so the two never
|
||||
# disagree about where the runtime and the bundled packages live.
|
||||
|
||||
var g_home: pointer = null
|
||||
|
||||
# the directory holding this binary, with a trailing '/'. argv[0] carries no
|
||||
# directory when the CLI was found on $PATH, which is the normal case for an
|
||||
# install — ask the shell where it found it.
|
||||
function self_dir() -> pointer {
|
||||
var a0 = arg(0)
|
||||
var slash = -1
|
||||
var i = 0
|
||||
while a0[i] != 0 { if a0[i] == '/' { slash = i }; i += 1 }
|
||||
if slash < 0 {
|
||||
let found = capture_line(`command -v {a0}`)
|
||||
if found == "" { return "" }
|
||||
a0 = found
|
||||
slash = -1
|
||||
i = 0
|
||||
while a0[i] != 0 { if a0[i] == '/' { slash = i }; i += 1 }
|
||||
if slash < 0 { return "" }
|
||||
}
|
||||
return a0[0..slash + 1]
|
||||
}
|
||||
|
||||
# The toolchain install root, with a trailing '/', or "" for the current
|
||||
# directory. $LUDIC_HOME wins; otherwise the binary's own directory decides, and
|
||||
# a directory named `bin` means the root is its parent.
|
||||
function ludic_home() -> pointer {
|
||||
if g_home != null { return g_home }
|
||||
let env = getenv("LUDIC_HOME")
|
||||
if env != null {
|
||||
var e = env
|
||||
if len(e) > 0 and e[len(e) - 1] != '/' { e = e + "/" }
|
||||
g_home = e
|
||||
return g_home
|
||||
}
|
||||
var d = self_dir()
|
||||
if len(d) >= 4 and d[len(d) - 4..len(d)] == "bin/" { d = d[0..len(d) - 4] }
|
||||
# a source checkout the CLI was run from elsewhere in: prefer a ./bin here
|
||||
if not is_exec(`{d}bin/ludicc`) and is_exec("bin/ludicc") { d = "" }
|
||||
g_home = d
|
||||
return g_home
|
||||
}
|
||||
|
||||
# the path to a toolchain binary (ludicc, ludic-fmt, ludic-lsp). Falls back to
|
||||
# the bare name — on $PATH — when the install root has no bin/ of its own.
|
||||
function tool(name: pointer) -> pointer {
|
||||
let p = `{ludic_home()}bin/{name}`
|
||||
if is_exec(p) { return p }
|
||||
return name
|
||||
}
|
||||
|
||||
function ludicc() -> pointer { return tool("ludicc") }
|
||||
|
||||
# true in a checkout of the toolchain repo itself, where the `ludic dev` tasks
|
||||
# have something to work on.
|
||||
function in_toolchain_repo() -> bool {
|
||||
return file_exists("selfhost/ludicc.seed.ll") and file_exists("tools/ludic-cli/main.ludic")
|
||||
}
|
||||
397
tools/ludic-cli/project.ludic
Normal file
397
tools/ludic-cli/project.ludic
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
# project.ludic — the commands a user of the language runs on their own project:
|
||||
# new, build, run, test, fmt, lsp, doctor, upgrade.
|
||||
#
|
||||
# The shape of a project is a convention, not a configuration file: a manifest
|
||||
# (package.ludic), the program under src/, tests under tests/, assets under
|
||||
# assets/, output in build/. `ludic build` in such a directory needs no
|
||||
# arguments; given a path it compiles that file instead, so a single .ludic
|
||||
# lying around is still a one-command build.
|
||||
#
|
||||
# None of this assumes a checkout of the toolchain — the compiler, the engine
|
||||
# runtime and the bundled ludic.* packages are found through ludic_home().
|
||||
|
||||
# ---- project discovery ------------------------------------------------------
|
||||
|
||||
# the `package "name"` line of ./package.ludic, or "" when there is no manifest
|
||||
function manifest_name() -> pointer {
|
||||
let txt = read_file("package.ludic")
|
||||
if txt == null { return "" }
|
||||
let m = parse_manifest(txt)
|
||||
return m.module
|
||||
}
|
||||
|
||||
# the name to give the built binary: the manifest's module (its last dotted
|
||||
# segment, so ludic.snake builds `snake`), else the entry file's base name.
|
||||
function project_name(entry: pointer) -> pointer {
|
||||
let mod = manifest_name()
|
||||
if mod != "" {
|
||||
var last = -1
|
||||
var i = 0
|
||||
while mod[i] != 0 { if mod[i] == '.' { last = i }; i += 1 }
|
||||
if last >= 0 { return mod[last + 1..i] }
|
||||
return mod
|
||||
}
|
||||
return capture_line(`basename {entry} .ludic`)
|
||||
}
|
||||
|
||||
# The program to compile. An explicit path always wins; otherwise the
|
||||
# conventional entry points are tried in order, and finally a lone .ludic file in
|
||||
# the current directory (so a scratch directory needs no ceremony). "" when
|
||||
# nothing was found.
|
||||
function find_entry(explicit: pointer) -> pointer {
|
||||
if explicit != "" { return explicit }
|
||||
if file_exists("src/main.ludic") { return "src/main.ludic" }
|
||||
if file_exists("main.ludic") { return "main.ludic" }
|
||||
let one = capture_line("ls *.ludic 2>/dev/null")
|
||||
if one != "" and not shq("ls *.ludic 2>/dev/null | tail -n +2 | grep -q .") { return one }
|
||||
return ""
|
||||
}
|
||||
|
||||
# report the "no entry point" case the same way everywhere
|
||||
function no_entry() -> int {
|
||||
err("ludic: no program to build here.\n")
|
||||
err(" expected src/main.ludic (or main.ludic), or name a file: ludic build path/to/game.ludic\n")
|
||||
err(" start a new project with: ludic new <name>\n")
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---- ludic new --------------------------------------------------------------
|
||||
|
||||
# The smallest program worth running: a window, an entity moving under the ECS,
|
||||
# text, and a key to quit. A new project should do something the moment it is
|
||||
# created — an empty main() teaches nothing about the language.
|
||||
#
|
||||
# Backticks interpolate, and this text is full of braces, so it is built by
|
||||
# concatenation. The template lives here rather than in a data file because the
|
||||
# CLI must scaffold from an install where no repo tree exists.
|
||||
function template_main(name: pointer) -> pointer {
|
||||
var b = "# src/main.ludic — the program. Build and play it with: ludic run\n"
|
||||
b = b + "program " + name + " {\n"
|
||||
b = b + "\n"
|
||||
b = b + " property Pos { x: int = 0, y: int = 0 }\n"
|
||||
b = b + " property Vel { dx: int = 0, dy: int = 0 }\n"
|
||||
b = b + "\n"
|
||||
b = b + " handler Boot phase Start {\n"
|
||||
b = b + " spawn Ball { Pos { x: 152, y: 112 }, Vel { dx: 2, dy: 1 } }\n"
|
||||
b = b + " }\n"
|
||||
b = b + "\n"
|
||||
b = b + " handler Move phase FixedUpdate {\n"
|
||||
b = b + " for (p, v) in query [Pos, Vel] {\n"
|
||||
b = b + " p.x = p.x + v.dx\n"
|
||||
b = b + " p.y = p.y + v.dy\n"
|
||||
b = b + " if p.x < 0 or p.x > 304 { v.dx = 0 - v.dx }\n"
|
||||
b = b + " if p.y < 0 or p.y > 224 { v.dy = 0 - v.dy }\n"
|
||||
b = b + " }\n"
|
||||
b = b + " }\n"
|
||||
b = b + "\n"
|
||||
b = b + " handler Draw phase Render {\n"
|
||||
b = b + " Screen.clear(0x0d1020)\n"
|
||||
b = b + " for (p) in query [Pos] {\n"
|
||||
b = b + " Screen.fill_rectangle(x: p.x, y: p.y, width: 16, height: 16, color: Color.Crimson)\n"
|
||||
b = b + " }\n"
|
||||
b = b + " Screen.draw_text(x: 8, y: 8, text: \"HELLO LUDIC\", color: Color.White, scale: 1)\n"
|
||||
b = b + " Screen.show()\n"
|
||||
b = b + " }\n"
|
||||
b = b + "\n"
|
||||
b = b + " handler Keys phase Input {\n"
|
||||
b = b + " if Input.key() == 'q' { quit() }\n"
|
||||
b = b + " }\n"
|
||||
b = b + "}\n"
|
||||
return b
|
||||
}
|
||||
|
||||
function template_test(name: pointer) -> pointer {
|
||||
var b = "# A test program is a set of test blocks; ludic test compiles and runs every\n"
|
||||
b = b + "# tests/*.ludic and reports the results.\n"
|
||||
b = b + "program " + name + "Spec {\n"
|
||||
b = b + " test \"arithmetic still works\" {\n"
|
||||
b = b + " expect_eq(2 + 2, 4)\n"
|
||||
b = b + " }\n"
|
||||
b = b + "}\n"
|
||||
return b
|
||||
}
|
||||
|
||||
function template_manifest(name: pointer) -> pointer {
|
||||
return "package \"" + name + "\"\nversion \"0.1.0\"\nkind source\n"
|
||||
}
|
||||
|
||||
function template_gitignore() -> pointer {
|
||||
return "build/\nludic_modules/\nvendor/\n*.ll\n"
|
||||
}
|
||||
|
||||
function template_readme(name: pointer) -> pointer {
|
||||
var b = "# " + name + "\n"
|
||||
b = b + "\n"
|
||||
b = b + "A game written in Ludic (https://workshopsoft.pages.workshopsoft.io/ludic/).\n"
|
||||
b = b + "\n"
|
||||
b = b + " ludic run # build and play\n"
|
||||
b = b + " ludic test # run the tests\n"
|
||||
return b
|
||||
}
|
||||
|
||||
# ludic new <name> — scaffold a project that builds and runs as it stands.
|
||||
function cmd_new() -> int {
|
||||
if arg_total() < 3 {
|
||||
err("usage: ludic new <name>\n")
|
||||
return 1
|
||||
}
|
||||
let name = arg_n(2)
|
||||
if file_exists(name) {
|
||||
err(`ludic new: {name} already exists\n`)
|
||||
return 1
|
||||
}
|
||||
run(`mkdir -p {name}/src {name}/tests {name}/assets`)
|
||||
if not write_file(`{name}/package.ludic`, template_manifest(name)) {
|
||||
err(`ludic new: cannot write {name}/package.ludic\n`)
|
||||
return 1
|
||||
}
|
||||
write_file(`{name}/src/main.ludic`, template_main(title_case(name)))
|
||||
write_file(`{name}/tests/smoke.ludic`, template_test(title_case(name)))
|
||||
write_file(`{name}/.gitignore`, template_gitignore())
|
||||
write_file(`{name}/README.md`, template_readme(name))
|
||||
print(`created {name}/`)
|
||||
print(" package.ludic the manifest — name, version, dependencies")
|
||||
print(" src/main.ludic the program")
|
||||
print(" tests/smoke.ludic a test to grow")
|
||||
print(" assets/ sprites, fonts and sounds")
|
||||
print("")
|
||||
print(`next: cd {name} && ludic run`)
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---- ludic build / run ------------------------------------------------------
|
||||
|
||||
# shared flag parsing for build and run: returns the entry file ("" = none),
|
||||
# filling the globals below.
|
||||
var g_mode: int = 1 # 1 = windowed, 2 = headless
|
||||
var g_out: pointer = "" # -o
|
||||
var g_save: bool = false # --save-temps
|
||||
|
||||
function parse_build_args(start: int) -> pointer {
|
||||
var src = ""
|
||||
g_mode = 1
|
||||
g_out = ""
|
||||
g_save = false
|
||||
var ai = start
|
||||
while ai < arg_total() {
|
||||
let a = arg_n(ai)
|
||||
if a == "--headless" { g_mode = 2 }
|
||||
else if a == "--windowed" { g_mode = 1 }
|
||||
else if a == "--save-temps" { g_save = true }
|
||||
else if a == "-o" { ai += 1; if ai < arg_total() { g_out = arg_n(ai) } }
|
||||
else if a[0] != '-' { src = a }
|
||||
ai += 1
|
||||
}
|
||||
return find_entry(src)
|
||||
}
|
||||
|
||||
# where the binary lands: -o if given, else build/<name> (build/<name>_headless
|
||||
# for a headless build, so the two can coexist).
|
||||
function output_path(entry: pointer) -> pointer {
|
||||
if g_out != "" { return g_out }
|
||||
let name = project_name(entry)
|
||||
if g_mode == 2 { return `build/{name}_headless` }
|
||||
return `build/{name}`
|
||||
}
|
||||
|
||||
# ludic build [file] [--headless] [-o out] [--save-temps]
|
||||
function cmd_build() -> int {
|
||||
let entry = parse_build_args(2)
|
||||
if entry == "" { return no_entry() }
|
||||
let out = output_path(entry)
|
||||
if not compile_app(entry, out, g_mode, g_save) { return 1 }
|
||||
if g_mode == 2 {
|
||||
print(`built {out} (headless: reads one key per frame from stdin, writes build/out.ppm)`)
|
||||
} else {
|
||||
print(`built {out}`)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
# ludic run [file] [--headless] — build, then run it from the project directory
|
||||
# so assets/ resolves relative to the game.
|
||||
function cmd_run() -> int {
|
||||
let entry = parse_build_args(2)
|
||||
if entry == "" { return no_entry() }
|
||||
let out = output_path(entry)
|
||||
if not compile_app(entry, out, g_mode, g_save) { return 1 }
|
||||
return sh(`./{out}`)
|
||||
}
|
||||
|
||||
# `ludic mygame.ludic` — the file is argv[1], so the scan starts there.
|
||||
function cmd_run_file() -> int {
|
||||
let entry = parse_build_args(1)
|
||||
if entry == "" { return no_entry() }
|
||||
let out = output_path(entry)
|
||||
if not compile_app(entry, out, g_mode, g_save) { return 1 }
|
||||
return sh(`./{out}`)
|
||||
}
|
||||
|
||||
# ---- ludic test -------------------------------------------------------------
|
||||
|
||||
# every test program in the project: tests/*.ludic plus any *_test.ludic under
|
||||
# src/ (both conventions are in use, and neither is worth arguing about).
|
||||
function test_files() -> []pointer {
|
||||
var files = split_lines(capture("ls tests/*.ludic 2>/dev/null"))
|
||||
let more = split_lines(capture("find src -name '*_test.ludic' 2>/dev/null | sort"))
|
||||
var i = 0
|
||||
while i < len(more) { push(files, more[i]); i += 1 }
|
||||
return files
|
||||
}
|
||||
|
||||
# ludic test [file...] — compile each test program headlessly and run it. A test
|
||||
# program's own runner prints ok/FAIL per test block and exits non-zero if any
|
||||
# failed, so this reports one line per file and forwards the failure.
|
||||
function cmd_test() -> int {
|
||||
var files = new []pointer
|
||||
var ai = 2
|
||||
while ai < arg_total() { if arg_n(ai)[0] != '-' { push(files, arg_n(ai)) }; ai += 1 }
|
||||
if len(files) == 0 { files = test_files() }
|
||||
if len(files) == 0 {
|
||||
err("ludic test: no tests found (expected tests/*.ludic or src/**/*_test.ludic)\n")
|
||||
return 1
|
||||
}
|
||||
run("mkdir -p build")
|
||||
var failed = 0
|
||||
var i = 0
|
||||
while i < len(files) {
|
||||
let f = files[i]
|
||||
let bin = `{tmp_dir()}/{flat(strip_ext(f))}`
|
||||
if not compile_app(f, bin, 2, false) {
|
||||
print(` {c_red()}FAIL{c_reset()} {f} (did not compile)`)
|
||||
failed += 1
|
||||
} else {
|
||||
# a test program prints one line per test block; that belongs on screen
|
||||
# when something failed and nowhere when everything passed.
|
||||
let log = `{bin}.out`
|
||||
let rc = sh(`{bin} < /dev/null > {log} 2>&1`)
|
||||
if rc == 0 { print(` {c_green()}PASS{c_reset()} {f}`) }
|
||||
else {
|
||||
print(` {c_red()}FAIL{c_reset()} {f} (exit {string(rc)})`)
|
||||
let txt = read_file(log)
|
||||
if txt != null { out(txt) }
|
||||
failed += 1
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
print("")
|
||||
if failed == 0 {
|
||||
print(`== {string(len(files))} test files passed ==`)
|
||||
return 0
|
||||
}
|
||||
print(`== {string(failed)} of {string(len(files))} test files failed ==`)
|
||||
return 1
|
||||
}
|
||||
|
||||
# drop the extension from a path ("tests/combat.ludic" -> "tests/combat")
|
||||
function strip_ext(p: pointer) -> pointer {
|
||||
let n = len(p)
|
||||
if n > 6 and p[n - 6..n] == ".ludic" { return p[0..n - 6] }
|
||||
return p
|
||||
}
|
||||
|
||||
# ---- ludic fmt / lsp --------------------------------------------------------
|
||||
|
||||
# ludic fmt [paths...] — the formatter over the project (src/ and tests/ by
|
||||
# default), or over the paths named.
|
||||
function cmd_fmt() -> int {
|
||||
var args = ""
|
||||
var ai = 2
|
||||
while ai < arg_total() { args = `{args} {arg_n(ai)}`; ai += 1 }
|
||||
if args == "" {
|
||||
let found = capture_line("find src tests -name '*.ludic' 2>/dev/null | sort")
|
||||
if found == "" {
|
||||
err("ludic fmt: nothing to format (no src/ or tests/ here — name the files instead)\n")
|
||||
return 1
|
||||
}
|
||||
args = ` {found}`
|
||||
}
|
||||
return sh(`{tool("ludic-fmt")}{args}`)
|
||||
}
|
||||
|
||||
# ludic lsp — the language server on stdio. Editors are configured to run this,
|
||||
# so the server's location is the CLI's problem rather than the user's.
|
||||
function cmd_lsp() -> int {
|
||||
var args = ""
|
||||
var ai = 2
|
||||
while ai < arg_total() { args = `{args} {arg_n(ai)}`; ai += 1 }
|
||||
return sh(`exec {tool("ludic-lsp")}{args}`)
|
||||
}
|
||||
|
||||
# ---- ludic doctor -----------------------------------------------------------
|
||||
|
||||
function doctor_line(label: pointer, good: bool, detail: pointer) -> void {
|
||||
if good { print(` {c_green()}ok{c_reset()} {label} {detail}`) }
|
||||
else { print(` {c_red()}no{c_reset()} {label} {detail}`) }
|
||||
}
|
||||
|
||||
# ludic doctor — answer "is my install healthy?" without making the user guess.
|
||||
# Every line is something that has actually gone wrong for someone: a missing
|
||||
# clang, a half-unpacked install, a PATH that finds nothing.
|
||||
function cmd_doctor() -> int {
|
||||
let home = ludic_home()
|
||||
var shown = home
|
||||
if shown == "" { shown = "(current directory)" }
|
||||
print(`ludic doctor`)
|
||||
print("")
|
||||
doctor_line("install root ", true, shown)
|
||||
var bad_count = 0
|
||||
|
||||
# $LUDIC_CC may carry flags (the Linux build injects a shim); the first word
|
||||
# is the program to look for.
|
||||
let cc_name = getenv_or("LUDIC_CC", "clang")
|
||||
let cc_prog = capture_line(`printf '%s' '{cc_name}' | cut -d' ' -f1`)
|
||||
let have_cc = shq(`command -v {cc_prog} >/dev/null 2>&1`)
|
||||
doctor_line("C toolchain ", have_cc, `{cc_name} (assembles and links the emitted IR)`)
|
||||
if not have_cc { bad_count += 1 }
|
||||
|
||||
let have_cc2 = is_exec(ludicc()) or shq("command -v ludicc >/dev/null 2>&1")
|
||||
doctor_line("compiler ", have_cc2, ludicc())
|
||||
if not have_cc2 { bad_count += 1 }
|
||||
|
||||
let have_rt = file_exists(`{home}runtime/native/cocoa.ll`)
|
||||
doctor_line("runtime ", have_rt, `{home}runtime/native/ (the engine, spliced into a game)`)
|
||||
if not have_rt { bad_count += 1 }
|
||||
|
||||
let have_pkgs = file_exists(`{home}packages/ludic.core/package.ludic`)
|
||||
doctor_line("packages ", have_pkgs, `{home}packages/ (the bundled ludic.* modules)`)
|
||||
|
||||
let ver = read_file(`{home}VERSION`)
|
||||
var vs = "(unknown)"
|
||||
if ver != null { vs = s_trim(ver) }
|
||||
doctor_line("version ", ver != null, vs)
|
||||
|
||||
let on_path = capture_line("command -v ludic")
|
||||
doctor_line("on PATH ", on_path != "", on_path)
|
||||
|
||||
if not is_darwin() {
|
||||
print("")
|
||||
print(" note windowing is macOS-only today; on this host a game builds headless")
|
||||
print(" (ludic build --headless) but cannot open a window.")
|
||||
}
|
||||
print("")
|
||||
if bad_count == 0 { print("everything checks out."); return 0 }
|
||||
print(`{string(bad_count)} problem(s) above. Reinstall with:`)
|
||||
print(` curl -fsSL {install_url()} | sh`)
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---- ludic upgrade ----------------------------------------------------------
|
||||
|
||||
function install_url() -> pointer { return getenv_or("LUDIC_INSTALL_URL", "https://workshopsoft.pages.workshopsoft.io/ludic/install.sh") }
|
||||
|
||||
# ludic upgrade [version] — re-run the installer, which replaces the install in
|
||||
# place. One code path for installing and updating means an upgrade can never
|
||||
# drift from a fresh install.
|
||||
function cmd_upgrade() -> int {
|
||||
var ver = ""
|
||||
if arg_total() >= 3 { ver = arg_n(2) }
|
||||
if not shq("command -v curl >/dev/null 2>&1") {
|
||||
err("ludic upgrade: needs curl\n")
|
||||
return 1
|
||||
}
|
||||
print(`upgrading from {install_url()}`)
|
||||
if ver == "" { return sh(`curl -fsSL {install_url()} | sh`) }
|
||||
return sh(`curl -fsSL {install_url()} | sh -s -- --version {ver}`)
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
# release.ludic — versioning + release cutting for the toolchain.
|
||||
#
|
||||
# x version print the toolchain version (from the VERSION file)
|
||||
# x release [level] cut a release: aggregate changes/ into CHANGELOG.md,
|
||||
# ludic version print the toolchain version (from the VERSION file)
|
||||
# ludic dev release [level] cut a release: aggregate changes/ into CHANGELOG.md,
|
||||
# bump VERSION, commit, and tag vX.Y.Z. `level` is
|
||||
# major|minor|patch; omitted, it is derived from the
|
||||
# highest `bump:` among the pending changesets.
|
||||
# x release [level] --dry-run render the changelog section to stdout and
|
||||
# ludic dev release [level] --dry-run render the changelog section to stdout and
|
||||
# stop: nothing is written, committed, tagged or pushed.
|
||||
# x release [level] --publish ...then push main + the tag and create a
|
||||
# ludic dev release [level] --publish ...then push main + the tag and create a
|
||||
# Forgejo release with source + toolchain tarballs.
|
||||
# Needs FORGEJO_TOKEN in the environment.
|
||||
#
|
||||
|
|
@ -49,7 +49,7 @@ function compute_next(cur: pointer, level: pointer) -> pointer {
|
|||
return `{maj}.{min}.{p2}`
|
||||
}
|
||||
|
||||
# x version — report the toolchain version. Prefer the compiler's own --version
|
||||
# ludic version — report the toolchain version. Prefer the compiler's own --version
|
||||
# (proving that path works); fall back to the file if ludicc is not built yet.
|
||||
function cmd_version() -> int {
|
||||
if is_exec("bin/ludicc") {
|
||||
|
|
@ -233,16 +233,16 @@ function build_section(ver: pointer) -> void {
|
|||
write_file(tmp_path("rel_section.md"), render_section(ver, date, "changes"))
|
||||
}
|
||||
|
||||
# x changelog-render <version> <date> <dir> — print the CHANGELOG section that
|
||||
# ludic dev changelog-render <version> <date> <dir> — print the CHANGELOG section that
|
||||
# `dir`'s changesets would produce. Used to re-render the sections of releases
|
||||
# cut before the renderer preserved markdown structure: check the changesets out
|
||||
# of the tag's parent commit, point this at them, and splice the result back in.
|
||||
function cmd_changelog_render() -> int {
|
||||
if arg_count() < 5 {
|
||||
err("usage: x changelog-render <version> <date> <changesets-dir>\n")
|
||||
if arg_total() < 5 {
|
||||
err("usage: ludic dev changelog-render <version> <date> <changesets-dir>\n")
|
||||
return 1
|
||||
}
|
||||
out(render_section(arg(2), arg(3), arg(4)))
|
||||
out(render_section(arg_n(2), arg_n(3), arg_n(4)))
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -250,7 +250,7 @@ function cmd_changelog_render() -> int {
|
|||
# release section (or at the end of the header if this is the first release).
|
||||
function prepend_changelog() -> void {
|
||||
if not file_exists("CHANGELOG.md") {
|
||||
run("printf '# Changelog\n\nAll notable changes to the Ludic toolchain, newest first. Generated from the\nchangesets under changes/ by x release; do not edit released sections by hand.\n\n' > CHANGELOG.md")
|
||||
run("printf '# Changelog\n\nAll notable changes to the Ludic toolchain, newest first. Generated from the\nchangesets under changes/ by ludic dev release; do not edit released sections by hand.\n\n' > CHANGELOG.md")
|
||||
}
|
||||
let ln = capture_line("grep -n '^## ' CHANGELOG.md | head -1 | cut -d: -f1")
|
||||
if (ln == "") {
|
||||
|
|
@ -289,11 +289,11 @@ function changelog_section(ver: pointer) -> pointer {
|
|||
return sslice(text, start, n)
|
||||
}
|
||||
|
||||
# x changelog-section <version> — print that release's CHANGELOG section.
|
||||
# ludic dev changelog-section <version> — print that release's CHANGELOG section.
|
||||
function cmd_changelog_section() -> int {
|
||||
if arg_count() < 3 { err("usage: x changelog-section <version>\n"); return 1 }
|
||||
let sec = changelog_section(arg(2))
|
||||
if slen(sec) == 0 { err(`changelog-section: no section for v{arg(2)} in CHANGELOG.md\n`); return 1 }
|
||||
if arg_total() < 3 { err("usage: ludic dev changelog-section <version>\n"); return 1 }
|
||||
let sec = changelog_section(arg_n(2))
|
||||
if slen(sec) == 0 { err(`changelog-section: no section for v{arg_n(2)} in CHANGELOG.md\n`); return 1 }
|
||||
out(sec)
|
||||
return 0
|
||||
}
|
||||
|
|
@ -305,8 +305,14 @@ function sha256_cmd() -> pointer {
|
|||
}
|
||||
|
||||
# Build the release artifacts into dist/: a reproducible source+seed tarball from
|
||||
# the tag, the toolchain built on this host, and one `.sha256` beside each — a
|
||||
# release without checksums asks everyone downstream to trust the transport.
|
||||
# the tag, a ready-to-run toolchain built on this host, and one `.sha256` beside
|
||||
# each — a release without checksums asks everyone downstream to trust the
|
||||
# transport.
|
||||
#
|
||||
# The toolchain tarball is a complete install root, not just binaries: bin/ next
|
||||
# to the engine runtime, the bundled ludic.* packages and VERSION, laid out
|
||||
# exactly as ludic_home() expects. install.sh unpacks it and is done; anything
|
||||
# missing here is a game that will not link on a machine with no checkout.
|
||||
#
|
||||
# The checksums are per-artifact rather than a single SHA256SUMS on purpose. A
|
||||
# release is assembled from more than one host (a Linux runner cannot build the
|
||||
|
|
@ -325,10 +331,24 @@ function build_artifacts(ver: pointer) -> bool {
|
|||
if is_exec("bin/ludicc") {
|
||||
let plat = capture_line("uname -s | tr '[:upper:]' '[:lower:]'")
|
||||
let arch = capture_line("uname -m")
|
||||
if not shq(`tar -czf dist/ludic-{ver}-{plat}-{arch}.tar.gz bin selfhost/ludicc.seed.ll VERSION`) {
|
||||
let name = `ludic-{ver}-{plat}-{arch}`
|
||||
let stage = `{tmp_dir()}/{name}`
|
||||
run(`rm -rf {stage} && mkdir -p {stage}/bin`)
|
||||
if not shq(`cp bin/ludic bin/ludicc bin/ludic-fmt bin/ludic-lsp {stage}/bin/`) {
|
||||
err("release: the toolchain is not built (run: ludic dev build)\n")
|
||||
return false
|
||||
}
|
||||
run(`cp -R runtime {stage}/runtime`)
|
||||
run(`cp -R packages {stage}/packages`)
|
||||
run(`cp VERSION LICENSE README.md {stage}/`)
|
||||
# the Linux link needs the Darwin-stdio shim injected through $LUDIC_CC; it
|
||||
# travels with the toolchain that needs it (see tools/ci/linux_stdio_shim.ll).
|
||||
if plat == "linux" { run(`mkdir -p {stage}/lib && cp tools/ci/linux_stdio_shim.ll {stage}/lib/`) }
|
||||
if not shq(`tar -czf dist/{name}.tar.gz -C {tmp_dir()} {name}`) {
|
||||
err("release: building the toolchain tarball failed\n")
|
||||
return false
|
||||
}
|
||||
run(`rm -rf {stage}`)
|
||||
}
|
||||
if not shq(`cd dist && for f in *.tar.gz; do {sha256_cmd()} "$f" > "$f.sha256" || exit 1; done`) {
|
||||
err("release: writing the per-artifact .sha256 files failed\n")
|
||||
|
|
@ -338,10 +358,10 @@ function build_artifacts(ver: pointer) -> bool {
|
|||
return true
|
||||
}
|
||||
|
||||
# x publish [vX.Y.Z] — publish an already-tagged release: build the artifacts,
|
||||
# ludic dev publish [vX.Y.Z] — publish an already-tagged release: build the artifacts,
|
||||
# take the notes from that version's CHANGELOG section, and create the Forgejo
|
||||
# release. Defaults to the version in VERSION. This is what CI runs on a tag
|
||||
# push, and what `x release --publish` calls once it has tagged.
|
||||
# push, and what `ludic dev release --publish` calls once it has tagged.
|
||||
function publish_tag(ver: pointer) -> int {
|
||||
if getenv_or("FORGEJO_TOKEN", "") == "" { err("publish: set FORGEJO_TOKEN (a Forgejo access token)\n"); return 1 }
|
||||
let body = tmp_path("rel_notes.md")
|
||||
|
|
@ -358,8 +378,8 @@ function publish_tag(ver: pointer) -> int {
|
|||
|
||||
function cmd_publish() -> int {
|
||||
var ver = read_version_or("")
|
||||
if arg_count() >= 3 {
|
||||
var a = arg(2)
|
||||
if arg_total() >= 3 {
|
||||
var a = arg_n(2)
|
||||
if s_starts(a, "v") { a = sslice(a, 1, slen(a)) }
|
||||
ver = a
|
||||
}
|
||||
|
|
@ -381,8 +401,8 @@ function cmd_release() -> int {
|
|||
var publish = false
|
||||
var dry = false
|
||||
var ai = 2
|
||||
while ai < arg_count() {
|
||||
let a = arg(ai)
|
||||
while ai < arg_total() {
|
||||
let a = arg_n(ai)
|
||||
if (a == "--publish") { publish = true }
|
||||
else { if (a == "--dry-run") { dry = true }
|
||||
else { if (a == "major") or (a == "minor") or (a == "patch") { level = a }
|
||||
|
|
@ -409,7 +429,7 @@ function cmd_release() -> int {
|
|||
let sec = read_file(tmp_path("rel_section.md"))
|
||||
if sec != null { out(sec) }
|
||||
print("")
|
||||
print(` dry run — nothing written. cut it with: x release {level}`)
|
||||
print(` dry run — nothing written. cut it with: ludic dev release {level}`)
|
||||
return 0
|
||||
}
|
||||
prepend_changelog()
|
||||
|
|
@ -424,7 +444,7 @@ function cmd_release() -> int {
|
|||
print(` committed + tagged v{ver}`)
|
||||
|
||||
if publish { return publish_release(ver) }
|
||||
print(` local release ready. publish with: FORGEJO_TOKEN=… x release {level} --publish`)
|
||||
print(` local release ready. publish with: FORGEJO_TOKEN=… ludic dev release {level} --publish`)
|
||||
print(` (or push: git push origin HEAD && git push origin v{ver})`)
|
||||
return 0
|
||||
}
|
||||
|
|
@ -98,17 +98,18 @@ function line_count(path: pointer) -> pointer {
|
|||
# ensure bin/ludicc exists and is current with the seed. Built from the checked-in
|
||||
# IR seed with clang alone — no C compiler is ever involved.
|
||||
function ensure_ludicc() -> void {
|
||||
if not in_toolchain_repo() { return }
|
||||
run("mkdir -p bin build")
|
||||
if (not is_exec("bin/ludicc")) or newer("selfhost/ludicc.seed.ll", "bin/ludicc") {
|
||||
print("cc: selfhost/ludicc.seed.ll -> bin/ludicc (from the IR seed, no C compiler)")
|
||||
if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) {
|
||||
err("x: could not assemble the seed\n"); exit(1)
|
||||
err("ludic: could not assemble the seed\n"); exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---- selfhost-build: assemble + compile the self-host compiler ---------------
|
||||
# usage: x selfhost-build [ludicc] [outbin] (defaults: bin/ludicc, build/selfhost)
|
||||
# usage: ludic dev selfhost-build [ludicc] [outbin] (defaults: bin/ludicc, build/selfhost)
|
||||
function cmd_selfhost_build(lc: pointer, outbin: pointer) -> int {
|
||||
run("mkdir -p build")
|
||||
let src = "build/selfhost.ludic"
|
||||
|
|
@ -122,7 +123,7 @@ function cmd_selfhost_build(lc: pointer, outbin: pointer) -> int {
|
|||
}
|
||||
|
||||
# ---- sh-compile: compile one .ludic with a given selfhost binary and link ----
|
||||
# usage: x sh-compile <selfhost-binary> <input.ludic> <output-binary>
|
||||
# usage: ludic dev sh-compile <selfhost-binary> <input.ludic> <output-binary>
|
||||
function cmd_sh_compile(shbin: pointer, in: pointer, outbin: pointer) -> int {
|
||||
if not shq(`{shbin} {in} > {outbin}.ll`) { return 1 }
|
||||
if not shq(`{cc()} {outbin}.ll -o {outbin}`) { return 1 }
|
||||
|
|
@ -143,7 +144,7 @@ function game_build_ok(shbin: pointer, game: pointer, outbin: pointer) -> bool {
|
|||
return true
|
||||
}
|
||||
|
||||
# usage: x game-build <selfhost-binary> <game.ludic> <out-binary>
|
||||
# usage: ludic dev game-build <selfhost-binary> <game.ludic> <out-binary>
|
||||
function cmd_game_build(shbin: pointer, game: pointer, outbin: pointer) -> int {
|
||||
if game_build_ok(shbin, game, outbin) { print(`built {outbin}`); return 0 }
|
||||
print("build failed:")
|
||||
|
|
@ -193,7 +194,7 @@ function cmd_bootstrap_cfree() -> int {
|
|||
print(" out.ll == seed.ll — the compiler rebuilds itself with no C compiler")
|
||||
return 0
|
||||
}
|
||||
print(" out.ll != seed.ll (seed is stale — regenerate with: x reseed)")
|
||||
print(" out.ll != seed.ll (seed is stale — regenerate with: ludic dev reseed)")
|
||||
out(capture("diff build/cfree/out.ll selfhost/ludicc.seed.ll | head"))
|
||||
return 1
|
||||
}
|
||||
|
|
@ -202,7 +203,7 @@ function cmd_bootstrap_cfree() -> int {
|
|||
# Uses the CURRENT seed to build the new one, compiling twice so the seed is a
|
||||
# fixed point of the NEW compiler, not a one-step image of the old. Falls back to
|
||||
# bin/ludicc only if the seed itself no longer assembles. Verify with
|
||||
# `x bootstrap-cfree` afterwards.
|
||||
# `ludic dev bootstrap-cfree` afterwards.
|
||||
function cmd_reseed() -> int {
|
||||
run("mkdir -p build/cfree")
|
||||
if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 }
|
||||
|
|
@ -19,7 +19,7 @@ function sh_case(name: pointer, exp: pointer) -> void {
|
|||
# framebuffer's SHA-256 against the text baseline in selfhost/golden/renders.sha256.
|
||||
# A hash is tiny and text-diffable — a regression shows as a changed hex line in a
|
||||
# review, not an opaque "binary files differ". Regenerate the baseline with
|
||||
# `bin/x golden` (see cmd_golden below).
|
||||
# `bin/ludic dev golden` (see cmd_golden below).
|
||||
function game_case(path: pointer, keys: pointer) -> void {
|
||||
let nm = flat(path)
|
||||
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `{tmp_dir()}/g_{nm}`) { bad(`{path}: build`); return }
|
||||
|
|
@ -32,13 +32,13 @@ function game_case(path: pointer, keys: pointer) -> void {
|
|||
if not is_darwin() { skip(`{path} render (golden hash blessed on macOS)`); return }
|
||||
let got = capture_line("shasum -a 256 build/out.ppm | cut -d' ' -f1")
|
||||
let want = capture_line(`grep '^{path} ' selfhost/golden/renders.sha256 | cut -d' ' -f2`)
|
||||
if (want == "") { bad2(path, "no golden hash (run: bin/x golden)"); return }
|
||||
if (want == "") { bad2(path, "no golden hash (run: bin/ludic dev golden)"); return }
|
||||
if (got == want) { ok(`{path} matches the golden render`) }
|
||||
else { bad2(path, `render hash {got} != golden {want}`) }
|
||||
}
|
||||
|
||||
# render one golden game and return its "<path> <sha256>\n" manifest line (empty
|
||||
# string on build failure). Shared by `bin/x golden` and nothing else.
|
||||
# string on build failure). Shared by `bin/ludic dev golden` and nothing else.
|
||||
function bless_render(path: pointer, keys: pointer) -> pointer {
|
||||
let nm = flat(path)
|
||||
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `{tmp_dir()}/bg_{nm}`) {
|
||||
|
|
@ -117,12 +117,12 @@ function cmd_selfhost_test() -> int {
|
|||
game_case("rendering/draw_prims", "")
|
||||
|
||||
print("== the bootstrap fixpoint (seeded from bin/ludicc) ==")
|
||||
if shq(`bin/x bootstrap > {tmp_dir()}/boot.out 2>&1`) { ok("gen2.ll == gen3.ll (compiler reproduces itself)") }
|
||||
if shq(`bin/ludic dev bootstrap > {tmp_dir()}/boot.out 2>&1`) { ok("gen2.ll == gen3.ll (compiler reproduces itself)") }
|
||||
else { bad("fixpoint not reached"); out(capture(`cat {tmp_dir()}/boot.out`)) }
|
||||
|
||||
print("== the C-free bootstrap (from the checked-in IR seed, no C compiler) ==")
|
||||
if shq(`bin/x bootstrap-cfree > {tmp_dir()}/cfree.out 2>&1`) { ok("seed.ll rebuilds the compiler and is its own fixed point") }
|
||||
else { bad("C-free bootstrap failed (seed stale? run: x reseed)"); out(capture(`cat {tmp_dir()}/cfree.out`)) }
|
||||
if shq(`bin/ludic dev bootstrap-cfree > {tmp_dir()}/cfree.out 2>&1`) { ok("seed.ll rebuilds the compiler and is its own fixed point") }
|
||||
else { bad("C-free bootstrap failed (seed stale? run: ludic dev reseed)"); out(capture(`cat {tmp_dir()}/cfree.out`)) }
|
||||
|
||||
return report()
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
# builds from the seed, compiles every example to its byte-exact golden render,
|
||||
# self-hosts to a fixpoint, and rebuilds itself with no C compiler.
|
||||
#
|
||||
# It drives the other commands through `bin/x <sub>` exactly where test.sh used
|
||||
# It drives the other commands through `bin/ludic <sub>` exactly where test.sh used
|
||||
# to spawn a sibling script, so each stage stays an independent, observable run.
|
||||
|
||||
# compile a feature example as a game, run it (optional keys on stdin) and match
|
||||
|
|
@ -23,7 +23,7 @@ function feat_case(path: pointer, keys: pointer, exp: pointer, label: pointer) -
|
|||
# a feature example that consumes an in-repo controller package (packages/): same
|
||||
# as feat_case, but the compiler is pointed at packages/ via LUDIC_MODULES so a
|
||||
# `import "ludic.foo/bar.ludic"` resolves, and LUDIC_HOME anchors the engine
|
||||
# runtime. This is exactly how `x app` / an external project builds against a
|
||||
# runtime. This is exactly how `ludic build` / an external project builds against a
|
||||
# fetched source package (the do_import module-root fallback, #63).
|
||||
function controller_case(path: pointer, keys: pointer, exp: pointer, label: pointer) -> void {
|
||||
let nm = flat(path)
|
||||
|
|
@ -59,6 +59,38 @@ function runtime_home_case() -> void {
|
|||
if (got == "7") { ok(lbl) } else { bad2(lbl, `got [{got}] want [7]`) }
|
||||
}
|
||||
|
||||
# The install layout: bin/ beside runtime/ and packages/, reached through $PATH
|
||||
# from an unrelated directory, with nothing in the environment. This is what
|
||||
# `curl https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh` lays down and the only shape most
|
||||
# users will ever run, so it is asserted rather than assumed: a staged copy of
|
||||
# the toolchain compiles and runs a project created by `ludic new`.
|
||||
function install_layout_case() -> void {
|
||||
let root = capture_line("pwd")
|
||||
let home = `{tmp_dir()}/install`
|
||||
let lbl = "an installed layout (bin/ + runtime/ + packages/, found on PATH) builds a new project"
|
||||
run(`rm -rf {home} && mkdir -p {home}/bin`)
|
||||
run(`cp bin/ludic bin/ludicc bin/ludic-fmt {home}/bin/`)
|
||||
run(`cp -R runtime {home}/runtime && cp -R packages {home}/packages && cp VERSION {home}/VERSION`)
|
||||
let work = `{tmp_dir()}/installwork`
|
||||
run(`rm -rf {work} && mkdir -p {work}`)
|
||||
# PATH only — no LUDIC_HOME, and the current directory has no bin/ of its own.
|
||||
let envp = `env -u LUDIC_HOME PATH={home}/bin:/usr/bin:/bin:/usr/sbin:/sbin`
|
||||
if not shq(`cd {work} && {envp} ludic new demo > {work}/new.out 2>&1`) {
|
||||
bad2(lbl, capture_line(`tail -1 {work}/new.out`)); return
|
||||
}
|
||||
if not shq(`cd {work}/demo && {envp} ludic build --headless > {work}/build.out 2>&1`) {
|
||||
bad2(lbl, capture_line(`tail -1 {work}/build.out`)); return
|
||||
}
|
||||
if not shq(`cd {work}/demo && {envp} ludic test > {work}/test.out 2>&1`) {
|
||||
bad2(lbl, capture_line(`tail -1 {work}/test.out`)); return
|
||||
}
|
||||
let ver = capture_line(`{envp} ludic version`)
|
||||
if ver != `ludic {s_trim(read_file("VERSION"))}` {
|
||||
bad2("an installed `ludic version` reports the install's VERSION", `got [{ver}]`); return
|
||||
}
|
||||
ok(lbl)
|
||||
}
|
||||
|
||||
# a "does it still compile" smoke test (parse -> lower -> link), no run
|
||||
function qsmoke(path: pointer) -> void {
|
||||
let nm = flat(path)
|
||||
|
|
@ -126,7 +158,7 @@ function panic_case() -> void {
|
|||
else { bad2("panic", `rc={string(rc)} err=[{msg}]`) }
|
||||
}
|
||||
|
||||
# issue #45: `bin/x test --coverage`. Compile each test-spec with `--coverage`,
|
||||
# issue #45: `bin/ludic dev test --coverage`. Compile each test-spec with `--coverage`,
|
||||
# run it with LUDIC_COVERAGE pointed at a per-file dump, then aggregate the dumps
|
||||
# into a clean per-file line-coverage report. The instrumentation is flag-gated,
|
||||
# so this reuses the same seed-built bin/ludicc the rest of the suite does.
|
||||
|
|
@ -176,7 +208,7 @@ function cmd_test_coverage() -> int {
|
|||
}
|
||||
COV_COVERED = 0
|
||||
COV_TOTAL = 0
|
||||
print("== line coverage (bin/x test --coverage) ==")
|
||||
print("== line coverage (bin/ludic dev test --coverage) ==")
|
||||
cov_one("library/coverage", `{tmp_dir()}/cov_out`)
|
||||
cov_one("library/testing", `{tmp_dir()}/cov_out`)
|
||||
var pct = 100
|
||||
|
|
@ -186,7 +218,7 @@ function cmd_test_coverage() -> int {
|
|||
return 0
|
||||
}
|
||||
|
||||
function cmd_test() -> int {
|
||||
function cmd_dev_test() -> int {
|
||||
PASS = 0
|
||||
FAIL = 0
|
||||
|
||||
|
|
@ -199,7 +231,7 @@ function cmd_test() -> int {
|
|||
|
||||
print("== the compiler builds from the IR seed (no C compiler) ==")
|
||||
run("rm -f bin/ludicc")
|
||||
if shq("bin/x app examples/games/snake.ludic --headless >/dev/null 2>&1") and is_exec("bin/ludicc") {
|
||||
if shq("bin/ludic build examples/games/snake.ludic --headless >/dev/null 2>&1") and is_exec("bin/ludicc") {
|
||||
ok("clang selfhost/ludicc.seed.ll -> bin/ludicc")
|
||||
} else { bad("compiler did not build from the seed") }
|
||||
|
||||
|
|
@ -209,11 +241,11 @@ function cmd_test() -> int {
|
|||
game_case("games/chronorift", "ddddwwwwaassK")
|
||||
|
||||
print("== self-hosting and the bootstrap fixpoints ==")
|
||||
run(`bin/x selfhost-test > {tmp_dir()}/sh.out 2>&1`)
|
||||
run(`bin/ludic dev selfhost-test > {tmp_dir()}/sh.out 2>&1`)
|
||||
if shq(`grep -q 'gen2.ll == gen3.ll' {tmp_dir()}/sh.out`) { ok("the compiler reproduces itself (fixpoint)") } else { bad("fixpoint") }
|
||||
if shq(`grep -q 'no C compiler' {tmp_dir()}/sh.out`) { ok("the compiler rebuilds itself from the seed, C-free") } else { bad("C-free bootstrap") }
|
||||
let corr = capture_line(`grep -c PASS {tmp_dir()}/sh.out`)
|
||||
ok(`self-host suite: {corr} checks passed (see: x selfhost-test)`)
|
||||
ok(`self-host suite: {corr} checks passed (see: ludic dev selfhost-test)`)
|
||||
|
||||
print("== documented syntax stays compilable (guards against spec/compiler drift) ==")
|
||||
qsmoke("lang/qdecl")
|
||||
|
|
@ -367,12 +399,12 @@ function cmd_test() -> int {
|
|||
smoke("networking/net_rt")
|
||||
smoke("library/cursor_capture") # #89 Input.cursor_mode compiles (no-op headless; windowed links cocoa.ll)
|
||||
|
||||
print("== self-hosted front-end binaries (ludicc / ludic) ==")
|
||||
# the two commands are one multi-call native binary built from the seed with
|
||||
# clang alone. ludicc compiles; ludic compiles-and-runs.
|
||||
if shq(`bin/x build-cli > {tmp_dir()}/cli.out 2>&1`) and is_exec("bin/ludicc") and is_exec("bin/ludic") {
|
||||
ok("x build-cli builds bin/ludicc and bin/ludic from the seed (clang-only)")
|
||||
} else { bad2("x build-cli", capture_line(`tail -1 {tmp_dir()}/cli.out`)) }
|
||||
print("== the compiler and the CLI (ludicc / ludic) ==")
|
||||
# ludicc comes out of the IR seed with clang alone; the CLI is then compiled
|
||||
# by it, from Ludic.
|
||||
if shq(`bin/ludic dev build-cli > {tmp_dir()}/cli.out 2>&1`) and is_exec("bin/ludicc") {
|
||||
ok("ludic dev build-cli builds bin/ludicc from the seed (clang-only)")
|
||||
} else { bad2("ludic dev build-cli", capture_line(`tail -1 {tmp_dir()}/cli.out`)) }
|
||||
|
||||
# ludicc -o drives clang to a native binary. A game links windowed by default
|
||||
# (LUDIC_HOME points the linker at runtime/native/cocoa.ll) — that path is
|
||||
|
|
@ -398,37 +430,50 @@ function cmd_test() -> int {
|
|||
ok("ludicc --emit-llvm -> LLVM IR")
|
||||
} else { bad2("ludicc --emit-llvm", capture_line(`tail -1 {tmp_dir()}/cli.out`)) }
|
||||
|
||||
# ludic compiles, runs, and forwards the program's exit code.
|
||||
# `ludic <file>` compiles, runs, and forwards the program's exit code.
|
||||
write_file(`{tmp_dir()}/cli_exit.ludic`, "entry { exit(42) }\n")
|
||||
let rc = sh(`LUDIC_HOME=. bin/ludic {tmp_dir()}/cli_exit.ludic > {tmp_dir()}/cli.out 2>&1`)
|
||||
let rc = sh(`bin/ludic {tmp_dir()}/cli_exit.ludic --headless > {tmp_dir()}/cli.out 2>&1`)
|
||||
if (rc == 42) { ok("ludic app.ludic -> compiles, runs, forwards exit code") }
|
||||
else { bad2("ludic run: expected exit 42", `got {string(rc)}`) }
|
||||
|
||||
# The CLI has to work as an install does: from another directory, with no
|
||||
# ./bin, finding its compiler, the engine runtime and the bundled packages
|
||||
# from its own location. This is the shape `curl … | sh` produces.
|
||||
install_layout_case()
|
||||
|
||||
# install.sh is what the landing page tells people to pipe into sh, and it is
|
||||
# published with the docs site — so it is checked here rather than discovered
|
||||
# broken by a first-time user. `sh -n` parses it without running anything.
|
||||
if not file_exists("install.sh") { bad("install.sh is missing") }
|
||||
else if shq("sh -n install.sh 2>/dev/null") and shq("sh install.sh --help >/dev/null 2>&1") {
|
||||
ok("install.sh parses under POSIX sh and answers --help")
|
||||
} else { bad("install.sh does not parse under POSIX sh") }
|
||||
|
||||
# the package manager (issue #63): fetch + MVS resolve + content-addressed
|
||||
# store + ludic_modules/ links + build, run as its own hermetic suite (it
|
||||
# stands up throwaway git repos, so like selfhost-test it runs as a sub-process
|
||||
# and this stage checks its footer).
|
||||
print("== package manager (x test-pkg) ==")
|
||||
run(`bin/x test-pkg > {tmp_dir()}/pkg.out 2>&1`)
|
||||
print("== package manager (ludic dev test-pkg) ==")
|
||||
run(`bin/ludic dev test-pkg > {tmp_dir()}/pkg.out 2>&1`)
|
||||
if shq(`grep -q '0 failed' {tmp_dir()}/pkg.out`) {
|
||||
let pc = capture_line(`grep -c PASS {tmp_dir()}/pkg.out`)
|
||||
ok(`package-manager suite: {pc} checks passed (see: x test-pkg)`)
|
||||
} else { bad2("x test-pkg", capture_line(`grep -i fail {tmp_dir()}/pkg.out | head -1`)) }
|
||||
ok(`package-manager suite: {pc} checks passed (see: ludic dev test-pkg)`)
|
||||
} else { bad2("ludic dev test-pkg", capture_line(`grep -i fail {tmp_dir()}/pkg.out | head -1`)) }
|
||||
|
||||
# docs site generator + guard, in Ludic (no Python). docs-gen emits the whole
|
||||
# pages payload; docs-check is its coverage/integrity guard; docs-palette is
|
||||
# the named-colour source of truth (its --check mode regenerates the tracked
|
||||
# emit_color.ludic + palette.json into scratch files and compares).
|
||||
print("== docs site (Ludic generator, no Python) ==")
|
||||
if shq(`bin/x docs-gen --out {tmp_dir()}/docs > {tmp_dir()}/docs.out 2>&1`) {
|
||||
ok("x docs-gen -> the pages-branch payload")
|
||||
} else { bad2("x docs-gen", capture_line(`tail -1 {tmp_dir()}/docs.out`)) }
|
||||
if shq(`bin/x docs-check {tmp_dir()}/docs > {tmp_dir()}/docs_chk.out 2>&1`) {
|
||||
ok("x docs-check passes on the generated site")
|
||||
} else { bad2("x docs-check", capture_line(`tail -1 {tmp_dir()}/docs_chk.out`)) }
|
||||
if shq(`bin/x docs-palette --check > {tmp_dir()}/pal.out 2>&1`) {
|
||||
ok("x docs-palette regenerates emit_color.ludic + palette.json byte-identically")
|
||||
} else { bad2("x docs-palette --check", capture_line(`tail -1 {tmp_dir()}/pal.out`)) }
|
||||
if shq(`bin/ludic dev docs-gen --out {tmp_dir()}/docs > {tmp_dir()}/docs.out 2>&1`) {
|
||||
ok("ludic dev docs-gen -> the pages-branch payload")
|
||||
} else { bad2("ludic dev docs-gen", capture_line(`tail -1 {tmp_dir()}/docs.out`)) }
|
||||
if shq(`bin/ludic dev docs-check {tmp_dir()}/docs > {tmp_dir()}/docs_chk.out 2>&1`) {
|
||||
ok("ludic dev docs-check passes on the generated site")
|
||||
} else { bad2("ludic dev docs-check", capture_line(`tail -1 {tmp_dir()}/docs_chk.out`)) }
|
||||
if shq(`bin/ludic dev docs-palette --check > {tmp_dir()}/pal.out 2>&1`) {
|
||||
ok("ludic dev docs-palette regenerates emit_color.ludic + palette.json byte-identically")
|
||||
} else { bad2("ludic dev docs-palette --check", capture_line(`tail -1 {tmp_dir()}/pal.out`)) }
|
||||
|
||||
return report()
|
||||
}
|
||||
|
|
@ -27,12 +27,12 @@ function sync_vscode_grammar() -> void {
|
|||
# is `flag` present anywhere in argv?
|
||||
function has_flag(flag: pointer) -> bool {
|
||||
var i = 2
|
||||
while i < arg_count() { if (arg(i) == flag) { return true }; i += 1 }
|
||||
while i < arg_total() { if (arg_n(i) == flag) { return true }; i += 1 }
|
||||
return false
|
||||
}
|
||||
|
||||
# ---- tools: build the editor toolchain --------------------------------------
|
||||
# usage: x tools [--install] [--test]
|
||||
# usage: ludic dev tools [--install] [--test]
|
||||
function cmd_tools() -> int {
|
||||
ensure_ludicc()
|
||||
print("ludicc: tools/ludic-tools/fmt.ludic -> bin/ludic-fmt (Ludic -> LLVM IR -> binary, no C)")
|
||||
|
|
@ -57,8 +57,8 @@ function cmd_test_tools() -> int {
|
|||
PASS = 0
|
||||
FAIL = 0
|
||||
let fmt = "bin/ludic-fmt"
|
||||
if not is_exec(fmt) { err("bin/ludic-fmt missing — run: x tools\n"); return 2 }
|
||||
if not is_exec("bin/ludic-lsp") { err("bin/ludic-lsp missing — run: x tools\n"); return 2 }
|
||||
if not is_exec(fmt) { err("bin/ludic-fmt missing — run: ludic dev tools\n"); return 2 }
|
||||
if not is_exec("bin/ludic-lsp") { err("bin/ludic-lsp missing — run: ludic dev tools\n"); return 2 }
|
||||
|
||||
print("formatter")
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ function cmd_test_tools() -> int {
|
|||
# features (go-to-def, find-usages, workspace symbols) can't resolve. Gate the
|
||||
# end-to-end protocol test on Darwin until the directory walk is portable.
|
||||
if is_darwin() {
|
||||
let lrc = sh("bin/x test-lsp")
|
||||
let lrc = sh("bin/ludic dev test-lsp")
|
||||
if lrc == 0 { ok("language server protocol") }
|
||||
else if lrc == 2 { skip("language server protocol (bin/ludic-lsp not built)") }
|
||||
else { bad("language server protocol") }
|
||||
|
|
@ -160,10 +160,10 @@ function cmd_test_tools() -> int {
|
|||
# VS Code carries its own copy of the shared grammar; catch an edit to the copy
|
||||
if shq("cmp -s tools/editors/shared/ludic.tmLanguage.json tools/editors/vscode/syntaxes/ludic.tmLanguage.json") and shq("cmp -s tools/editors/shared/ludic.markdown-injection.json tools/editors/vscode/syntaxes/ludic.markdown-injection.json") and shq("cmp -s tools/editors/shared/language-configuration.json tools/editors/vscode/language-configuration.json") {
|
||||
ok("vscode grammar copies match tools/editors/shared")
|
||||
} else { bad2("vscode grammar copies have drifted", "run: x tools to resync") }
|
||||
} else { bad2("vscode grammar copies have drifted", "run: ludic dev tools to resync") }
|
||||
|
||||
# the JetBrains plugin is off by default (its first build pulls a gigabyte of
|
||||
# IntelliJ SDK). One env var away: LUDIC_TEST_JETBRAINS=1 x test-tools
|
||||
# IntelliJ SDK). One env var away: LUDIC_TEST_JETBRAINS=1 ludic dev test-tools
|
||||
if (getenv_or("LUDIC_TEST_JETBRAINS", "0") == "1") {
|
||||
if shq("test -x tools/editors/jetbrains/gradlew") {
|
||||
if shq("cd tools/editors/jetbrains && ./gradlew buildPlugin verifyPluginStructure --console=plain -q >/dev/null 2>&1 && test -f build/distributions/Ludic-1.0.0.zip") { ok("jetbrains plugin builds and verifies") } else { bad("jetbrains plugin build") }
|
||||
|
|
@ -175,19 +175,19 @@ function cmd_test_tools() -> int {
|
|||
|
||||
# the vocabulary lives in one place; drift between it and its copies (the
|
||||
# TextMate grammar, the Kotlin lexer) is the failure mode this layout prevents.
|
||||
# The check is written in Ludic (tools/x/checks.ludic) and runs through x — no
|
||||
# The check is written in Ludic (tools/ludic-cli/checks.ludic) and runs through x — no
|
||||
# Python in the loop.
|
||||
if shq("bin/x check-vocabulary 2>/dev/null") { ok("vocabulary in sync across grammar/lexer/header") } else { bad("vocabulary drifted") }
|
||||
if shq("bin/ludic dev check-vocabulary 2>/dev/null") { ok("vocabulary in sync across grammar/lexer/header") } else { bad("vocabulary drifted") }
|
||||
|
||||
# every feature the compiler actually implements — namespace methods, keywords,
|
||||
# types, phases — must have a docs/language page. This reads the implementation
|
||||
# (emit_ns_call + ludic_syntax.h), so shipping a feature without docs fails here.
|
||||
# Ported to Ludic; runs through x.
|
||||
if shq("bin/x check-impl > /dev/null 2>&1") { ok("docs cover every implemented feature") } else { bad("docs drifted from the implementation") }
|
||||
if shq("bin/ludic dev check-impl > /dev/null 2>&1") { ok("docs cover every implemented feature") } else { bad("docs drifted from the implementation") }
|
||||
|
||||
# every ```ludic fence in the docs must parse (or be marked skip/expect-error),
|
||||
# so a documented snippet can't drift from what the compiler accepts.
|
||||
if shq("bin/x check-docs > /dev/null 2>&1") { ok("every doc fence parses") } else { bad2("a doc fence drifted", "run: x check-docs") }
|
||||
if shq("bin/ludic dev check-docs > /dev/null 2>&1") { ok("every doc fence parses") } else { bad2("a doc fence drifted", "run: ludic dev check-docs") }
|
||||
|
||||
return report()
|
||||
}
|
||||
|
|
@ -195,9 +195,9 @@ function cmd_test_tools() -> int {
|
|||
# JSON/XML validity, checked by the Ludic validators in checks.ludic (no Python).
|
||||
function test_json(path: pointer) -> void {
|
||||
let bn = capture_line(`basename {path}`)
|
||||
if shq(`bin/x lint-asset {path}`) { ok(`valid JSON: {bn}`) } else { bad(`invalid JSON: {path}`) }
|
||||
if shq(`bin/ludic dev lint-asset {path}`) { ok(`valid JSON: {bn}`) } else { bad(`invalid JSON: {path}`) }
|
||||
}
|
||||
function test_xml(path: pointer) -> void {
|
||||
let bn = capture_line(`basename {path}`)
|
||||
if shq(`bin/x lint-asset {path}`) { ok(`valid XML: {bn}`) } else { bad(`invalid XML: {path}`) }
|
||||
if shq(`bin/ludic dev lint-asset {path}`) { ok(`valid XML: {bn}`) } else { bad(`invalid XML: {path}`) }
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
# build.ludic — building the toolchain binaries and user programs.
|
||||
#
|
||||
# Replaces build-cli.sh (the ludicc/ludic front-end) and build.sh (compiling a
|
||||
# Ludic program to a native app). All toolchain binaries land in bin/.
|
||||
|
||||
# compile a Ludic source to a native binary in bin/ via ludicc + clang (-O2).
|
||||
# Returns true on success. Used for x itself and the editor tools.
|
||||
function build_tool(name: pointer, src: pointer) -> bool {
|
||||
let ll = `build/{name}.ll`
|
||||
if not shq(`bin/ludicc {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false }
|
||||
# write to a temp then move, so a running bin/x can rebuild itself in place
|
||||
if not shq(`{cc()} -O2 {ll} -o bin/{name}.tmp`) { print(`build failed: {name} (link)`); return false }
|
||||
run(`mv -f bin/{name}.tmp bin/{name} && rm -f {ll}`)
|
||||
return true
|
||||
}
|
||||
|
||||
# ---- build-cli: the self-hosted front-end binaries ---------------------------
|
||||
# Both ludicc and ludic are the SAME multi-call binary assembled from the IR seed
|
||||
# with clang alone; invoked as `ludic` it compiles-and-runs, as `ludicc` it just
|
||||
# compiles.
|
||||
function cmd_build_cli() -> int {
|
||||
run("mkdir -p bin build")
|
||||
print("cc: selfhost/ludicc.seed.ll -> bin/ludicc, bin/ludic (from the IR seed, no C compiler)")
|
||||
if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) { err("x: seed did not assemble\n"); return 1 }
|
||||
run("cp bin/ludicc bin/ludic && chmod +x bin/ludicc bin/ludic")
|
||||
print("done. built bin/ludicc and bin/ludic")
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---- build: the whole toolchain (ludicc, ludic, x, ludic-fmt, ludic-lsp) -----
|
||||
function cmd_build() -> int {
|
||||
if cmd_build_cli() != 0 { return 1 }
|
||||
print("ludicc: tools/x/main.ludic -> bin/x (the task runner rebuilds itself)")
|
||||
if not build_tool("x", "tools/x/main.ludic") { return 1 }
|
||||
print("ludicc: tools/ludic-tools/fmt.ludic -> bin/ludic-fmt")
|
||||
if not build_tool("ludic-fmt", "tools/ludic-tools/fmt.ludic") { return 1 }
|
||||
print("ludicc: tools/ludic-tools/lsp.ludic -> bin/ludic-lsp")
|
||||
if not build_tool("ludic-lsp", "tools/ludic-tools/lsp.ludic") { return 1 }
|
||||
sync_vscode_grammar()
|
||||
print("done. toolchain in bin/ (ludicc, ludic, x, ludic-fmt, ludic-lsp)")
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---- app: compile a Ludic program to a native app ----------------------------
|
||||
# usage: x app <file.ludic> [--headless|--windowed] [--save-temps]
|
||||
# windowed is the default for a game; headless renders the last frame to
|
||||
# build/out.ppm (never the repo root).
|
||||
function cmd_app() -> int {
|
||||
ensure_ludicc()
|
||||
var src = null
|
||||
var mode = 1 # 1 = windowed, 2 = headless
|
||||
var save = false
|
||||
var ai = 2
|
||||
while ai < arg_count() {
|
||||
let a = arg(ai)
|
||||
if (a == "--headless") { mode = 2 }
|
||||
else if (a == "--windowed") { mode = 1 }
|
||||
else if (a == "--save-temps") { save = true }
|
||||
else if (a[0] != '-') { src = a } # 45 = '-', so skip other flags
|
||||
ai += 1
|
||||
}
|
||||
if (src == null) { err("usage: x app <file.ludic> [--headless] [--save-temps]\n"); return 1 }
|
||||
let name = capture_line(`basename {src} .ludic`)
|
||||
|
||||
let pbf = prebuilt_link_flags() # #64: link resolved prebuilt module dylibs, if any
|
||||
|
||||
if (mode == 2) {
|
||||
let outbin = `build/{name}_headless`
|
||||
let ll = `{outbin}.ll`
|
||||
print(`ludicc: {src} -> {outbin} (renders the last frame to build/out.ppm)`)
|
||||
if not shq(`bin/ludicc --headless {src} > {ll}`) { return 1 }
|
||||
if not shq(`{cc()} -O2 {ll}{pbf} -o {outbin}`) { return 1 }
|
||||
if not save { run(`rm -f {ll}`) }
|
||||
print(`done. run: mkdir -p build && printf 'ddss' | ./{outbin} && open build/out.ppm`)
|
||||
return 0
|
||||
}
|
||||
|
||||
let outbin = `build/{name}`
|
||||
let ll = `{outbin}.ll`
|
||||
print(`ludicc: {src} -> {outbin} (Ludic-written compiler -> LLVM IR -> windowed binary)`)
|
||||
if not shq(`bin/ludicc --windowed {src} > {ll}`) { return 1 }
|
||||
# audio.ll (#22) is always linked here for the dev runner — unused snd_* are
|
||||
# dead-stripped; the canonical `ludicc -o` path links it only when Audio.* is used.
|
||||
if not shq(`{cc()} -O2 {ll} runtime/native/cocoa.ll runtime/native/audio.ll -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{pbf} -o {outbin}`) { return 1 }
|
||||
if not save { run(`rm -f {ll}`) }
|
||||
print(`done. run: ./{outbin} (from the repo root, so assets/ resolves)`)
|
||||
return 0
|
||||
}
|
||||
|
||||
# Remove every generated artifact and leave the tracked source untouched: the
|
||||
# whole build/ tree (IR, objects, compiled apps, the headless render at
|
||||
# build/out.ppm), and any stray *.tmp/*.ll left behind by a failed build. A
|
||||
# legacy out.ppm in the root (from an older toolchain) is swept too. The
|
||||
# toolchain binaries in bin/ are kept so the running `x` survives; rebuild them
|
||||
# any time with `x build`.
|
||||
function cmd_clean() -> int {
|
||||
run("rm -rf build")
|
||||
run("rm -f out.ppm bin/*.tmp")
|
||||
print("cleaned: build/ (incl. build/out.ppm), bin/*.tmp (bin/ toolchain kept — `x build` to rebuild)")
|
||||
return 0
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
# x — the Ludic task runner. One native binary (bin/x) that replaces every
|
||||
# build/test/bootstrap shell script in the repo. It is written in Ludic and
|
||||
# compiled by Ludic; the unix tools and clang it drives are invoked through the
|
||||
# `run` intrinsic, the same way the old *.sh files shelled out.
|
||||
#
|
||||
# Bootstrap it once from a clean checkout (the only step Ludic cannot do for
|
||||
# itself, since compiling Ludic needs a compiler):
|
||||
#
|
||||
# mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x
|
||||
#
|
||||
# Thereafter `bin/x build` rebuilds the whole toolchain — including bin/x itself.
|
||||
# Always run x from the repository root.
|
||||
program X {
|
||||
import "prelude.ludic"
|
||||
import "selfhost.ludic"
|
||||
import "build.ludic"
|
||||
import "tools.ludic"
|
||||
import "selfhost_test.ludic"
|
||||
import "test.ludic"
|
||||
import "json.ludic"
|
||||
import "lsp_test.ludic"
|
||||
import "forgejo.ludic"
|
||||
import "checks.ludic"
|
||||
import "docgen.ludic"
|
||||
import "docgen_gen.ludic"
|
||||
import "docgen_check.ludic"
|
||||
import "release.ludic"
|
||||
import "pkg.ludic"
|
||||
import "pkg_test.ludic"
|
||||
|
||||
function usage() -> void {
|
||||
print("x — the Ludic task runner (run from the repository root)")
|
||||
print("")
|
||||
print("build & run:")
|
||||
print(" x build build the whole toolchain into bin/ (ludicc, ludic, x, ludic-fmt, ludic-lsp)")
|
||||
print(" x build-cli build just bin/ludicc and bin/ludic from the IR seed")
|
||||
print(" x app <file.ludic> [--headless] [--save-temps]")
|
||||
print(" compile a Ludic program to a native app in build/")
|
||||
print(" x tools [--install] [--test] build the editor toolchain (ludic-fmt, ludic-lsp)")
|
||||
print(" x clean remove build/ (incl. build/out.ppm) and stray artifacts (keeps bin/)")
|
||||
print("")
|
||||
print("packages (issue #63 — MVS resolution, content-addressed store, ludic_modules/ links):")
|
||||
print(" x add <module>[@version] add a dependency to package.ludic, then resolve + fetch + link")
|
||||
print(" x get resolve every package.ludic dependency, link them, write the lock")
|
||||
print(" x update [module] bump a dep (or all) to its latest published version, then relock")
|
||||
print(" x verify check every locked package against the store by content hash")
|
||||
print(" x vendor copy the resolved packages into ./vendor for offline builds")
|
||||
print(" x build-lib <module.ludic> (#64) compile a package's module to a prebuilt dylib in lib/<target>/")
|
||||
print(" x link-flags (#64) print the clang flags to link this project's prebuilt module dylibs")
|
||||
print("")
|
||||
print("test:")
|
||||
print(" x test the full regression suite")
|
||||
print(" x test-pkg the package-manager suite (hermetic git repos, offline)")
|
||||
print(" x test --coverage per-file line coverage over the test specs")
|
||||
print(" x selfhost-test the self-hosting suite (correctness + bootstrap fixpoints)")
|
||||
print(" x test-tools the editor-toolchain suite")
|
||||
print(" x test-lsp drive bin/ludic-lsp over real LSP traffic (exit 2 = no server)")
|
||||
print(" x golden regenerate selfhost/golden/renders.sha256 (review with git diff)")
|
||||
print("")
|
||||
print("doc / lint checks (Ludic, no Python):")
|
||||
print(" x check-docs every ```ludic doc fence parses (or is marked skip/expect-error)")
|
||||
print(" x check-impl every implemented feature has a docs/language page")
|
||||
print(" x check-vocabulary the vocabulary is in sync across grammar / lexer / header / parser")
|
||||
print(" x lint-asset <file> validate one editor .json / .xml asset")
|
||||
print(" x docs-gen [--out DIR] generate the documentation site (default build/pages)")
|
||||
print(" x docs-check [DIR] coverage/integrity guard over a generated docs site")
|
||||
print(" x docs-palette [--check] regenerate emit_color.ludic + palette.json from the palette table (--check: compare only)")
|
||||
print("")
|
||||
print("release:")
|
||||
print(" x version print the toolchain version (ludicc --version)")
|
||||
print(" x release [major|minor|patch] [--dry-run] [--publish]")
|
||||
print(" cut a release: CHANGELOG + VERSION bump + tag (+ Forgejo release)")
|
||||
print(" x publish [vX.Y.Z] publish an already-tagged release (artifacts + notes); what CI runs on a tag push")
|
||||
print(" x changelog-section <ver> print that release's CHANGELOG.md section")
|
||||
print(" x changelog-render <ver> <date> <dir>")
|
||||
print(" render a CHANGELOG section from a directory of changesets")
|
||||
print("")
|
||||
print("self-host internals:")
|
||||
print(" x selfhost-build [ludicc] [out] assemble + compile the self-host compiler")
|
||||
print(" x bootstrap the self-hosting fixpoint proof (seeded from bin/ludicc)")
|
||||
print(" x bootstrap-cfree rebuild the compiler from the seed with no C compiler")
|
||||
print(" x reseed regenerate selfhost/ludicc.seed.ll after a compiler change")
|
||||
print(" x game-build <ludicc> <game.ludic> <out> compile a Ludic game (headless)")
|
||||
print(" x sh-compile <ludicc> <in.ludic> <out> compile + link one .ludic file")
|
||||
}
|
||||
|
||||
# a positional argument, or a default when absent
|
||||
function argn(i: int, dflt: pointer) -> pointer {
|
||||
if (i < arg_count()) { return arg(i) }
|
||||
return dflt
|
||||
}
|
||||
|
||||
# route one subcommand to its implementation and return its exit code; -1 when
|
||||
# the name is unknown. Returning (rather than exiting in place) lets entry tear
|
||||
# down the per-process scratch directory before the process ends.
|
||||
function dispatch(cmd: pointer) -> int {
|
||||
if (cmd == "build") { return cmd_build() }
|
||||
if (cmd == "build-cli") { return cmd_build_cli() }
|
||||
if (cmd == "app") { return cmd_app() }
|
||||
if (cmd == "tools") { return cmd_tools() }
|
||||
if (cmd == "clean") { return cmd_clean() }
|
||||
if (cmd == "test") {
|
||||
if (argn(2, "") == "--coverage") { return cmd_test_coverage() }
|
||||
return cmd_test()
|
||||
}
|
||||
if (cmd == "selfhost-test") { return cmd_selfhost_test() }
|
||||
if (cmd == "test-tools") { return cmd_test_tools() }
|
||||
if (cmd == "test-lsp") { return cmd_test_lsp() }
|
||||
if (cmd == "check-docs") { return cmd_check_docs() }
|
||||
if (cmd == "check-impl") { return cmd_check_impl() }
|
||||
if (cmd == "check-vocabulary") { return cmd_check_vocab() }
|
||||
if (cmd == "lint-asset") { return cmd_lint_asset() }
|
||||
if (cmd == "docs-palette") { return cmd_docs_palette() }
|
||||
if (cmd == "docs-gen") { return cmd_docs_gen() }
|
||||
if (cmd == "docs-check") { return cmd_docs_check() }
|
||||
if (cmd == "golden") { return cmd_golden() }
|
||||
if (cmd == "bootstrap") { return cmd_bootstrap() }
|
||||
if (cmd == "bootstrap-cfree") { return cmd_bootstrap_cfree() }
|
||||
if (cmd == "reseed") { return cmd_reseed() }
|
||||
if (cmd == "selfhost-build") { return cmd_selfhost_build(argn(2, "bin/ludicc"), argn(3, "build/selfhost")) }
|
||||
if (cmd == "sh-compile") {
|
||||
if (arg_count() < 5) { err("usage: x sh-compile <ludicc> <in.ludic> <out>\n"); exit(1) }
|
||||
return cmd_sh_compile(arg(2), arg(3), arg(4))
|
||||
}
|
||||
if (cmd == "game-build") {
|
||||
if (arg_count() < 5) { err("usage: x game-build <ludicc> <game.ludic> <out>\n"); exit(1) }
|
||||
return cmd_game_build(arg(2), arg(3), arg(4))
|
||||
}
|
||||
if (cmd == "add") { return cmd_pkg_add() }
|
||||
if (cmd == "get") { return cmd_pkg_get() }
|
||||
if (cmd == "update") { return cmd_pkg_update() }
|
||||
if (cmd == "verify") { return cmd_pkg_verify() }
|
||||
if (cmd == "vendor") { return cmd_pkg_vendor() }
|
||||
if (cmd == "build-lib") { return cmd_pkg_build_lib() }
|
||||
if (cmd == "link-flags") { return cmd_pkg_link_flags() }
|
||||
if (cmd == "test-pkg") { return cmd_test_pkg() }
|
||||
if (cmd == "version") or (cmd == "--version") or (cmd == "-v") { return cmd_version() }
|
||||
if (cmd == "release") { return cmd_release() }
|
||||
if (cmd == "changelog-render") { return cmd_changelog_render() }
|
||||
if (cmd == "changelog-section") { return cmd_changelog_section() }
|
||||
if (cmd == "publish") { return cmd_publish() }
|
||||
if (cmd == "help") or (cmd == "--help") or (cmd == "-h") { usage(); return 0 }
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
entry {
|
||||
if (arg_count() < 2) { usage(); exit(1) }
|
||||
let cmd = arg(1)
|
||||
let code = dispatch(cmd)
|
||||
tmp_cleanup()
|
||||
if code >= 0 { exit(code) }
|
||||
err(`x: unknown command '{cmd}'\n`)
|
||||
usage()
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue