Tooling

August 17, 2026 · View on GitHub

Index of the external tools this repository expects, and which of them you actually need. Companion to scripts/README.md, which catalogues the scripts we ship; this doc covers the tools they assume you have.

Run the check instead of reading the tables:

./scripts/dev/check-tooling.sh     # or: make check-tooling

It probes every tool below, reports what is missing, and exits non-zero only when the clone genuinely cannot build. --tier 1 narrows it, --quiet shows only rows needing attention, --strict also fails on tier 2.

How the tiers work

Tools are grouped by what breaks without them, not by how useful they are. "Optional" would put the Android NDK (a hard requirement for one lane) in the same bucket as actionlint (never required by anything), so it isn't used here.

TierTestConsequence
1 — Build & runno binary without itcheck-tooling.sh exits non-zero
2 — Pass reviewCI fails you without itwarning; --strict exits non-zero
3 — Per lanerequired only if you work on that lanereported
4 — Faster, not requirednothing breaksreported

Two rules keep the tables from going stale, and both matter more than the tables themselves:

  • No version literals here. Every floor and pin lives in the file that owns it, named in the Version owner column. check-tooling.sh parses those same files, so the doc, the script, and the build cannot disagree about a version — there is only ever one copy.
  • No probe commands here. How a tool is detected lives in check-tooling.sh. A second copy in prose is a second thing to forget.

Tier 1 — build & run

Without all five there is no lba2cc binary. The root README states the same set for players building from source.

ToolNeeded forVersion ownerInstall
CMakeevery build path../CMakeLists.txt (cmake_minimum_required)distro package; the ubuntu-latest runner already satisfies the floor
Ninjaall CMakePresets.json presets and make build use the Ninja generatorapt install ninja-build, brew install ninja, pacman -S mingw-w64-ucrt-x86_64-ninja
C/C++ compilerC++98 dialect; see CODESTYLE.mdGCC or Clang. MSVC is not supported
SDL3find_package(SDL3 CONFIG); shared by default, static for release packagingsee note belowbrew install sdl3, pacman -S mingw-w64-ucrt-x86_64-SDL3; most distros need a source build
gitthe format and clang-tidy scripts enumerate files with git ls-filesdistro package

SDL3 has no single version owner. The release-build pin is duplicated across four code sites — docker/Dockerfile.test, reusable-build-android.yml, build-linux-tarball.sh, and build-sdl3-android.sh — and the Dockerfile comment asks you to keep the four in sync when bumping. Local development builds against whatever SDL3 your system provides and does not care about the pin; only bundled and containerised builds do.

GNU Make is not in the table: it only drives the Makefile shortcuts, and plain cmake works without it.

On Windows all five hang off the MSYS2 environment. In the plain MSYS shell, cmake, ninja and gcc are simply not on PATH, so every row above fails at once and the cause is invisible. check-tooling.sh reports the active MSYSTEM first for exactly that reason — it should say UCRT64. See WINDOWS.md.

Tier 2 — pass review

CI enforces these, so a gap here means a red PR rather than a broken clone. Most rows are conditional on what your change touches — C/C++, shell, Python, workflows, or LIB386/. Lychee is the exception: docs-links.yml is deliberately not path-filtered, so it gates every PR including a docs-only one.

ToolNeeded forVersion ownerInstall
clang-formatC/C++ — format.yml, make format-check, the optional pre-commit hookclang-format-select.sh (CLANG_FORMAT_MAJOR)the format scripts print the exact package name for the pinned major when they refuse to run — check-format.sh
lycheeany change — docs-links.yml checks every relative link and #anchor in tracked markdown, via make docs-linksdocs-links.yml (LYCHEE_VERSION)cargo install lychee, or the release binary CI downloads
shellcheckshell — the shellcheck job over every tracked *.shlint.yml (SHELLCHECK_VERSION)distro package, or the release tarball CI uses
ruffPython — the ruff job over every tracked *.pylint.yml (RUFF_VERSION)pipx install ruff
actionlintworkflows — the actionlint job, which also shells out to shellcheck for every run: bodylint.yml (ACTIONLINT_VERSION)release binary
Python 3filter-format-files.py gates the format check; also the save probes and corpus harnessdistro package
DockerLIB386/run_tests_docker.sh, the ASM equivalence suiteDocker Engine or Docker Desktop

The linters take their rule selection from checked-in config — .shellcheckrc, ruff.toml — so a local run matches CI. The one exception is shellcheck severity, which it only accepts on the command line: CI passes -S warning, so add that locally or you will see info and style findings the gate ignores.

Without lychee, make docs-links still runs its second half — a grep for docs/<name>.md paths named by bare path in source, tests and CMake, which a link checker cannot see — and skips the link half with a warning rather than failing. Easy to mistake for a pass.

The format scripts refuse to run a non-matching major rather than silently disagreeing with CI, so install the exact version the owner file names. Only stdlib Python is needed here; Pillow is tier 4.

