feat(release): SemVer + ludicc --version, changesets, and x release
Some checks failed
commit-lint / conventional-commits (push) Waiting to run
bootstrap / cfree-fixpoint (push) Successful in 13s
ci / build-and-test (push) Has been cancelled

The project had no versioning discipline: 0 tags, no CHANGELOG, no way for the
compiler to report a version. Add a lightweight, native release flow.

- Versioning: SemVer, with VERSION as the single source of truth. `ludicc
  --version` (and `ludic --version`) read it at runtime — so a bump touches one
  file and never reseeds the compiler. `x version` reports it too.
- Changesets: one small Markdown file per user-facing change under changes/
  (bump level + type + summary; see changes/README.md). This replaces "remember
  to edit the changelog" with a mergeable artifact, no Node changeset tool.
- `x release [major|minor|patch] [--publish]`: fold the pending changesets into a
  new CHANGELOG.md section (grouped by type), bump VERSION, commit, and tag
  vX.Y.Z. The level defaults to the highest changeset bump. `--publish` also
  pushes and creates the Forgejo release with source + toolchain tarballs;
  tools/ci/forgejo_release.py is the small stdlib-Python HTTP glue for the
  release API (a native Http client is issue #6).

Seed the initial changesets describing the shipped surface; the first `x release`
turns them into the v0.1.0 CHANGELOG. Reseeded for the --version flag; C-free
bootstrap fixpoint holds; suites 56 / 29 / 29 on macOS, 51 / 28 (+skips) on Linux
CI, bootstrap-cfree byte-identical on both.

Part of the repository-cleanup / DX pass (with #32, #34).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-30 23:46:59 +03:00
parent 709465cdd8
commit fed80f2152
15 changed files with 787 additions and 293 deletions

View file

@ -63,6 +63,27 @@ The stdlib lives in the runtime (`runtime/`) and is surfaced as namespaces
3. Add or extend an example under `examples/` and a case in the test suite.
4. Run `python3 tools/docgen/gen.py && python3 tools/docgen/check.py` — the
check fails if any inventory symbol lacks a page or is still seed text.
5. Add a **changeset** for the user-facing change: a small file under
[`changes/`](changes/README.md) with a `bump:` level and a one-line summary.
The next release folds it into `CHANGELOG.md`.
## Versioning & releases
The toolchain is versioned with [SemVer](https://semver.org); `VERSION` is the
single source of truth and `ludicc --version` (or `x version`) reports it.
Releases are changeset-driven. Every user-facing change ships with a changeset
(step 5 above). To cut a release:
```bash
x release [major|minor|patch] # omit the level to derive it from the changesets
```
That aggregates the pending changesets into a new `CHANGELOG.md` section, bumps
`VERSION`, commits `chore(release): vX.Y.Z`, and tags it. Add `--publish` (with
`FORGEJO_TOKEN` set) to also push and create the Forgejo release with source and
toolchain tarballs. The tag doubles as the reproducible bootstrap point: the
source archive plus its checked-in seed rebuild that exact toolchain.
## Conventions

View file

@ -137,7 +137,10 @@ Ludic-written PNG/DEFLATE decoder — no zlib, no external dependency.
The compiler self-hosts to a byte-exact fixpoint and rebuilds from its IR seed
with no C compiler; the ECS runtime, windowed + headless 2D rendering, the event
bus, the deterministic networking stack, scenes, and save/load are all in place
and covered by `bin/x test`.
and covered by `bin/x test`. CI gates every push and PR on the build, the test
suites, and that C-free fixpoint. The toolchain is versioned with SemVer
(`ludicc --version`); releases and the `CHANGELOG.md` are cut from changesets by
`x release`.
Active work and proposals — the standard library, a fuller type system,
rendering/animation/lighting extras, input, filesystem/IO, testing, and

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.0.0

View file

@ -0,0 +1,3 @@
bump: minor
type: feat
Self-hosted, C-free toolchain — the compiler, runtime, task runner and editor tools are all written in Ludic and built from a checked-in LLVM-IR seed with clang alone; `x bootstrap-cfree` proves the compiler rebuilds itself byte-for-byte.

View file

@ -0,0 +1,3 @@
bump: minor
type: feat
Namespaced standard library — Math, Text, List, Random, Time, Screen, Color, Ease, Collide, Memory, Vector, DateTime/Date/Duration/Clock, Unicode, Os, Fs/Path/Mime, Log, Noise, Hash, Crypto and Uuid, each deterministic where a game needs it.

View file

@ -0,0 +1,3 @@
bump: minor
type: feat
Native 2D backend — an ECS core with a deterministic fixed-point (Q16.16) runtime, a windowed Cocoa target on macOS and a headless PPM renderer that runs anywhere.

View file

@ -0,0 +1,3 @@
bump: minor
type: feat
Editor tooling — `ludic-fmt` (formatter) and `ludic-lsp` (language server), plus VS Code and JetBrains integrations, all built by the toolchain.

3
changes/0005-ci.md Normal file
View file

@ -0,0 +1,3 @@
bump: minor
type: ci
Continuous integration — Forgejo Actions workflows build the toolchain from the seed, run the regression + editor suites, assert the C-free bootstrap fixpoint, and lint commit messages on every push and pull request.

View file

@ -0,0 +1,3 @@
bump: minor
type: feat
Versioning and releases — SemVer with `ludicc --version`, a changeset-driven `CHANGELOG.md`, and `x release` to bump, tag, and publish a Forgejo release with source and toolchain artifacts.

25
changes/README.md Normal file
View file

@ -0,0 +1,25 @@
# Changesets
A **changeset** is one small Markdown file describing a single user-facing change,
dropped in this directory. `x release` consumes every changeset here into a new
`CHANGELOG.md` section, bumps `VERSION`, and deletes the consumed files.
## Format
```
bump: minor
type: feat
One or more lines describing the change, in the past-agnostic imperative used in
the changelog. Markdown is fine.
```
- `bump:` — `major`, `minor`, or `patch` (SemVer). The release version is bumped
by the **highest** level among the pending changesets (unless `x release <level>`
overrides it).
- `type:` — the Conventional Commit type (`feat`, `fix`, `perf`, `docs`, …); it
becomes the bold prefix of the changelog bullet.
## Adding one
Create a file with a short, unique name, e.g. `changes/regex-namespace.md`. Any
filename works except this `README.md`, which the release step always skips.

View file

@ -7499,32 +7499,37 @@ declare void @win_close()
@.str7389 = private unnamed_addr constant [3 x i8] c"wb\00"
@.str7390 = private unnamed_addr constant [7 x i8] c".ludic\00"
@.str7391 = private unnamed_addr constant [2 x i8] c"/\00"
@.str7392 = private unnamed_addr constant [6 x i8] c"ludic\00"
@.str7393 = private unnamed_addr constant [11 x i8] c"--windowed\00"
@.str7394 = private unnamed_addr constant [11 x i8] c"--headless\00"
@.str7395 = private unnamed_addr constant [12 x i8] c"--emit-llvm\00"
@.str7396 = private unnamed_addr constant [6 x i8] c"--fmt\00"
@.str7397 = private unnamed_addr constant [13 x i8] c"--save-temps\00"
@.str7398 = private unnamed_addr constant [6 x i8] c"--run\00"
@.str7399 = private unnamed_addr constant [3 x i8] c"-o\00"
@.str7400 = private unnamed_addr constant [31 x i8] c"ludicc: ignoring unknown flag \00"
@.str7401 = private unnamed_addr constant [2 x i8] c"\0A\00"
@.str7402 = private unnamed_addr constant [90 x i8] c"usage: ludicc <file.ludic> [-o out] [--windowed|--headless] [--emit-llvm] [--save-temps]\0A\00"
@.str7403 = private unnamed_addr constant [27 x i8] c"ludicc: cannot open input\0A\00"
@.str7404 = private unnamed_addr constant [16 x i8] c"/tmp/ludic-run-\00"
@.str7405 = private unnamed_addr constant [10 x i8] c"mkdir -p \00"
@.str7406 = private unnamed_addr constant [4 x i8] c".ll\00"
@.str7407 = private unnamed_addr constant [25 x i8] c"ludicc: cannot write IR\0A\00"
@.str7408 = private unnamed_addr constant [9 x i8] c"LUDIC_CC\00"
@.str7409 = private unnamed_addr constant [6 x i8] c"clang\00"
@.str7410 = private unnamed_addr constant [11 x i8] c"LUDIC_HOME\00"
@.str7411 = private unnamed_addr constant [27 x i8] c" -O2 -Wno-override-module \00"
@.str7412 = private unnamed_addr constant [24 x i8] c"runtime/native/cocoa.ll\00"
@.str7413 = private unnamed_addr constant [2 x i8] c" \00"
@.str7414 = private unnamed_addr constant [42 x i8] c" -framework Cocoa -Wl,-rpath,@loader_path\00"
@.str7415 = private unnamed_addr constant [5 x i8] c" -o \00"
@.str7416 = private unnamed_addr constant [7 x i8] c"rm -f \00"
@.str7417 = private unnamed_addr constant [21 x i8] c"ludicc: link failed\0A\00"
@.str7392 = private unnamed_addr constant [11 x i8] c"LUDIC_HOME\00"
@.str7393 = private unnamed_addr constant [8 x i8] c"VERSION\00"
@.str7394 = private unnamed_addr constant [24 x i8] c"ludic (version unknown)\00"
@.str7395 = private unnamed_addr constant [7 x i8] c"ludic \00"
@.str7396 = private unnamed_addr constant [6 x i8] c"ludic\00"
@.str7397 = private unnamed_addr constant [10 x i8] c"--version\00"
@.str7398 = private unnamed_addr constant [11 x i8] c"--windowed\00"
@.str7399 = private unnamed_addr constant [11 x i8] c"--headless\00"
@.str7400 = private unnamed_addr constant [12 x i8] c"--emit-llvm\00"
@.str7401 = private unnamed_addr constant [6 x i8] c"--fmt\00"
@.str7402 = private unnamed_addr constant [13 x i8] c"--save-temps\00"
@.str7403 = private unnamed_addr constant [6 x i8] c"--run\00"
@.str7404 = private unnamed_addr constant [3 x i8] c"-o\00"
@.str7405 = private unnamed_addr constant [31 x i8] c"ludicc: ignoring unknown flag \00"
@.str7406 = private unnamed_addr constant [2 x i8] c"\0A\00"
@.str7407 = private unnamed_addr constant [90 x i8] c"usage: ludicc <file.ludic> [-o out] [--windowed|--headless] [--emit-llvm] [--save-temps]\0A\00"
@.str7408 = private unnamed_addr constant [27 x i8] c"ludicc: cannot open input\0A\00"
@.str7409 = private unnamed_addr constant [16 x i8] c"/tmp/ludic-run-\00"
@.str7410 = private unnamed_addr constant [10 x i8] c"mkdir -p \00"
@.str7411 = private unnamed_addr constant [4 x i8] c".ll\00"
@.str7412 = private unnamed_addr constant [25 x i8] c"ludicc: cannot write IR\0A\00"
@.str7413 = private unnamed_addr constant [9 x i8] c"LUDIC_CC\00"
@.str7414 = private unnamed_addr constant [6 x i8] c"clang\00"
@.str7415 = private unnamed_addr constant [11 x i8] c"LUDIC_HOME\00"
@.str7416 = private unnamed_addr constant [27 x i8] c" -O2 -Wno-override-module \00"
@.str7417 = private unnamed_addr constant [24 x i8] c"runtime/native/cocoa.ll\00"
@.str7418 = private unnamed_addr constant [2 x i8] c" \00"
@.str7419 = private unnamed_addr constant [42 x i8] c" -framework Cocoa -Wl,-rpath,@loader_path\00"
@.str7420 = private unnamed_addr constant [5 x i8] c" -o \00"
@.str7421 = private unnamed_addr constant [7 x i8] c"rm -f \00"
@.str7422 = private unnamed_addr constant [21 x i8] c"ludicc: link failed\0A\00"
define i32 @fn_str_eq(ptr %a, ptr %b) {
entry:
br label %loop
@ -62186,6 +62191,135 @@ ret:
ret void
}
define ptr @fn_chomp(ptr %arg_s) {
entry:
%retval = alloca ptr
%t0 = alloca ptr
%t4 = alloca i32
%t5 = alloca i32
%t11 = alloca i32
%t12 = alloca i32
store ptr %arg_s, ptr %t0
%t1 = load ptr, ptr %t0
%t2 = call i64 @strlen(ptr %t1)
%t3 = trunc i64 %t2 to i32
store i32 %t3, ptr %t4
br label %wcond0
wcond0:
%t6 = load i32, ptr %t4
%t7 = icmp sgt i32 %t6, 0
%t8 = zext i1 %t7 to i32
%t9 = icmp ne i32 %t8, 0
%t10 = zext i1 %t9 to i32
store i32 %t10, ptr %t5
br i1 %t9, label %sc3, label %scend4
sc3:
%t13 = load ptr, ptr %t0
%t14 = load i32, ptr %t4
%t15 = sub i32 %t14, 1
%t16 = getelementptr inbounds i8, ptr %t13, i32 %t15
%t17 = load i8, ptr %t16
%t18 = zext i8 %t17 to i32
%t19 = icmp eq i32 %t18, 10
%t20 = zext i1 %t19 to i32
%t21 = icmp ne i32 %t20, 0
%t22 = zext i1 %t21 to i32
store i32 %t22, ptr %t12
br i1 %t21, label %scend6, label %sc5
sc5:
%t23 = load ptr, ptr %t0
%t24 = load i32, ptr %t4
%t25 = sub i32 %t24, 1
%t26 = getelementptr inbounds i8, ptr %t23, i32 %t25
%t27 = load i8, ptr %t26
%t28 = zext i8 %t27 to i32
%t29 = icmp eq i32 %t28, 13
%t30 = zext i1 %t29 to i32
%t31 = icmp ne i32 %t30, 0
%t32 = zext i1 %t31 to i32
store i32 %t32, ptr %t12
br label %scend6
scend6:
%t33 = load i32, ptr %t12
%t34 = icmp ne i32 %t33, 0
%t35 = zext i1 %t34 to i32
store i32 %t35, ptr %t11
br i1 %t34, label %scend8, label %sc7
sc7:
%t36 = load ptr, ptr %t0
%t37 = load i32, ptr %t4
%t38 = sub i32 %t37, 1
%t39 = getelementptr inbounds i8, ptr %t36, i32 %t38
%t40 = load i8, ptr %t39
%t41 = zext i8 %t40 to i32
%t42 = icmp eq i32 %t41, 32
%t43 = zext i1 %t42 to i32
%t44 = icmp ne i32 %t43, 0
%t45 = zext i1 %t44 to i32
store i32 %t45, ptr %t11
br label %scend8
scend8:
%t46 = load i32, ptr %t11
%t47 = icmp ne i32 %t46, 0
%t48 = zext i1 %t47 to i32
store i32 %t48, ptr %t5
br label %scend4
scend4:
%t49 = load i32, ptr %t5
%t50 = icmp ne i32 %t49, 0
br i1 %t50, label %wbody1, label %wend2
wbody1:
%t51 = load i32, ptr %t4
%t52 = sub i32 %t51, 1
store i32 %t52, ptr %t4
br label %wcond0
wend2:
%t53 = load ptr, ptr %t0
%t54 = load i32, ptr %t4
%t55 = call ptr @fn_str_slice(ptr %t53, i32 0, i32 %t54)
store ptr %t55, ptr %retval
br label %ret
ret:
%t56 = load ptr, ptr %retval
ret ptr %t56
}
define void @fn_show_version() {
entry:
%t6 = alloca ptr
%t10 = alloca ptr
%t0 = load ptr, ptr @L_argv
%t1 = getelementptr ptr, ptr %t0, i32 0
%t2 = load ptr, ptr %t1
%t3 = call ptr @fn_dir_of(ptr %t2)
%t4 = call ptr @fn_getenv_or(ptr @.str7392, ptr %t3)
%t5 = call ptr @fn_ensure_slash(ptr %t4)
store ptr %t5, ptr %t6
%t7 = load ptr, ptr %t6
%t8 = call ptr @fn_path_join(ptr %t7, ptr @.str7393)
%t9 = call ptr @fn_read_file(ptr %t8)
store ptr %t9, ptr %t10
%t11 = load ptr, ptr %t10
%t12 = icmp eq ptr %t11, null
%t13 = zext i1 %t12 to i32
%t14 = icmp ne i32 %t13, 0
br i1 %t14, label %then0, label %else1
then0:
call i32 (ptr, ...) @printf(ptr @.fmt_line, ptr @.str7394)
br label %ifend2
else1:
%t15 = load ptr, ptr %t10
%t16 = call ptr @fn_chomp(ptr %t15)
%t17 = call ptr @fn_str_concat(ptr @.str7395, ptr %t16)
call i32 (ptr, ...) @printf(ptr @.fmt_line, ptr %t17)
br label %ifend2
ifend2:
call void @exit(i32 0)
unreachable
ret:
ret void
}
define i32 @main(i32 %argc, ptr %argv) {
entry:
%retval = alloca i32
@ -62198,20 +62332,20 @@ entry:
%t6 = alloca i32
%t13 = alloca i32
%t23 = alloca ptr
%t66 = alloca ptr
%t84 = alloca ptr
%t94 = alloca i32
%t104 = alloca i32
%t113 = alloca i32
%t130 = alloca i32
%t147 = alloca ptr
%t159 = alloca ptr
%t166 = alloca ptr
%t173 = alloca ptr
%t178 = alloca ptr
%t183 = alloca ptr
%t195 = alloca i32
%t211 = alloca i32
%t69 = alloca ptr
%t87 = alloca ptr
%t97 = alloca i32
%t107 = alloca i32
%t116 = alloca i32
%t133 = alloca i32
%t150 = alloca ptr
%t162 = alloca ptr
%t169 = alloca ptr
%t176 = alloca ptr
%t181 = alloca ptr
%t186 = alloca ptr
%t198 = alloca i32
%t214 = alloca i32
store i32 %argc, ptr @L_argc
store ptr %argv, ptr @L_argv
store i32 0, ptr %retval
@ -62226,7 +62360,7 @@ entry:
%t8 = getelementptr ptr, ptr %t7, i32 0
%t9 = load ptr, ptr %t8
%t10 = call ptr @fn_base_name(ptr %t9)
%t11 = call i32 @fn_str_eq(ptr %t10, ptr @.str7392)
%t11 = call i32 @fn_str_eq(ptr %t10, ptr @.str7396)
%t12 = icmp ne i32 %t11, 0
br i1 %t12, label %then0, label %ifend2
then0:
@ -62249,104 +62383,114 @@ wbody4:
%t22 = load ptr, ptr %t21
store ptr %t22, ptr %t23
%t24 = load ptr, ptr %t23
%t25 = call i32 @fn_str_eq(ptr %t24, ptr @.str7393)
%t25 = call i32 @fn_str_eq(ptr %t24, ptr @.str7397)
%t26 = icmp ne i32 %t25, 0
br i1 %t26, label %then6, label %else7
then6:
store i32 1, ptr %t2
call void @fn_show_version()
br label %ifend8
else7:
%t27 = load ptr, ptr %t23
%t28 = call i32 @fn_str_eq(ptr %t27, ptr @.str7394)
%t28 = call i32 @fn_str_eq(ptr %t27, ptr @.str7398)
%t29 = icmp ne i32 %t28, 0
br i1 %t29, label %then9, label %else10
then9:
store i32 2, ptr %t2
store i32 1, ptr %t2
br label %ifend11
else10:
%t30 = load ptr, ptr %t23
%t31 = call i32 @fn_str_eq(ptr %t30, ptr @.str7395)
%t31 = call i32 @fn_str_eq(ptr %t30, ptr @.str7399)
%t32 = icmp ne i32 %t31, 0
br i1 %t32, label %then12, label %else13
then12:
store i32 1, ptr %t3
store i32 2, ptr %t2
br label %ifend14
else13:
%t33 = load ptr, ptr %t23
%t34 = call i32 @fn_str_eq(ptr %t33, ptr @.str7396)
%t34 = call i32 @fn_str_eq(ptr %t33, ptr @.str7400)
%t35 = icmp ne i32 %t34, 0
br i1 %t35, label %then15, label %else16
then15:
store i32 1, ptr %t4
store i32 1, ptr %t3
br label %ifend17
else16:
%t36 = load ptr, ptr %t23
%t37 = call i32 @fn_str_eq(ptr %t36, ptr @.str7397)
%t37 = call i32 @fn_str_eq(ptr %t36, ptr @.str7401)
%t38 = icmp ne i32 %t37, 0
br i1 %t38, label %then18, label %else19
then18:
store i32 1, ptr %t5
store i32 1, ptr %t4
br label %ifend20
else19:
%t39 = load ptr, ptr %t23
%t40 = call i32 @fn_str_eq(ptr %t39, ptr @.str7398)
%t40 = call i32 @fn_str_eq(ptr %t39, ptr @.str7402)
%t41 = icmp ne i32 %t40, 0
br i1 %t41, label %then21, label %else22
then21:
store i32 1, ptr %t6
store i32 1, ptr %t5
br label %ifend23
else22:
%t42 = load ptr, ptr %t23
%t43 = call i32 @fn_str_eq(ptr %t42, ptr @.str7399)
%t43 = call i32 @fn_str_eq(ptr %t42, ptr @.str7403)
%t44 = icmp ne i32 %t43, 0
br i1 %t44, label %then24, label %else25
then24:
%t45 = load i32, ptr %t13
%t46 = add i32 %t45, 1
store i32 %t46, ptr %t13
%t47 = load i32, ptr %t13
%t48 = load i32, ptr @L_argc
%t49 = icmp slt i32 %t47, %t48
%t50 = zext i1 %t49 to i32
%t51 = icmp ne i32 %t50, 0
br i1 %t51, label %then27, label %ifend29
then27:
%t52 = load i32, ptr %t13
%t53 = load ptr, ptr @L_argv
%t54 = getelementptr ptr, ptr %t53, i32 %t52
%t55 = load ptr, ptr %t54
store ptr %t55, ptr %t1
br label %ifend29
ifend29:
store i32 1, ptr %t6
br label %ifend26
else25:
%t56 = load ptr, ptr %t23
%t57 = getelementptr inbounds i8, ptr %t56, i32 0
%t58 = load i8, ptr %t57
%t59 = zext i8 %t58 to i32
%t60 = icmp eq i32 %t59, 45
%t61 = zext i1 %t60 to i32
%t62 = icmp ne i32 %t61, 0
br i1 %t62, label %then30, label %else31
%t45 = load ptr, ptr %t23
%t46 = call i32 @fn_str_eq(ptr %t45, ptr @.str7404)
%t47 = icmp ne i32 %t46, 0
br i1 %t47, label %then27, label %else28
then27:
%t48 = load i32, ptr %t13
%t49 = add i32 %t48, 1
store i32 %t49, ptr %t13
%t50 = load i32, ptr %t13
%t51 = load i32, ptr @L_argc
%t52 = icmp slt i32 %t50, %t51
%t53 = zext i1 %t52 to i32
%t54 = icmp ne i32 %t53, 0
br i1 %t54, label %then30, label %ifend32
then30:
%t63 = load ptr, ptr %t23
%t64 = call ptr @fn_str_concat(ptr @.str7400, ptr %t63)
%t65 = call ptr @fn_str_concat(ptr %t64, ptr @.str7401)
store ptr %t65, ptr %t66
%t67 = load ptr, ptr @__stderrp
%t68 = load ptr, ptr %t66
%t69 = load ptr, ptr %t66
%t70 = call i64 @strlen(ptr %t69)
%t71 = trunc i64 %t70 to i32
%t72 = zext i32 %t71 to i64
%t73 = call i64 @fwrite(ptr %t68, i64 1, i64 %t72, ptr %t67)
%t74 = trunc i64 %t73 to i32
br label %ifend32
else31:
%t75 = load ptr, ptr %t23
store ptr %t75, ptr %t0
%t55 = load i32, ptr %t13
%t56 = load ptr, ptr @L_argv
%t57 = getelementptr ptr, ptr %t56, i32 %t55
%t58 = load ptr, ptr %t57
store ptr %t58, ptr %t1
br label %ifend32
ifend32:
br label %ifend29
else28:
%t59 = load ptr, ptr %t23
%t60 = getelementptr inbounds i8, ptr %t59, i32 0
%t61 = load i8, ptr %t60
%t62 = zext i8 %t61 to i32
%t63 = icmp eq i32 %t62, 45
%t64 = zext i1 %t63 to i32
%t65 = icmp ne i32 %t64, 0
br i1 %t65, label %then33, label %else34
then33:
%t66 = load ptr, ptr %t23
%t67 = call ptr @fn_str_concat(ptr @.str7405, ptr %t66)
%t68 = call ptr @fn_str_concat(ptr %t67, ptr @.str7406)
store ptr %t68, ptr %t69
%t70 = load ptr, ptr @__stderrp
%t71 = load ptr, ptr %t69
%t72 = load ptr, ptr %t69
%t73 = call i64 @strlen(ptr %t72)
%t74 = trunc i64 %t73 to i32
%t75 = zext i32 %t74 to i64
%t76 = call i64 @fwrite(ptr %t71, i64 1, i64 %t75, ptr %t70)
%t77 = trunc i64 %t76 to i32
br label %ifend35
else34:
%t78 = load ptr, ptr %t23
store ptr %t78, ptr %t0
br label %ifend35
ifend35:
br label %ifend29
ifend29:
br label %ifend26
ifend26:
br label %ifend23
@ -62361,242 +62505,242 @@ ifend14:
ifend11:
br label %ifend8
ifend8:
%t76 = load i32, ptr %t13
%t77 = add i32 %t76, 1
store i32 %t77, ptr %t13
%t79 = load i32, ptr %t13
%t80 = add i32 %t79, 1
store i32 %t80, ptr %t13
br label %wcond3
wend5:
%t78 = load ptr, ptr %t0
%t79 = icmp eq ptr %t78, null
%t80 = zext i1 %t79 to i32
%t81 = icmp ne i32 %t80, 0
br i1 %t81, label %then33, label %ifend35
then33:
call void @fn_die(ptr @.str7402)
br label %ifend35
ifend35:
%t82 = load ptr, ptr %t0
%t83 = call ptr @fn_read_file(ptr %t82)
store ptr %t83, ptr %t84
%t85 = load ptr, ptr %t84
%t86 = icmp eq ptr %t85, null
%t87 = zext i1 %t86 to i32
%t88 = icmp ne i32 %t87, 0
br i1 %t88, label %then36, label %ifend38
%t81 = load ptr, ptr %t0
%t82 = icmp eq ptr %t81, null
%t83 = zext i1 %t82 to i32
%t84 = icmp ne i32 %t83, 0
br i1 %t84, label %then36, label %ifend38
then36:
call void @fn_die(ptr @.str7403)
call void @fn_die(ptr @.str7407)
br label %ifend38
ifend38:
%t89 = load ptr, ptr %t0
%t90 = call ptr @fn_dir_of(ptr %t89)
store ptr %t90, ptr @g_cur_dir
%t91 = load ptr, ptr %t84
call void @fn_lex(ptr %t91)
call void @fn_parse_program()
%t92 = load i32, ptr %t4
%t93 = icmp ne i32 %t92, 0
br i1 %t93, label %then39, label %ifend41
%t85 = load ptr, ptr %t0
%t86 = call ptr @fn_read_file(ptr %t85)
store ptr %t86, ptr %t87
%t88 = load ptr, ptr %t87
%t89 = icmp eq ptr %t88, null
%t90 = zext i1 %t89 to i32
%t91 = icmp ne i32 %t90, 0
br i1 %t91, label %then39, label %ifend41
then39:
call void @fn_die(ptr @.str7408)
br label %ifend41
ifend41:
%t92 = load ptr, ptr %t0
%t93 = call ptr @fn_dir_of(ptr %t92)
store ptr %t93, ptr @g_cur_dir
%t94 = load ptr, ptr %t87
call void @fn_lex(ptr %t94)
call void @fn_parse_program()
%t95 = load i32, ptr %t4
%t96 = icmp ne i32 %t95, 0
br i1 %t96, label %then42, label %ifend44
then42:
call void @exit(i32 0)
unreachable
ifend41:
ifend44:
call void @fn_maybe_splice_runtime()
%t95 = load i32, ptr %t6
%t96 = icmp ne i32 %t95, 0
%t97 = zext i1 %t96 to i32
store i32 %t97, ptr %t94
br i1 %t96, label %scend43, label %sc42
sc42:
%t98 = load ptr, ptr %t1
%t99 = icmp ne ptr %t98, null
%t98 = load i32, ptr %t6
%t99 = icmp ne i32 %t98, 0
%t100 = zext i1 %t99 to i32
%t101 = icmp ne i32 %t100, 0
%t102 = zext i1 %t101 to i32
store i32 %t102, ptr %t94
br label %scend43
scend43:
%t103 = load i32, ptr %t94
store i32 %t103, ptr %t104
%t105 = load i32, ptr %t2
%t106 = icmp eq i32 %t105, 1
%t107 = zext i1 %t106 to i32
%t108 = icmp ne i32 %t107, 0
br i1 %t108, label %then44, label %else45
then44:
store i32 1, ptr @g_g_windowed
br label %ifend46
else45:
%t109 = load i32, ptr %t2
%t110 = icmp eq i32 %t109, 2
%t111 = zext i1 %t110 to i32
%t112 = icmp ne i32 %t111, 0
br i1 %t112, label %then47, label %else48
store i32 %t100, ptr %t97
br i1 %t99, label %scend46, label %sc45
sc45:
%t101 = load ptr, ptr %t1
%t102 = icmp ne ptr %t101, null
%t103 = zext i1 %t102 to i32
%t104 = icmp ne i32 %t103, 0
%t105 = zext i1 %t104 to i32
store i32 %t105, ptr %t97
br label %scend46
scend46:
%t106 = load i32, ptr %t97
store i32 %t106, ptr %t107
%t108 = load i32, ptr %t2
%t109 = icmp eq i32 %t108, 1
%t110 = zext i1 %t109 to i32
%t111 = icmp ne i32 %t110, 0
br i1 %t111, label %then47, label %else48
then47:
store i32 0, ptr @g_g_windowed
store i32 1, ptr @g_g_windowed
br label %ifend49
else48:
%t114 = load i32, ptr %t104
%t112 = load i32, ptr %t2
%t113 = icmp eq i32 %t112, 2
%t114 = zext i1 %t113 to i32
%t115 = icmp ne i32 %t114, 0
%t116 = zext i1 %t115 to i32
store i32 %t116, ptr %t113
br i1 %t115, label %sc50, label %scend51
sc50:
%t117 = call i32 @fn_has_systems()
br i1 %t115, label %then50, label %else51
then50:
store i32 0, ptr @g_g_windowed
br label %ifend52
else51:
%t117 = load i32, ptr %t107
%t118 = icmp ne i32 %t117, 0
%t119 = zext i1 %t118 to i32
store i32 %t119, ptr %t113
br label %scend51
scend51:
%t120 = load i32, ptr %t113
store i32 %t120, ptr @g_g_windowed
store i32 %t119, ptr %t116
br i1 %t118, label %sc53, label %scend54
sc53:
%t120 = call i32 @fn_has_systems()
%t121 = icmp ne i32 %t120, 0
%t122 = zext i1 %t121 to i32
store i32 %t122, ptr %t116
br label %scend54
scend54:
%t123 = load i32, ptr %t116
store i32 %t123, ptr @g_g_windowed
br label %ifend52
ifend52:
br label %ifend49
ifend49:
br label %ifend46
ifend46:
call void @fn_emit_program()
%t121 = load i32, ptr %t3
%t122 = icmp ne i32 %t121, 0
br i1 %t122, label %then52, label %ifend54
then52:
%t123 = load ptr, ptr %t1
%t124 = call i32 @fn_ir_flush(ptr %t123)
br label %ret
ifend54:
%t125 = load i32, ptr %t104
%t126 = icmp eq i32 %t125, 0
%t127 = zext i1 %t126 to i32
%t128 = icmp ne i32 %t127, 0
br i1 %t128, label %then55, label %ifend57
%t124 = load i32, ptr %t3
%t125 = icmp ne i32 %t124, 0
br i1 %t125, label %then55, label %ifend57
then55:
%t129 = call i32 @fn_ir_flush(ptr null)
%t126 = load ptr, ptr %t1
%t127 = call i32 @fn_ir_flush(ptr %t126)
br label %ret
ifend57:
%t131 = load i32, ptr %t6
%t132 = icmp ne i32 %t131, 0
%t133 = zext i1 %t132 to i32
store i32 %t133, ptr %t130
br i1 %t132, label %sc58, label %scend59
sc58:
%t134 = load ptr, ptr %t1
%t135 = icmp eq ptr %t134, null
%t128 = load i32, ptr %t107
%t129 = icmp eq i32 %t128, 0
%t130 = zext i1 %t129 to i32
%t131 = icmp ne i32 %t130, 0
br i1 %t131, label %then58, label %ifend60
then58:
%t132 = call i32 @fn_ir_flush(ptr null)
br label %ret
ifend60:
%t134 = load i32, ptr %t6
%t135 = icmp ne i32 %t134, 0
%t136 = zext i1 %t135 to i32
%t137 = icmp ne i32 %t136, 0
%t138 = zext i1 %t137 to i32
store i32 %t138, ptr %t130
br label %scend59
scend59:
%t139 = load i32, ptr %t130
store i32 %t136, ptr %t133
br i1 %t135, label %sc61, label %scend62
sc61:
%t137 = load ptr, ptr %t1
%t138 = icmp eq ptr %t137, null
%t139 = zext i1 %t138 to i32
%t140 = icmp ne i32 %t139, 0
br i1 %t140, label %then60, label %ifend62
then60:
%t141 = load ptr, ptr %t0
%t142 = call ptr @fn_base_name(ptr %t141)
%t143 = call ptr @fn_strip_ludic(ptr %t142)
%t144 = call ptr @fn_str_concat(ptr @.str7404, ptr %t143)
store ptr %t144, ptr %t1
br label %ifend62
ifend62:
%t145 = load ptr, ptr %t1
%t146 = call ptr @fn_dir_of(ptr %t145)
store ptr %t146, ptr %t147
%t148 = load ptr, ptr %t147
%t149 = call i64 @strlen(ptr %t148)
%t150 = trunc i64 %t149 to i32
%t151 = icmp sgt i32 %t150, 0
%t152 = zext i1 %t151 to i32
%t153 = icmp ne i32 %t152, 0
br i1 %t153, label %then63, label %ifend65
%t141 = zext i1 %t140 to i32
store i32 %t141, ptr %t133
br label %scend62
scend62:
%t142 = load i32, ptr %t133
%t143 = icmp ne i32 %t142, 0
br i1 %t143, label %then63, label %ifend65
then63:
%t154 = load ptr, ptr %t147
%t155 = call ptr @fn_str_concat(ptr @.str7405, ptr %t154)
%t156 = call i32 @system(ptr %t155)
%t144 = load ptr, ptr %t0
%t145 = call ptr @fn_base_name(ptr %t144)
%t146 = call ptr @fn_strip_ludic(ptr %t145)
%t147 = call ptr @fn_str_concat(ptr @.str7409, ptr %t146)
store ptr %t147, ptr %t1
br label %ifend65
ifend65:
%t157 = load ptr, ptr %t1
%t158 = call ptr @fn_str_concat(ptr %t157, ptr @.str7406)
store ptr %t158, ptr %t159
%t160 = load ptr, ptr %t159
%t161 = call i32 @fn_ir_flush(ptr %t160)
%t162 = icmp eq i32 %t161, 0
%t163 = zext i1 %t162 to i32
%t164 = icmp ne i32 %t163, 0
br i1 %t164, label %then66, label %ifend68
%t148 = load ptr, ptr %t1
%t149 = call ptr @fn_dir_of(ptr %t148)
store ptr %t149, ptr %t150
%t151 = load ptr, ptr %t150
%t152 = call i64 @strlen(ptr %t151)
%t153 = trunc i64 %t152 to i32
%t154 = icmp sgt i32 %t153, 0
%t155 = zext i1 %t154 to i32
%t156 = icmp ne i32 %t155, 0
br i1 %t156, label %then66, label %ifend68
then66:
call void @fn_die(ptr @.str7407)
%t157 = load ptr, ptr %t150
%t158 = call ptr @fn_str_concat(ptr @.str7410, ptr %t157)
%t159 = call i32 @system(ptr %t158)
br label %ifend68
ifend68:
%t165 = call ptr @fn_getenv_or(ptr @.str7408, ptr @.str7409)
store ptr %t165, ptr %t166
%t167 = load ptr, ptr @L_argv
%t168 = getelementptr ptr, ptr %t167, i32 0
%t169 = load ptr, ptr %t168
%t170 = call ptr @fn_dir_of(ptr %t169)
%t171 = call ptr @fn_getenv_or(ptr @.str7410, ptr %t170)
%t172 = call ptr @fn_ensure_slash(ptr %t171)
store ptr %t172, ptr %t173
%t174 = load ptr, ptr %t166
%t175 = call ptr @fn_str_concat(ptr %t174, ptr @.str7411)
%t176 = load ptr, ptr %t159
%t177 = call ptr @fn_str_concat(ptr %t175, ptr %t176)
store ptr %t177, ptr %t178
%t179 = load i32, ptr @g_g_windowed
%t180 = icmp ne i32 %t179, 0
br i1 %t180, label %then69, label %ifend71
%t160 = load ptr, ptr %t1
%t161 = call ptr @fn_str_concat(ptr %t160, ptr @.str7411)
store ptr %t161, ptr %t162
%t163 = load ptr, ptr %t162
%t164 = call i32 @fn_ir_flush(ptr %t163)
%t165 = icmp eq i32 %t164, 0
%t166 = zext i1 %t165 to i32
%t167 = icmp ne i32 %t166, 0
br i1 %t167, label %then69, label %ifend71
then69:
%t181 = load ptr, ptr %t173
%t182 = call ptr @fn_path_join(ptr %t181, ptr @.str7412)
store ptr %t182, ptr %t183
%t184 = load ptr, ptr %t178
%t185 = call ptr @fn_str_concat(ptr %t184, ptr @.str7413)
%t186 = load ptr, ptr %t183
%t187 = call ptr @fn_str_concat(ptr %t185, ptr %t186)
%t188 = call ptr @fn_str_concat(ptr %t187, ptr @.str7414)
store ptr %t188, ptr %t178
call void @fn_die(ptr @.str7412)
br label %ifend71
ifend71:
%t189 = load ptr, ptr %t178
%t190 = call ptr @fn_str_concat(ptr %t189, ptr @.str7415)
%t191 = load ptr, ptr %t1
%t192 = call ptr @fn_str_concat(ptr %t190, ptr %t191)
store ptr %t192, ptr %t178
%t193 = load ptr, ptr %t178
%t194 = call i32 @system(ptr %t193)
store i32 %t194, ptr %t195
%t196 = load i32, ptr %t5
%t197 = icmp eq i32 %t196, 0
%t198 = zext i1 %t197 to i32
%t199 = icmp ne i32 %t198, 0
br i1 %t199, label %then72, label %ifend74
%t168 = call ptr @fn_getenv_or(ptr @.str7413, ptr @.str7414)
store ptr %t168, ptr %t169
%t170 = load ptr, ptr @L_argv
%t171 = getelementptr ptr, ptr %t170, i32 0
%t172 = load ptr, ptr %t171
%t173 = call ptr @fn_dir_of(ptr %t172)
%t174 = call ptr @fn_getenv_or(ptr @.str7415, ptr %t173)
%t175 = call ptr @fn_ensure_slash(ptr %t174)
store ptr %t175, ptr %t176
%t177 = load ptr, ptr %t169
%t178 = call ptr @fn_str_concat(ptr %t177, ptr @.str7416)
%t179 = load ptr, ptr %t162
%t180 = call ptr @fn_str_concat(ptr %t178, ptr %t179)
store ptr %t180, ptr %t181
%t182 = load i32, ptr @g_g_windowed
%t183 = icmp ne i32 %t182, 0
br i1 %t183, label %then72, label %ifend74
then72:
%t200 = load ptr, ptr %t159
%t201 = call ptr @fn_str_concat(ptr @.str7416, ptr %t200)
%t202 = call i32 @system(ptr %t201)
%t184 = load ptr, ptr %t176
%t185 = call ptr @fn_path_join(ptr %t184, ptr @.str7417)
store ptr %t185, ptr %t186
%t187 = load ptr, ptr %t181
%t188 = call ptr @fn_str_concat(ptr %t187, ptr @.str7418)
%t189 = load ptr, ptr %t186
%t190 = call ptr @fn_str_concat(ptr %t188, ptr %t189)
%t191 = call ptr @fn_str_concat(ptr %t190, ptr @.str7419)
store ptr %t191, ptr %t181
br label %ifend74
ifend74:
%t203 = load i32, ptr %t195
%t204 = icmp ne i32 %t203, 0
%t205 = zext i1 %t204 to i32
%t206 = icmp ne i32 %t205, 0
br i1 %t206, label %then75, label %ifend77
%t192 = load ptr, ptr %t181
%t193 = call ptr @fn_str_concat(ptr %t192, ptr @.str7420)
%t194 = load ptr, ptr %t1
%t195 = call ptr @fn_str_concat(ptr %t193, ptr %t194)
store ptr %t195, ptr %t181
%t196 = load ptr, ptr %t181
%t197 = call i32 @system(ptr %t196)
store i32 %t197, ptr %t198
%t199 = load i32, ptr %t5
%t200 = icmp eq i32 %t199, 0
%t201 = zext i1 %t200 to i32
%t202 = icmp ne i32 %t201, 0
br i1 %t202, label %then75, label %ifend77
then75:
call void @fn_die(ptr @.str7417)
%t203 = load ptr, ptr %t162
%t204 = call ptr @fn_str_concat(ptr @.str7421, ptr %t203)
%t205 = call i32 @system(ptr %t204)
br label %ifend77
ifend77:
%t207 = load i32, ptr %t6
%t208 = icmp ne i32 %t207, 0
br i1 %t208, label %then78, label %ifend80
%t206 = load i32, ptr %t198
%t207 = icmp ne i32 %t206, 0
%t208 = zext i1 %t207 to i32
%t209 = icmp ne i32 %t208, 0
br i1 %t209, label %then78, label %ifend80
then78:
%t209 = load ptr, ptr %t1
%t210 = call i32 @system(ptr %t209)
store i32 %t210, ptr %t211
%t212 = load i32, ptr %t211
%t213 = lshr i32 %t212, 8
%t214 = and i32 %t213, 255
call void @exit(i32 %t214)
unreachable
call void @fn_die(ptr @.str7422)
br label %ifend80
ifend80:
%t210 = load i32, ptr %t6
%t211 = icmp ne i32 %t210, 0
br i1 %t211, label %then81, label %ifend83
then81:
%t212 = load ptr, ptr %t1
%t213 = call i32 @system(ptr %t212)
store i32 %t213, ptr %t214
%t215 = load i32, ptr %t214
%t216 = lshr i32 %t215, 8
%t217 = and i32 %t216, 255
call void @exit(i32 %t217)
unreachable
ifend83:
br label %ret
ret:
%t215 = load i32, ptr %retval
ret i32 %t215
%t218 = load i32, ptr %retval
ret i32 %t218
}

View file

@ -51,6 +51,24 @@ function die(msg: pointer) -> void {
exit(1)
}
# strip trailing newline/carriage-return/space (the VERSION file's trailing \n)
function chomp(s: pointer) -> pointer {
var n = len(s)
while (n > 0) and ((s[n - 1] == 10) or (s[n - 1] == 13) or (s[n - 1] == 32)) { n = n - 1 }
return s[0..n]
}
# print "ludic <version>" and exit. The version is read at runtime from the
# VERSION file next to LUDIC_HOME (the single source of truth a release bumps),
# so a version change never requires reseeding the compiler.
function show_version() -> void {
let home = ensure_slash(getenv_or("LUDIC_HOME", dir_of(arg(0))))
let v = read_file(path_join(home, "VERSION"))
if (v == null) { print("ludic (version unknown)") }
else { print(`ludic {chomp(v)}`) }
exit(0)
}
entry {
var path = null
var out = null
@ -66,7 +84,8 @@ entry {
var ai = 1
while ai < arg_count() {
let a = arg(ai)
if (a == ("--windowed")) { want = 1 }
if (a == ("--version")) { show_version() }
else { if (a == ("--windowed")) { want = 1 }
else { if (a == ("--headless")) { want = 2 }
else { if (a == ("--emit-llvm")) { emit_ir = true }
else { if (a == ("--fmt")) { fmt = true }
@ -80,7 +99,7 @@ entry {
let m = `ludicc: ignoring unknown flag {a}\n`
file_write(file_stderr(), m, len(m))
} else { path = a }
} } } } } } }
} } } } } } } }
ai = ai + 1
}

