Tagged releases
Pushing a v* tag builds the workspace's programs for four platforms and attaches one archive per platform, plus a SHA256SUMS manifest, to that tag's GitHub release. GitHub releases are the only distribution channel. The archives carry compiled programs and the project's own text files only: no game data is packaged, and no step of the pipeline reads a disc image.
At a glance
- Workflow
.github/workflows/release.yml: tag trigger, test gate, two build jobs, publish- Toolchains
scripts/ci/setup-cross-toolchain.sh: the cross toolchain per target, idempotent- Build and package
scripts/ci/release-build.sh: build, stage, archive, checksum- For players
- Which archive to download and what to open: desktop apps
Cutting a release
git tag v0.2.0
git push origin v0.2.0
That is the whole procedure. The workflow runs the test gate, builds every target, and creates the release for the tag if it does not exist yet.
publish edits the release, after the test gate and both build jobs, so a release never appears with a platform missing and SHA256SUMS always covers every archive. The macOS build runs beside the test gate rather than after it; nothing it builds is published until the gate is green.- A tag containing
-(sayv0.2.0-rc1) is marked as a prerelease. - A dry run publishes nothing. The
workflow_dispatchtrigger (gh workflow run release.yml -f tag=<ref>) runs every job and leaves the archives as workflow artifacts unless itspublishinput is checked. A dry run takes any ref, a branch included; publishing takes only an existing tag, since the release action would otherwise mint a new one.
Target matrix
The self-hosted runner is arm64 Linux, so every other target is a cross-compile - including x86_64 Linux.
| Target | Toolchain | Top-level programs |
|---|---|---|
aarch64-unknown-linux-gnu | Native | Legend of Legaia, Legaia Explorer |
x86_64-pc-windows-gnu | mingw-w64 cross | Legend of Legaia.exe, Legaia Explorer.exe (both GUI-subsystem) |
x86_64-unknown-linux-gnu | cargo-zigbuild + an amd64 ALSA / libudev sysroot, glibc pinned to 2.28 | As on arm64 Linux |
universal-apple-darwin | Native on a macOS runner; the two slices fused by lipo | Legend of Legaia.app, Legaia Explorer.app |
Every target also carries every command-line program. The per-target list is declared in release-build.sh, which fails when an expected program is missing from the build, so a target cannot quietly drop one.
Archive layout
The top folder holds only what a player should open; every command-line program sits one level down.
legaia-tools-<version>-<target>/
Legend of Legaia (.exe on Windows, .app on macOS)
Legaia Explorer (.exe on Windows, .app on macOS)
README-PLAY.txt
LICENSE LICENSE-MIT THIRD-PARTY-LICENSES.txt
packs/
sample.lxpack the Sample Expansion Pack
command-line-tools/
README.txt every tool, one line each
legaia-engine the engine: launcher, play-window, every subcommand
asset-viewer legaia-extract legaia-patcher asset tim tmd vab ...
Legend of Legaia on Linux and Windows is a small launcher stub that starts command-line-tools/legaia-engine with the same arguments; a lone file argument (from "Open with", or a drop on the icon) becomes --open <file>. One copy of the engine serves both the game and the command line. The stub and Explorer's Play button find the engine by walking up to four folders above the running program, which also covers a plain cargo build and a macOS bundle.
The bundled pack. packs/sample.lxpack is the Sample Expansion Pack, bundled at staging time by the format's one deterministic writer, which refuses a pack that fails validation - the no-disc-bytes rule among the checks. What ships is disc coordinates plus the pack's own art and audio.
Icons and the per-platform details
The game and Explorer carry original icons drawn by a script in crates/app-theme/icons/; no game asset is drawn from, and the outputs are committed so a release never runs the script. Windows executables embed the icon through windres; the macOS bundles name it in Info.plist; the engine and Explorer also set a window icon at runtime. release-build.sh fails unless all three Windows executables carry an icon group.
- macOS
- Built on a GitHub-hosted runner, since Apple's SDK cannot be redistributed onto a Linux one. Both slices are fused into one file per program, deployment target macOS 11. The bundles are ad-hoc signed - arm64 macOS refuses to run unsigned code - but not notarised, so Gatekeeper asks the player to confirm the first launch;
README-PLAY.txtgives the steps. - Windows console
legaia-engine.exeis a console program and keeps the terminal contract every subcommand relies on. The top-levelLegend of Legaia.exeis a GUI-subsystem stub that starts it with no console window and reports a failure in a message box. The build reads the PE headers and fails unless the stub is GUI and the engine console.- Legaia Explorer
- A GUI program on every target with no command-line contract, so it is GUI-subsystem itself on Windows and needs no stub. It draws through OpenGL (egui's glow backend) and links no wgpu.
- Why
-gnuon Windows - mingw-w64 is an apt package that emits x86_64 PE objects on an arm64 host, and the whole workspace crosses with it, GUI included. The MSVC ABI would need a downloaded Microsoft SDK.
- Why an ALSA sysroot
- The engine's audio and controller crates bind
libasoundandlibudevthroughpkg-config, which needs x86_64 copies to cross-link. Arm64 Ubuntu's package mirror publishes no amd64 packages, so the setup script fetches the four.debs from the main archive and unpacks them into a cache - no root, no apt sources touched. The glibc 2.28 pin survives, and the build checks it in the symbol table.
Artifacts
legaia-tools-<version>-aarch64-unknown-linux-gnu.tar.gz
legaia-tools-<version>-x86_64-pc-windows-gnu.zip
legaia-tools-<version>-x86_64-unknown-linux-gnu.tar.gz
legaia-tools-<version>-universal-apple-darwin.tar.gz
SHA256SUMS
<version> is the tag without its leading v. Each archive extracts to one top-level folder. Besides the programs and the license files (the project is MIT OR Unlicense) it carries three generated text files: README-PLAY.txt (what to open, bring your own disc image, controls, where settings and saves live), command-line-tools/README.txt (one line per tool), and THIRD-PARTY-LICENSES.txt.
Third-party notices are written at staging time by scripts/ci/third-party-licenses.py from the crates the programs actually link (cargo tree per target, so features resolve as in the build), with each distinct license text once. The release fails when a linked crate declares no license or a license has no text anywhere.
Where the build host can run the result (the native Linux row and macOS), the build starts the staged engine, game entry and Explorer with --version, so a program that cannot start fails the release rather than a player. To check a download:
sha256sum -c SHA256SUMS --ignore-missingRunner provisioning
| Requirement | How it arrives | Root? |
|---|---|---|
mingw-w64 | sudo apt install mingw-w64 | Yes, once |
| Rust target std | rustup target add, automatic | No |
zig | pip wheel into a cache venv, automatic | No |
cargo-zigbuild | cargo install, automatic | No |
| amd64 ALSA / libudev sysroot | .debs unpacked into the cache, automatic | No |
libasound2-dev, libudev-dev | Already present for the native build | Yes, once |
Root-free tooling is cached under $LEGAIA_RELEASE_CACHE (default ~/.cache/legaia-release); deleting it forces a clean reinstall. The macOS job names its hosted image by version; when GitHub retires that image the job fails before its first step, and the fix is to move runs-on to a supported label and rehearse with a build-only dispatch before tagging.
Interaction with main-ci.yml
main-ci.yml does not trigger on tags: two workflows on one tag would contend for the single self-hosted runner. The release stays test-gated anyway, because its verify job runs the same format, lint and test gates, and publish needs it. Disc-gated tests skip there exactly as in CI.
Local rehearsal
Both scripts run outside CI, the fastest way to check a matrix change before tagging:
scripts/ci/setup-cross-toolchain.sh x86_64-pc-windows-gnu
scripts/ci/release-build.sh 0.0.0-test x86_64-pc-windows-gnu
On a Mac the same pair builds the universal-apple-darwin row. The archive and its .sha256 land in target/dist/, inside the gitignored target/.