Fix terminal on Windows, pin cross images, add per-platform CI #2
fix/windows-pty-and-build-refresh
into main
Repo refresh: verify the build on Windows and Linux, add a CLAUDE.md, fix what that turned up, and add per-platform CI. Several of these are real user-facing bugs, not housekeeping.
Huorn was broken on Windows
PlainShellSession::read treated a zero-byte read as EOF. That’s correct on Unix — a PTY read of 0 means the child is gone — but on Windows the conout reader is alacritty’s UnblockedReader, whose read returns Ok(0) both for “no output yet” and for “pipe closed”.
So every terminal died on its first poll_pty(), before the shell emitted a byte. The block went blank the instant you activated it. Liveness now comes from the ConPTY child watcher, and read_pty_output asks the session whether the child is gone rather than inferring it from a byte count — which is what the TerminalSession contract already said (Ok(0) means “nothing available right now”).
No behaviour change on Unix: a zero-byte read still sets alive = false there, so the same branch is taken.
Cross-compilation was quietly raising the Linux glibc floor
Cross.toml pinned both Linux images to the floating :main tag. Rebuilding today against it took the natives from glibc 2.30 to 2.39 — silently dropping every player on anything older than Ubuntu 24.04. Pinned to 0.2.5, which lands at glibc 2.18, and rebuilt both Linux natives on that baseline. Strictly better than what was committed.
runClient couldn’t be used to play
--server localhost was hardcoded on both loaders, so runClient went straight into a connection attempt instead of the title screen — there was no way to reach singleplayer. Split into a separate runClientJoinServer, which run_server_client.sh now calls.
That same inherited argument had also broken runVisualTest, which drives the game from the title screen and so never returned. Two more things kept it from running: a fresh run directory opens on the accessibility onboarding screen rather than the title screen, and its screenshot regions were swapped (the camera faces south, so +X renders to the left).
CI — the repo had never run any
.anvil-ci.yml was inert on two counts: the server reads .anvil.yml, and the file was written in GitHub Actions’ schema rather than Anvil’s. anvil ci list returned nothing — the release pipeline JOURNAL.md describes has never executed once. Replaced rather than repaired.
Four verification steps — linux-amd64, linux-arm64, macos-arm64, windows-amd64 — each running the same three checks via ci/verify.sh (or ci/verify.ps1, since Anvil runs bare Windows steps through PowerShell):
cargo test— the Rust suite, including per-platform PTY behaviourgradlew build— compiles and remaps both loadersrunGametest— boots a dedicated server and binds that platform’s committed native over JNI
The third is why these run per platform rather than once on Linux. A stale or broken native passes the other two, and only a real server boot catches it — which is how the post-rename native with the wrong JNI class path got through. It’s headless and takes ~40s.
No step declares an image:, so all run bare on the runner host; containerising would verify Linux three times. ci/check-natives.sh adds the guard neither build performs: every platform has a native, and each carries the current JNI class path.
Writing this found three things no amount of reading would have, each from an actual run:
- The Windows runner’s
JAVA_HOMEwas JDK 25, which Loom 1.6 cannot use. cargowas absent from the runner service’s PATH (rustup installs under~/.cargo/bin, which only login shells pick up).- The Linux runner’s
$HOMEis read-only, so the Gradle wrapper couldn’t unpack its distribution.
Rather than require every self-hosted runner to be configured just so, the scripts now discover a usable JDK 17/21 and cargo, and fall back to a workspace-local GRADLE_USER_HOME when the default isn’t writable. Deriving JAVA_HOME needs care — java on PATH is usually a symlink on Unix and Oracle’s javapath shim on Windows, and the naive parent-of-parent isn’t a JDK home — so symlinks are resolved and a candidate is only accepted if it contains a release file.
The release step no longer rebuilds natives. They’re committed artifacts already verified by check-natives and the gametests; rebuilding at release time would ship binaries nothing tested. It’s main-only via ANVIL_BRANCH.
Smaller things
- Jar naming.
archives_base_namewas declared but never wired up, so artifacts shipped asfabric-<version>.jarwhileINSTALL.mdtold players to look forhuorn-fabric-<version>.jar. Wired up; CI release attachments updated. build_natives.shonly ran on macOS. It invokedaarch64-apple-darwinunconditionally, so on a Linux runner it died at the first target underset -e. Now host-aware, and prints what it skipped.- Rust integration tests were Unix-only, and partly vacuous: they terminated commands with
\n, which ConPTY echoes without executing, and the assertions only grepped for the echoed text. They now use a sharedtests/commonhelper that sends\r. - A flaky test.
test_terminal_send_keyslept 100ms then sent Ctrl+C and asserted the terminal was alive. On Windows a Ctrl+C landing mid-startup kills PowerShell outright, so it passed or failed depending on machine load. It now waits for a readiness marker. CLAUDE.md, README refresh,.gitattributes, and untracking a stale build artifact carrying a pre-rename path.
Verification
| Windows | Linux | macOS | |
|---|---|---|---|
cargo test (103) |
✅ | ✅ | not run |
./gradlew build |
✅ JDK 17 | ✅ JDK 21 | not run |
runGametest (50) |
✅ | ✅ | not run |
build_natives.sh |
✅ | ✅ via cross |
not run |
| In-game | ✅ visual test 2/2 + a ~19 min play session | — | not run |
| In CI | queued (runner contention) | ✅ passed, 343s | ✗ no runner |
⚠️ Needs verification on macOS before merge
The macOS natives here are unchanged — I had no Mac to build them on. That’s deliberate and safe (the Rust change is a no-op on Unix, so they’re functionally current), but:
- Nothing in this PR has been compiled or run on macOS. At minimum please run
ci/verify.sh, which is exactly what CI runs. build_natives.shwas rewritten and its macOS branch is the one path never executed. It should build both macOS targets natively and both Linux targets viacross. Highest-risk untested code here.- Rebuild the macOS natives on a Mac before the next release so they match current source.
verify-macos-arm64 is intentionally left in the pipeline even though no macOS runner is registered. It will stay queued — so runs won’t show a final green — which keeps the gap visible rather than forgotten, and it starts working the moment a runner appears. Don’t delete it for a checkmark.
Also note: no ANVIL_TOKEN CI secret is set on this repo, so releases will fail loudly on main until one is added.
I deliberately skipped cargo update: bumping the lockfile would leave the macOS natives out of sync with dependencies I can’t rebuild them against.
🤖 Generated with Claude Code