`ludic build` produces a program. Double-clicked it opens a Terminal window, it wears the generic executable icon, it calls itself whatever the file is called, and it carries none of its assets. `ludic bundle` produces an application. Everything it needs is in package.ludic, so the command takes no arguments: an Info.plist and PkgInfo from `app` lines, an .icns built by sips and iconutil at all ten sizes macOS asks for from a single source PNG, the asset pack in Contents/Resources, and an ad-hoc signature - which is not optional on Apple silicon, where an unsigned binary is killed rather than warned about. The bundle identifier falls back to the package path reversed, so a project that never thinks about it still gets a defensible one instead of two apps sharing a key Launch Services hangs the Dock, saved state and permissions off. A bundled game is moved to ~/Library/Application Support/<name> before main, because Finder starts a .app with its working directory at "/" where no save could ever be written. Reads come out of the pack, writes land somewhere real and per-user, and the game's save code needs no change and no platform knowledge. The splash is the other half of looking like an application. A game that loads 165 MB spends a visible moment doing it with nothing on screen, which from the outside is indistinguishable from a launch that failed. splash_show puts a borderless window up from the same constructor that mounts the pack - before main, so it appears while the process is still starting rather than after the slow part it exists to cover - and reads the artwork out of the pack like any other asset. It turns the run loop enough times to be mapped and composited there and then; once composited the backing store survives a busy main thread, so it stays up for the whole load. Nothing hides it automatically. Only the game knows when its first real frame is ready, and a splash that vanishes before that leaves the same black gap it was covering, so the game calls App.splash_hide(). Headless there is no splash and the call lowers to nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
136 lines
6.3 KiB
Text
136 lines
6.3 KiB
Text
# ludic — the command-line interface to the Ludic language. 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.
|
|
#
|
|
# This is everything a user of the language does: create a project, build it, run
|
|
# it, test it, add a package, format, upgrade. Nothing here is about the
|
|
# toolchain's own repository — building the compiler, reseeding it, generating
|
|
# the docs site, cutting a release live in a separate tool (dev.ludic ->
|
|
# bin/ludic-dev) that only exists in a checkout and is never installed. Someone
|
|
# who installs Ludic should not have to read past commands they cannot use.
|
|
#
|
|
# 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/dev.ludic -o bin/ludic-dev
|
|
# bin/ludic-dev build
|
|
program Ludic {
|
|
import "prelude.ludic"
|
|
import "build.ludic"
|
|
import "project.ludic"
|
|
import "pkg.ludic"
|
|
import "assets.ludic"
|
|
import "pack.ludic"
|
|
import "bundle.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(" assets [--force] fetch the CC0 materials the renderer needs into assets/polyhaven/")
|
|
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("shipping:")
|
|
print(" pack [--out FILE] [dirs...] pack the assets into build/<name>.lpak")
|
|
print(" pack --list FILE print what is in a pack")
|
|
print(" pack --verify FILE re-hash every entry against the table")
|
|
print(" bundle [file] build a macOS .app: icon, metadata, assets, splash")
|
|
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 [--check] [paths...] format Ludic source in place (--check: report, write nothing)")
|
|
print(" lsp run the language server on stdio (what editors spawn)")
|
|
print(" help this message")
|
|
print("")
|
|
print("A file argument runs it: ludic mygame.ludic")
|
|
}
|
|
|
|
# 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"
|
|
}
|
|
|
|
# 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 == "assets") { return cmd_fetch_assets() }
|
|
if (cmd == "pack") { return cmd_pack() }
|
|
if (cmd == "bundle") { return cmd_bundle() }
|
|
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 }
|
|
|
|
# The toolchain's own tasks used to live here under `ludic dev`. Say where
|
|
# they went rather than printing "unknown command" at someone who read an
|
|
# older README or a commit message.
|
|
if (cmd == "dev") {
|
|
err("ludic: the toolchain's own tasks live in `ludic-dev`, which is built from a\n")
|
|
err(" checkout of the Ludic repository and is not part of an install:\n")
|
|
err(" clang selfhost/ludicc.seed.ll -o bin/ludicc\n")
|
|
err(" bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev && bin/ludic-dev help\n")
|
|
return 1
|
|
}
|
|
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)
|
|
}
|
|
}
|