The package name is not guessable on MSYS2, and the scripts' own error message covers Debian and macOS only: there it is pacman -S mingw-w64-ucrt-x86_64-clang-tools-extra, and the binary is unversioned (clang-format, never clang-format-N). check-tooling.sh prints the right one for your platform.

Two things about the container:

  • Presence is not the question. A Docker Desktop install can leave a docker on PATH in WSL that resolves fine and then fails at exec, which verify-release.sh documents and check-tooling.sh mirrors by calling docker info. run_tests_docker.sh checks the same thing before it does any work, because otherwise a setup problem surfaces as a confusing failure from the first real docker call.

    On WSL the usual cause is that Docker Desktop is running fine and its WSL integration is off for your distro. /mnt/wsl/docker-desktop/cli-tools is mounted whatever the setting, so docker resolves and prints client info, but no /var/run/docker.sock is created and the daemon is unreachable:

    failed to connect to the docker API at unix:///var/run/docker.sock
    

    Confirm the daemon itself is healthy through the Windows binary — "/mnt/c/Program Files/Docker/Docker/resources/bin/docker.exe" info — then fix it in Docker Desktop under Settings > Resources > WSL Integration, enabling your distro. A working integration leaves /var/run/docker.sock owned by the docker group. Pointing DOCKER_HOST at the proxy socket under shared-sockets/host-services/ looks tempting and is a dead end: it is root:root, and being in the docker group does not reach it.

  • Podman is not a drop-in. Both scripts invoke docker by name, so podman needs a shim or alias on PATH. The commands themselves are compatible — run_tests_docker.sh only uses images -q, build --platform and run --rm --platform, all of which podman accepts. The harder case is verify-release.sh, which runs --privileged tonistiigi/binfmt to register arm64 emulation; rootless podman cannot do that, and the script already falls back to skipping the arm64 artifact. Neither path is verified against podman, so treat it as unsupported-but-probably-fine rather than tested.

Everything the suite needs inside the image — UASM, 32-bit multilib, both SDL3 builds — is pinned and fetched by docker/Dockerfile.test, not by you. That is the point of running it in a container.

Tier 3 — per lane

Hard requirements with a narrow audience. Nothing here matters until you work on that specific lane.

ASM equivalence on the host

The container path above is the supported one. Building the suite natively needs all three, and check-tooling.sh expects most hosts to lack the last two.

ToolNeeded forVersion ownerInstall
objcopyLBA2_BUILD_ASM_EQUIV_TESTS=ON (default ON when tests are enabled)binutils
32-bit runtimelinking the 32-bit equivalence targetsgcc-multilib g++-multilib
UASMENABLE_ASM=ON onlydocker/Dockerfile.test (ARG UASM_VERSION)the image fetches it; see TESTING.md

make test sets LBA2_BUILD_ASM_EQUIV_TESTS=OFF, so none of this is needed for the host-only pass.

Releasing

Maintainer lane; see RELEASING.md.

ToolNeeded forVersion ownerInstall
gh (authenticated)release upload and edit, verify-release.shcli.github.com
git-cliffgit cliff --prepend for CHANGELOG; not needed for a first releasecargo install git-cliff or a release binary
tarLinux tarball bundling, artifact verificationdistro package

Platform artifacts

One bundler per platform, each with its own host requirement.

ToolNeeded forVersion ownerInstall
zip or Python 3bundle-windows.sh prefers zip, falls back to the stdlib zipfiledistro package
mingw-w64the cross_linux2win preset and cmake/mingw-w64-i686.cmake; Unix hosts only, so the probe skips it on Windowsapt install mingw-w64, pacman -S mingw-w64-gcc
MSYS2 UCRT64native Windows builds — the recommended local path, bit-for-bit CI's toolchain. Probed via MSYSTEM, see the tier 1 noteWINDOWS.md
hdiutil, xcrunDMG creation; bundle-macos.sh hard-requires a macOS hostXcode command-line tools

The AppImage is the exception: make-appimage.sh calls pacman, get-debloated-pkgs, and quick-sharun, and runs inside the ghcr.io/pkgforge-dev/archlinux container in CI. It has no local dry-run on a non-Arch host, unlike the other three bundlers.

Android

See ANDROID.md.

ToolNeeded forVersion ownerInstall
Android NDKnative build; set ANDROID_NDKbuild-android.sh (header) and its default path at line 28Android Studio SDK manager or the NDK zip
SDK build-toolsaapt2, zipalign, apksignerbundle-android.sh picks the highest installedSDK command-line tools
JDK (javac, keytool)compiles the SDL3 Java activity to classes.dex; generates the debug keystorereusable-build-android.yml (java-version)Temurin, or any distro JDK at that version
adbinstalling and reading logs on a deviceSDK platform-tools

Game-data folder picker (Linux runtime)

Not a build dependency — it's what the shipped binary calls when it needs to ask where your retail data is. zenity or an xdg-desktop-portal backend; the per-distro table with the reasoning is in GAME_DATA.md.

Tier 4 — faster, not required

Nothing in this section is wired into CI or any script's happy path. Each entry either degrades gracefully or is purely for your own loop.