107
tools/ci/forgejo_release.py Normal file
View file

@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Create a Forgejo release and attach build artifacts.
forgejo_release.py <tag> <body-file> <assets-dir>
Reads the access token from $FORGEJO_TOKEN. `x release --publish` calls this
after it has pushed the tag; it is the one piece of the release flow that speaks
HTTP, which the Ludic task runner cannot do yet (a native Http client is issue
#6). Python stdlib only — no third-party deps — matching the docgen tooling.
Idempotent-ish: if the release for <tag> already exists it is reused, and each
asset in <assets-dir> is uploaded (a duplicate name is skipped, not fatal).
"""
import json
import os
import sys
import urllib.request
import urllib.error
API = "https://git.workshopsoft.io/api/v1/repos/workshopsoft/ludic"
def token():
t = os.environ.get("FORGEJO_TOKEN", "").strip()
if not t:
sys.exit("forgejo_release: FORGEJO_TOKEN is not set")
return t
def api(method, path, tok, data=None, headers=None, raw=False):
url = API + path
h = {"Authorization": "token " + tok}
body = None
if data is not None and not raw:
h["Content-Type"] = "application/json"
body = json.dumps(data).encode()
elif raw:
body = data
h.update(headers or {})
req = urllib.request.Request(url, data=body, headers=h, method=method)
with urllib.request.urlopen(req) as r:
return r.getcode(), json.loads(r.read().decode() or "null")
def get_release_by_tag(tag, tok):
try:
_, rel = api("GET", "/releases/tags/" + tag, tok)
return rel
except urllib.error.HTTPError as e:
if e.code == 404:
return None
raise
def main():
if len(sys.argv) != 4:
sys.exit(__doc__)
tag, body_file, assets_dir = sys.argv[1], sys.argv[2], sys.argv[3]
tok = token()
body = ""
if os.path.exists(body_file):
with open(body_file, encoding="utf-8") as fh:
body = fh.read()
rel = get_release_by_tag(tag, tok)
if rel is None:
_, rel = api("POST", "/releases", tok, {
"tag_name": tag,
"name": tag,
"body": body,
"draft": False,
"prerelease": tag.startswith("v0."),
})
print("created release", tag, "id", rel["id"])
else:
print("release", tag, "already exists (id %s); uploading assets" % rel["id"])
rid = rel["id"]
existing = {a["name"] for a in (rel.get("assets") or [])}
if os.path.isdir(assets_dir):
for name in sorted(os.listdir(assets_dir)):
if name in existing:
print(" skip (exists):", name)
continue
path = os.path.join(assets_dir, name)
if not os.path.isfile(path):
continue
with open(path, "rb") as fh:
blob = fh.read()
boundary = "----ludicrelease7f3b2a"
payload = (
("--%s\r\n" % boundary)
+ ('Content-Disposition: form-data; name="attachment"; filename="%s"\r\n' % name)
+ "Content-Type: application/octet-stream\r\n\r\n"
).encode() + blob + ("\r\n--%s--\r\n" % boundary).encode()
code, _ = api(
"POST", "/releases/%s/assets?name=%s" % (rid, name), tok,
data=payload, raw=True,
headers={"Content-Type": "multipart/form-data; boundary=" + boundary},
)
print(" uploaded:", name, "(HTTP %d)" % code)
print("done:", API + "/releases/tag/" + tag)
if __name__ == "__main__":
main()

View file

@ -17,6 +17,7 @@ program X {
import "tools.ludic"
import "selfhost_test.ludic"
import "test.ludic"
import "release.ludic"
function usage() -> void {
print("x — the Ludic task runner (run from the repository root)")
@ -35,6 +36,11 @@ program X {
print(" x test-tools the editor-toolchain suite")
print(" x golden regenerate selfhost/golden/renders.sha256 (review with git diff)")
print("")
print("release:")
print(" x version print the toolchain version (ludicc --version)")
print(" x release [major|minor|patch] [--publish]")
print(" cut a release: CHANGELOG + VERSION bump + tag (+ Forgejo release)")
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)")
@ -75,6 +81,8 @@ program X {
if (arg_count() < 5) { err("usage: x game-build <ludicc> <game.ludic> <out>\n"); exit(1) }
exit(cmd_game_build(arg(2), arg(3), arg(4)))
}
if (cmd == "version") or (cmd == "--version") or (cmd == "-v") { exit(cmd_version()) }
if (cmd == "release") { exit(cmd_release()) }
if (cmd == "help") or (cmd == "--help") or (cmd == "-h") { usage(); exit(0) }
err(`x: unknown command '{cmd}'\n`)

148
tools/x/release.ludic Normal file
View file

@ -0,0 +1,148 @@
# 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,
# 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] --publish ...then push main + the tag and create a
# Forgejo release with source + toolchain tarballs.
# Needs FORGEJO_TOKEN in the environment.
#
# The scheme is SemVer. VERSION is the single source of truth (ludicc --version
# reads it at runtime), so a bump touches one file and never reseeds the
# compiler. Changesets live as one small Markdown file per change under changes/
# (see changes/README.md); a release consumes them into a CHANGELOG.md section.
# the current version string, or a fallback when VERSION is absent
function read_version_or(dflt: pointer) -> pointer {
let v = capture_line("cat VERSION 2>/dev/null")
if (v == "") { return dflt }
return v
}
# true when there is at least one pending changeset (changes/*.md, minus README)
function has_changesets() -> bool {
return capture_line("ls changes/*.md 2>/dev/null | grep -v '/README.md' | head -1") != ""
}
# the highest bump level requested across the pending changesets ("" if none).
# README.md is excluded — its format example carries a literal `bump:` line that
# must not count as a real changeset.
function highest_bump() -> pointer {
if shq("grep -rhqE '^bump:[[:space:]]*major' --include='*.md' --exclude='README.md' changes 2>/dev/null") { return "major" }
if shq("grep -rhqE '^bump:[[:space:]]*minor' --include='*.md' --exclude='README.md' changes 2>/dev/null") { return "minor" }
if shq("grep -rhqE '^bump:[[:space:]]*patch' --include='*.md' --exclude='README.md' changes 2>/dev/null") { return "patch" }
return ""
}
# cur + a SemVer bump of `level` -> the next version string
function compute_next(cur: pointer, level: pointer) -> pointer {
let maj = capture_line(`printf '%s' '{cur}' | cut -d. -f1`)
let min = capture_line(`printf '%s' '{cur}' | cut -d. -f2`)
let pat = capture_line(`printf '%s' '{cur}' | cut -d. -f3`)
if (level == "major") { let m2 = capture_line(`expr {maj} + 1`); return `{m2}.0.0` }
if (level == "minor") { let n2 = capture_line(`expr {min} + 1`); return `{maj}.{n2}.0` }
let p2 = capture_line(`expr {pat} + 1`)
return `{maj}.{min}.{p2}`
}
# x 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") {
run("LUDIC_HOME=. bin/ludicc --version")
return 0
}
print(`ludic {read_version_or("(version unknown)")}`)
return 0
}
# assemble the CHANGELOG.md section for `ver` from the pending changesets into
# /tmp/x_rel_section.md (header + one bullet per changeset, sorted by type).
function build_section(ver: pointer) -> void {
let date = capture_line("date +%Y-%m-%d")
run(`printf '## v%s — %s\n\n' '{ver}' '{date}' > /tmp/x_rel_section.md`)
# one bullet per changeset: "- **<type>**: <body>" (body = the non-header text)
run("( for f in changes/*.md; do case \"$f\" in */README.md) continue;; esac; t=$(sed -n 's/^type:[[:space:]]*//p' \"$f\" | head -1); b=$(grep -vE '^(type|bump):' \"$f\" | sed '/^[[:space:]]*$/d' | tr '\\n' ' ' | sed 's/[[:space:]]*$//'); printf -- '- **%s**: %s\\n' \"$t\" \"$b\"; done | sort ) >> /tmp/x_rel_section.md")
run("printf '\n' >> /tmp/x_rel_section.md")
}
# prepend /tmp/x_rel_section.md into CHANGELOG.md, above the first existing
# 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")
}
let ln = capture_line("grep -n '^## ' CHANGELOG.md | head -1 | cut -d: -f1")
if (ln == "") {
run("cat CHANGELOG.md /tmp/x_rel_section.md > /tmp/x_rel_new.md")
} else {
run(`head -n $(expr {ln} - 1) CHANGELOG.md > /tmp/x_rel_new.md`)
run("cat /tmp/x_rel_section.md >> /tmp/x_rel_new.md")
run(`tail -n +{ln} CHANGELOG.md >> /tmp/x_rel_new.md`)
}
run("cp /tmp/x_rel_new.md CHANGELOG.md")
}
# push main + the tag and create the Forgejo release with build artifacts.
function publish_release(ver: pointer) -> int {
let tok = getenv_or("FORGEJO_TOKEN", "")
if (tok == "") { err("release --publish: set FORGEJO_TOKEN (a Forgejo access token)\n"); return 1 }
if not shq("git push origin HEAD") { err("release: git push (main) failed\n"); return 1 }
if not shq(`git push origin v{ver}`) { err("release: git push (tag) failed\n"); return 1 }
# artifacts: a reproducible source+seed tarball, and the built macOS toolchain.
run("mkdir -p dist")
run(`git archive --format=tar.gz --prefix=ludic-{ver}/ -o dist/ludic-{ver}-src.tar.gz v{ver}`)
if is_exec("bin/ludicc") {
let plat = capture_line("uname -s | tr '[:upper:]' '[:lower:]'")
let arch = capture_line("uname -m")
run(`tar -czf dist/ludic-{ver}-{plat}-{arch}.tar.gz bin selfhost/ludicc.seed.ll VERSION`)
}
# the changelog section written by build_section is the release body
if not shq(`FORGEJO_TOKEN='{tok}' python3 tools/ci/forgejo_release.py v{ver} /tmp/x_rel_section.md dist`) {
err("release: creating the Forgejo release failed\n"); return 1
}
return 0
}
function cmd_release() -> int {
# parse args: an optional level positional, and a --publish flag
var level = ""
var publish = false
var ai = 2
while ai < arg_count() {
let a = arg(ai)
if (a == "--publish") { publish = true }
else { if (a == "major") or (a == "minor") or (a == "patch") { level = a }
else { err(`release: unknown argument {a}\n`); return 1 } }
ai = ai + 1
}
if not has_changesets() {
err("release: no changesets under changes/ — add one (see changes/README.md)\n"); return 1
}
if (level == "") { level = highest_bump() }
if (level == "") { err("release: no bump: level in any changeset\n"); return 1 }
let cur = read_version_or("0.0.0")
let ver = compute_next(cur, level)
print(`releasing v{ver} ({level} bump from {cur})`)
build_section(ver)
prepend_changelog()
if not write_file("VERSION", `{ver}\n`) { err("release: cannot write VERSION\n"); return 1 }
run("rm -f $(ls changes/*.md | grep -v '/README.md')")
# stage only the release artifacts — never a blanket `git add -A`, which would
# sweep unrelated in-flight edits (this tree is worked on concurrently).
if not shq("git add VERSION CHANGELOG.md changes") { err("release: git add failed\n"); return 1 }
if not shq(`git commit -q -m 'chore(release): v{ver}'`) { err("release: git commit failed\n"); return 1 }
if not shq(`git tag v{ver}`) { err("release: git tag failed (already exists?)\n"); return 1 }
print(` committed + tagged v{ver}`)
if publish { return publish_release(ver) }
print(` local release ready. publish with: FORGEJO_TOKEN=… x release {level} --publish`)
print(` (or push: git push origin HEAD && git push origin v{ver})`)
return 0
}