`Gl.*` binds the whole OpenGL 4.1 core API — every entry point of the platform gl3.h with every GL_* constant, generated by `ludic-dev glgen` with per-call ABI thunks. Windowed builds get an NSOpenGLContext on the existing window at Retina resolution; headless builds render into an offscreen CGL context, so a program that uses Gl.* renders and screenshots identically under the test harness. It links gl.ll, the thunks and OpenGL.framework only when used; every other build stays byte-identical. packages/ludic.render3d is a physically based renderer written on that surface: HDRI image-based lighting, GPU-generated terrain with scanned PBR materials, CDLOD, cascaded shadows, glTF with skinning, instanced vegetation with impostors, procedural grass, water, SSAO, and an HDR pipeline with bloom, auto-exposure and ACES. It also carries this session's work on it: the terrain at half its cost (10.3 -> 5.4 ms of frame), the streaming hitch that got worse the longer you played, a resize that emptied the world, and the packaging that lets a game use the renderer from its own repository — `ludic assets`, the material manifest shipping with the package, and shader lookup falling back to the install root. See changes/ for each, with its numbers. The camping game that drove all of it has moved out to its own repository, Maroon Lake; examples/rendering/smooth.ludic stays as the renderer's example here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
539 lines
20 KiB
Text
539 lines
20 KiB
Text
# 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.
|
|
# The built binary's name, from the manifest's module path when there is one.
|
|
#
|
|
# A module path is a URL — `git.host/user/maroon-lake` — so the last path segment comes
|
|
# first: taking the last DOT of that would have cut inside the host and produced
|
|
# `io/user/maroon-lake`, which git-hosted names all share and which `build/{name}` then
|
|
# turned into directories. Within the segment a dot still separates a namespace from the
|
|
# package (`ludic.render3d` builds as `render3d`).
|
|
function project_name(entry: pointer) -> pointer {
|
|
var mod = manifest_name()
|
|
if mod != "" {
|
|
var n = 0
|
|
while mod[n] != 0 { n += 1 }
|
|
var slash = -1
|
|
var i = 0
|
|
while i < n { if mod[i] == '/' { slash = i }; i += 1 }
|
|
if slash >= 0 { mod = mod[slash + 1..n]; n = n - slash - 1 }
|
|
var last = -1
|
|
i = 0
|
|
while i < n { if mod[i] == '.' { last = i }; i += 1 }
|
|
if last >= 0 { return mod[last + 1..n] }
|
|
if n > 0 { 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 ---------------------------------------------------------------
|
|
|
|
# A project name is a directory name and a package identifier, so it is held to
|
|
# what both accept: letters, digits, '-', '_' and '.'. Anything else — a space, a
|
|
# slash, a quote — either breaks the shell commands that create the tree or
|
|
# produces a manifest nobody can depend on, and failing here with the rule beats
|
|
# failing later with something obscure.
|
|
function valid_project_name(name: pointer) -> bool {
|
|
let n = slen(name)
|
|
if n == 0 { return false }
|
|
var i = 0
|
|
while i < n {
|
|
let c = name[i]
|
|
let alnum = (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9')
|
|
let punct = c == '-' or c == '_' or c == '.'
|
|
if not (alnum or punct) { return false }
|
|
i += 1
|
|
}
|
|
return true
|
|
}
|
|
|
|
# The `program <Name>` identifier for a project called `name`.
|
|
#
|
|
# The name is a directory name and the identifier is Ludic source, and they do
|
|
# not accept the same characters: `ludic new my-game` wrote `program My-Game`,
|
|
# which is a subtraction, and `ludic new 2048` wrote an identifier starting with
|
|
# a digit. Both scaffolded a project that would not compile — the first thing the
|
|
# user did with it. So: split on anything that is not a letter or digit,
|
|
# capitalise each piece, and join. A leading digit gets a `Game` prefix, since an
|
|
# identifier cannot start with one.
|
|
function ident_of(name: pointer) -> pointer {
|
|
let n = slen(name)
|
|
let b = bytes(n + 8)
|
|
var out = 0
|
|
var at_start = true
|
|
var i = 0
|
|
while i < n {
|
|
var c = name[i]
|
|
let alpha = (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z')
|
|
let digit = c >= '0' and c <= '9'
|
|
if alpha or digit {
|
|
if at_start {
|
|
if c >= 'a' and c <= 'z' { c -= 32 }
|
|
at_start = false
|
|
} else {
|
|
if c >= 'A' and c <= 'Z' { c += 32 }
|
|
}
|
|
b[out] = c
|
|
out += 1
|
|
} else {
|
|
at_start = true # the next letter starts a new word
|
|
}
|
|
i += 1
|
|
}
|
|
b[out] = 0
|
|
if out == 0 { return "Game" }
|
|
if b[0] >= '0' and b[0] <= '9' { return "Game" + sslice(b, 0, out) }
|
|
return sslice(b, 0, out)
|
|
}
|
|
|
|
# ---- 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_count() < 3 {
|
|
err("usage: ludic new <name>\n")
|
|
return 1
|
|
}
|
|
let name = arg(2)
|
|
if not valid_project_name(name) {
|
|
err(`ludic new: '{name}' is not a usable project name\n`)
|
|
err(" use letters, digits, '-', '_' or '.' (it names a directory and a package)\n")
|
|
return 1
|
|
}
|
|
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(ident_of(name)))
|
|
write_file(`{name}/tests/smoke.ludic`, template_test(ident_of(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
|
|
|
|
# g_argerr is set when the command line itself was wrong — an unknown flag, or
|
|
# -o with nothing after it. Silently ignoring those meant `ludic build --headles`
|
|
# quietly produced a windowed binary and `ludic build -o` quietly ignored the
|
|
# request, which is the kind of thing you only notice much later.
|
|
var g_argerr: bool = false
|
|
|
|
function parse_build_args(start: int) -> pointer {
|
|
var src = ""
|
|
g_mode = 1
|
|
g_out = ""
|
|
g_save = false
|
|
g_argerr = false
|
|
var ai = start
|
|
while ai < arg_count() {
|
|
let a = arg(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_count() { g_out = arg(ai) }
|
|
else { err("ludic: -o needs a path\n"); g_argerr = true }
|
|
}
|
|
else if a[0] == '-' {
|
|
err(`ludic: unknown option {a}\n`)
|
|
err(" build/run take: [file] [--headless|--windowed] [-o out] [--save-temps]\n")
|
|
g_argerr = true
|
|
}
|
|
else { src = a }
|
|
ai += 1
|
|
}
|
|
if g_argerr { return "" }
|
|
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 g_argerr { return 1 }
|
|
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 g_argerr { return 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 mygame.ludic` — the file is argv[1], so the scan starts there.
|
|
function cmd_run_file() -> int {
|
|
let entry = parse_build_args(1)
|
|
if g_argerr { return 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_count() { if arg(ai)[0] != '-' { push(files, arg(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
|
|
}
|
|
var mi = 0
|
|
while mi < len(files) {
|
|
if not file_exists(files[mi]) { err(`ludic test: no such file: {files[mi]}\n`); return 1 }
|
|
mi += 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 [--check] [paths...] — format the project's source in place (src/ and
|
|
# tests/ by default), or the paths named. --check writes nothing and exits
|
|
# non-zero if anything is unformatted, which is what a pre-commit hook or CI
|
|
# wants.
|
|
#
|
|
# In place is the default because that is what `fmt` means everywhere else and
|
|
# what this command's own help promised; the underlying ludic-fmt defaults to
|
|
# printing to stdout, which as a project-level command would just scroll the file
|
|
# past you and change nothing.
|
|
function cmd_fmt() -> int {
|
|
var mode = "-w"
|
|
var args = ""
|
|
var ai = 2
|
|
while ai < arg_count() {
|
|
let a = arg(ai)
|
|
if a == "--check" { mode = "--check" }
|
|
else if a[0] == '-' {
|
|
err(`ludic fmt: unknown option {a}\n`)
|
|
err(" usage: ludic fmt [--check] [paths...]\n")
|
|
return 1
|
|
}
|
|
else { args = `{args} {a}` }
|
|
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")} {mode}{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_count() { args = `{args} {arg(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 version — report the version of the toolchain this CLI belongs to.
|
|
#
|
|
# It asks the compiler (proving that path works), addressed through ludic_home()
|
|
# rather than as a relative bin/ludicc: `ludic version` is run from a user's
|
|
# project, not from the toolchain's own directory, and looking beside the current
|
|
# directory found nothing there and reported "(version unknown)" for a perfectly
|
|
# good install.
|
|
function cmd_version() -> int {
|
|
let cc = ludicc()
|
|
if is_exec(cc) {
|
|
run(`{cc} --version`)
|
|
return 0
|
|
}
|
|
let v = read_file(`{ludic_home()}VERSION`)
|
|
if v != null { print(`ludic {s_trim(v)}`); return 0 }
|
|
print("ludic (version unknown)")
|
|
return 0
|
|
}
|
|
|
|
# ---- 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_count() >= 3 { ver = arg(2) }
|
|
if not shq("command -v curl >/dev/null 2>&1") {
|
|
err("ludic upgrade: needs curl\n")
|
|
return 1
|
|
}
|
|
# stderr, not stdout: this line is progress, and stdout is block-buffered when
|
|
# piped, which would print it after the installer it introduces
|
|
err(`upgrading from {install_url()}\n`)
|
|
if ver == "" { return sh(`curl -fsSL {install_url()} | sh`) }
|
|
return sh(`curl -fsSL {install_url()} | sh -s -- --version {ver}`)
|
|
}
|