shisaku ~/devlog
$ auth github

Author sign in

Sign in with GitHub to comment on devlog entries.

Continue with GitHub
← all posts

Devlog #18 — One Dist, One Box, Three Ships

EchoWarrior's release pipeline now ships the game, Leitmotif, and soundgarden as one cooperating suite, with optimized binaries that remain fully traceable through out-of-line debug symbols.

Devlog #18 — One Dist, One Box, Three Ships

Date: July 4, 2026

Focus: the release pipeline grew up. scripts/dist.ps1 and dist.sh used to package exactly one thing — the game. But EchoWarrior is no longer one binary: it’s a game plus two studio apps (Leitmotif, the scene director; soundgarden, the audio studio), each married to a CLI that lives in the game’s workspace. This is the devlog where all three learn to ship from one command, in one cooperating package — and — the part with teeth — where every shipped binary becomes traceable without giving up a single optimization.


1. Three products, one box

The game already had a good packaging story: dist/EchoWarrior-v<ver>-windows-x64/ plus a flat zip. The studio apps had none — you built them by cd-ing into tools/leitmotif and remembering the right incantation. The first draft of this change gave each app its own package under the same naming schema; it survived about an hour before the obvious flaw surfaced: three separate folders can’t cooperate. Leitmotif authors choreography.toml for a game install, soundgarden curates that install’s audio manifests — separated from the game folder, they’re editors with nothing to edit. So the final layout is one suite, placed the way the pieces expect to find each other:

dist/
  EchoWarrior-v0.67.21-windows-x64/
    echo_warrior.exe · data.pak · identity.pak · MODDING.md
    tools/
      leitmotif/       leitmotif.exe · choreo.exe · README.md
      soundgarden/     soundgarden.exe · (audio.exe) · README.md
  EchoWarrior - 0.67.21.zip              # the same tree, zipped

Each app sits next to the CLI it drives. Leitmotif never touches game internals — it shells out to choreo (validate/convert/schema/preview), and soundgarden does the same with audio. On Windows, CreateProcess resolves child processes from the application’s own directory, so choreo.exe beside leitmotif.exe just works — no PATH surgery, no installer. (Linux/macOS don’t search the app dir, so dist.sh prints the CHOREO_BIN=… launch hint the apps already understand.) And both apps sit inside the game folder they operate on: loose Assets/ files written next to echo_warrior.exe override data.pak, so a player who opens tools/leitmotif/ and saves a choreography is modding their own install — which is the entire point of this game. The tools/ nesting isn’t decoration either; it mirrors the repo, so a modder who graduates from the shipped suite to the source tree finds everything where they left it.

One honest wrinkle: the audio CLI still lives on the unmerged worktree-soundgarden-audio-studio branch. The script doesn’t pretend otherwise and doesn’t fail the build over it — soundgarden stages with a loud warning naming the branch. The day that branch merges, the warning disappears on its own, because the CLI staging is driven by what the cargo build produced, not by a hardcoded list.

2. speed_trace: stop choosing between fast and observable

The second half of the change is a new cargo profile, and it exists because of a lie we kept telling ourselves: “we’ll profile a debug build.” Debug builds of this game are a different game — the benchmark devlog (#17) exists precisely because release-only behavior matters. But release builds stripped of debug info are mute: capture an ETW/Tracy/Superluminal trace on a tester’s machine and you get a wall of anonymous addresses.

speed_trace refuses the trade:

[profile.speed_trace]
inherits = "release"          # thin LTO, codegen-units = 1 — full speed
debug = "full"                # every frame symbolizes
split-debuginfo = "packed"    # …but out-of-line: MSVC .pdb / DWARF side file
strip = "none"

The numbers from the first real build tell the story: echo_warrior.exe at 11 MB with an 83 MB .pdb next to it, not inside it. The side file stays in target/ — dist stages only the exe, so packages don’t grow by a byte. But when a trace comes back from any machine, the matching .pdb on the build machine symbolizes it completely. Same machine code as release (the profile only adds debug info, which doesn’t touch codegen), so the trace is of the thing we ship.

The profile is mirrored into both tool crates (tools/*/src-tauri/Cargo.toml), with the LTO/codegen settings spelled out there since their release profiles were stock. Both dist scripts build everything with it — one cargo build --profile speed_trace --locked produces the game and every companion CLI in a single pass.

3. The -- gauntlet

Getting a cargo profile through to Tauri involves three argument parsers, each of which eats one --:

npm run tauri:build -- --no-bundle -- --profile speed_trace --locked
                    │                │
                    │                └─ tauri build: rest goes to cargo
                    └─ npm: rest goes to the script

In bash that line works as written. In PowerShell it doesn’t — pwsh’s own parser treats a bare -- as its end-of-parameters token and removes the first one before npm ever sees it, which silently turns --no-bundle into an npm flag. The fix is one of those tricks worth writing down because it looks like a typo:

npm run tauri:build '--' '--no-bundle' '--' '--profile' $cargoProfile '--locked'

Quoted '--' tokens are strings, not parameter markers — PowerShell passes all of them through verbatim (verified with cmd /c echo). Tauri’s CLI, for its part, genuinely honors custom profiles from the passthrough: it compiled into src-tauri/target/speed_trace/ and reported the binary from there. --no-bundle keeps the pipeline installer-free — dist’s schema is folder + zip, and NSIS has no seat at that table.

4. What “done” looked like

Per the house rule — a feature that doesn’t survive the release pipeline is not done — verification was the pipeline itself:

The escape hatch survived too: -SkipTools / --skip-tools gives a game-only suite, and npm’s absence is caught before the five-minute game build, not after.

$ comments

Reader notes

0 notes

No notes yet.

If you're reading this as a developer: this devlog is built in the open.

If you're reading this as a modder: the direction is source-visible, inspectable systems.