ToolWhat it buys youNotes
clang-tidy + run-clang-tidymemory-safety and UB checks via run-clang-tidy.shscope in .clang-tidy. The only linter with no CI job, so nothing enforces it. Needs compile_commands.json
Pillowscreenshot comparison in the automation suitelib.sh skips image asserts when it is absent, so the suite passes either way
ImageMagickPPM to PNG in render_polyrec.shwithout it the PPMs are kept and conversion is skipped
actruns CI jobs locallyLinux jobs only; the macOS and Windows runners cannot be emulated
gdb / lldbdebuggingthe linux_clang and linux_sanitize presets pair with it

Engine-internal tooling

Everything above is a binary you install. The engine also ships a lot of its own instrumentation — polyrec, the control harness, the console, the trace commands — and that is what you actually reach for when debugging the game rather than the build.

It is deliberately not tiered here, for two reasons. Nothing needs installing, so "what breaks without it" has no meaning. And it changes on a different clock: a PR that adds a console command would age this doc, while the external tool list moves once a year. So this section routes; it never lists individual commands.

Prefer the live enumerators over any doc — they are generated from the code and cannot drift:

lba2cc --help                                   # player-facing flags
lba2cc --help-all                               # every flag, grouped
lba2cc --headless --exec "cmdlist" --tick 2 --exit   # every console command
lba2cc --headless --exec "varlist" --tick 2 --exit   # every cvar
CapabilityWhat it is forWhere it is documented
Control harnessboot, restore a save, run commands, advance N ticks, dump state or a screenshot, exit — all in one invocation. The backbone of headless verificationCONTROL.md
Debug console~40 commands and a set of cvars, always available (no build flag)CONSOLE.md
Polyrecpolygon record/replay; byte-compares ASM against C++ draw calls, with a bisect driver for first divergencePOLYREC.md
Trace commandsper-subsystem logging you toggle at runtime rather than rebuildingCONSOLE.md, TIMING.md
Perftraceper-frame timing ring buffer for frame-pacing workPERFTRACE.md
Adeline debug toolsthe original 1997 developer tools, behind DEBUG_TOOLS=ONDEBUG.md
Automation suitethe shell suite that drives the harness for regressionCONTROL.md, TESTING.md
Savegame corpusreplays a corpus of real saves through the load pathSAVEGAME.md

The one place the two axes meet is tier 4: Pillow and ImageMagick are external packages that only exist to make the internal harnesses more useful, and both degrade to a skip when absent.

By task

I want to…TiersExtra
Build and play1retail game data — GAME_DATA.md
Fix a bug in SOURCES/1 + clang-formatmake test for the host pass
Touch LIB386/1 + 2the container runs the ASM suite for you
Edit a script or workflow1 + shellcheck / ruff / actionlintrun shellcheck -S warning to match the gate
Edit docs onlylycheemake docs-links; the docs-only CI gate covers build and test, but the link check still runs
Run the control harness1retail data; Pillow for image asserts — CONTROL.md
Drive a running engine1 + Python 3retail data; a -DLBA2_CONTROL_SERVER=ON build; see CONTROL.md
Build for Android1 + Android lane
Cut a release1 + releasing + the target platform's bundlerRELEASING.md
Poke at disc images or HQR data1 + Python 3stdlib only; the two art scripts and the ACF decoder need Pillow

Deliberately not required

Named here so nobody adds them by accident:

  • A dependency manager (Nix, devenv, mise, asdf, vcpkg, Conan). Contributors are on Linux, macOS, Windows/MSYS2, and WSL, and the tool set is small enough that per-platform package managers stay cheaper than a lockfile everyone has to adopt.
  • A pinned compiler. The engine targets C++98 across GCC and Clang on four platforms; pinning one compiler would hide exactly the portability breaks CI exists to catch. See COMPILER_NOTES.md and PLATFORM.md.
  • Node, Rust, or Go toolchains. git-cliff ships prebuilt binaries; nothing else needs them.
  • Third-party Python packages, with one exception. Pillow is the only one, and it is needed by exactly three asset scripts — art_catalog_screen.py, art_treatment_preview.py, acf_decode.py — plus the automation suite's optional image asserts. Everything else, including the save probes and the corpus harness, runs on a bare python3. Keep it that way: a new script that needs numpy needs a conversation first.

Keeping this current

Same contract as scripts/README.md and docs/README.md: when you make a script or workflow depend on a new external tool, add its row here and a probe to check-tooling.sh in the same commit. There is a row for this in the AGENTS.md table.

What actually rots in a doc like this is versions and per-distro install lines, not the tool list. So:

  • Never write a version number in this file. Add it to the Version owner column as a link to the file that pins it, and teach check-tooling.sh to parse that file. A version stated in two places is already drifting.
  • Only write an install command where the obvious one is wrong. apt install cmake earns nothing. The four-way portal-backend split in GAME_DATA.md earns its space because guessing there breaks D-Bus.
  • If a tool has no probe, it does not belong in tiers 1–3. Those tiers are claims about whether work is possible, and an unprobed claim is the one that goes stale silently.