ludic/tools/x/main.ludic
Orkuncakilkaya 5c4c10a1d7 fix(release): keep a changeset's markdown in the changelog
build_section piped every changeset body through `tr '\n' ' '`. A multi-line
changeset came out as one paragraph, so nested bullets rendered as inline
" - " runs and a whole release read as a single unbroken block — v0.3.0 was one
~4 KB bullet.

The renderer is now Ludic rather than a shell one-liner. A section is grouped
by conventional-commit type (Features, Fixes, Performance, ...), each changeset
is one bullet, and continuation lines are indented two spaces so nested lists
and paragraphs stay inside their item. Bullets are sorted within a group, so
cutting the same release twice produces the same text.

Also:

- `x release --dry-run` renders the pending section to stdout and touches
  nothing, so a release can be read before it is cut.
- `x changelog-render` re-renders a section from a directory of changesets, and
  `x changelog-section` prints one release's section back out of CHANGELOG.md.
- The v0.1.0 and v0.3.0 sections are re-rendered with the former, from the
  changesets recovered at each tag's parent commit; the bullet counts (6 and
  25) and word multisets are unchanged. v0.2.0 is left alone: it carries a
  hand-written summary and topical subheadings, and regenerating it would have
  replaced curation with raw changeset dumps. The file header now says that
  a section may carry such a summary, since it previously claimed released
  sections are never hand-edited.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 01:47:49 +03:00

157 lines
8.5 KiB
Text

# 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)
}
}