From fed80f215228fc7023130c7868bc24404c6bc1d8 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 30 Aug 2026 23:46:59 +0300 Subject: [PATCH] feat(release): SemVer + ludicc --version, changesets, and x release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CONTRIBUTING.md | 21 + README.md | 5 +- VERSION | 1 + changes/0001-self-hosted-toolchain.md | 3 + changes/0002-namespaced-stdlib.md | 3 + changes/0003-native-2d-backend.md | 3 + changes/0004-editor-tooling.md | 3 + changes/0005-ci.md | 3 + changes/0006-versioning.md | 3 + changes/README.md | 25 + selfhost/ludicc.seed.ll | 724 +++++++++++++++----------- selfhost/main.ludic | 23 +- tools/ci/forgejo_release.py | 107 ++++ tools/x/main.ludic | 8 + tools/x/release.ludic | 148 ++++++ 15 files changed, 787 insertions(+), 293 deletions(-) create mode 100644 VERSION create mode 100644 changes/0001-self-hosted-toolchain.md create mode 100644 changes/0002-namespaced-stdlib.md create mode 100644 changes/0003-native-2d-backend.md create mode 100644 changes/0004-editor-tooling.md create mode 100644 changes/0005-ci.md create mode 100644 changes/0006-versioning.md create mode 100644 changes/README.md create mode 100644 tools/ci/forgejo_release.py create mode 100644 tools/x/release.ludic diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c76d543..0167418b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/README.md b/README.md index c14260a9..5b77bff0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..77d6f4ca --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.0 diff --git a/changes/0001-self-hosted-toolchain.md b/changes/0001-self-hosted-toolchain.md new file mode 100644 index 00000000..5c21d5d1 --- /dev/null +++ b/changes/0001-self-hosted-toolchain.md @@ -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. diff --git a/changes/0002-namespaced-stdlib.md b/changes/0002-namespaced-stdlib.md new file mode 100644 index 00000000..957927c4 --- /dev/null +++ b/changes/0002-namespaced-stdlib.md @@ -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. diff --git a/changes/0003-native-2d-backend.md b/changes/0003-native-2d-backend.md new file mode 100644 index 00000000..f1e281db --- /dev/null +++ b/changes/0003-native-2d-backend.md @@ -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. diff --git a/changes/0004-editor-tooling.md b/changes/0004-editor-tooling.md new file mode 100644 index 00000000..86af912e --- /dev/null +++ b/changes/0004-editor-tooling.md @@ -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. diff --git a/changes/0005-ci.md b/changes/0005-ci.md new file mode 100644 index 00000000..24ddd426 --- /dev/null +++ b/changes/0005-ci.md @@ -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. diff --git a/changes/0006-versioning.md b/changes/0006-versioning.md new file mode 100644 index 00000000..0d34f56b --- /dev/null +++ b/changes/0006-versioning.md @@ -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. diff --git a/changes/README.md b/changes/README.md new file mode 100644 index 00000000..bee24a4a --- /dev/null +++ b/changes/README.md @@ -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 ` + 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. diff --git a/selfhost/ludicc.seed.ll b/selfhost/ludicc.seed.ll index cc350de8..1adb0870 100644 --- a/selfhost/ludicc.seed.ll +++ b/selfhost/ludicc.seed.ll @@ -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 [-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 [-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 } diff --git a/selfhost/main.ludic b/selfhost/main.ludic index 60183d42..78ebbfb9 100644 --- a/selfhost/main.ludic +++ b/selfhost/main.ludic @@ -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 " 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 } diff --git a/tools/ci/forgejo_release.py b/tools/ci/forgejo_release.py new file mode 100644 index 00000000..2c4d5b35 --- /dev/null +++ b/tools/ci/forgejo_release.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Create a Forgejo release and attach build artifacts. + + forgejo_release.py + +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 already exists it is reused, and each +asset in 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() diff --git a/tools/x/main.ludic b/tools/x/main.ludic index 18292b22..3db83115 100644 --- a/tools/x/main.ludic +++ b/tools/x/main.ludic @@ -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 \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`) diff --git a/tools/x/release.ludic b/tools/x/release.ludic new file mode 100644 index 00000000..8de1e1b9 --- /dev/null +++ b/tools/x/release.ludic @@ -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: "- ****: " (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 +}