150 lines
6.5 KiB
Markdown
150 lines
6.5 KiB
Markdown
|
|
# Headless Linux dev container for autonomous Chessistics testing
|
|||
|
|
|
|||
|
|
## Why
|
|||
|
|
|
|||
|
|
Today the automation harness only runs on Windows because the Godot binary is
|
|||
|
|
hardcoded to `C:\Apps\godot\Godot_v4.6.2-stable_mono_win64_console.exe`. When
|
|||
|
|
Claude Code runs inside the project's dev container (Linux / `node:20`), it
|
|||
|
|
can read source code and run `dotnet test`, but it **cannot launch the actual
|
|||
|
|
game** — there's no Godot binary, no .NET SDK, and no display server for the
|
|||
|
|
renderer.
|
|||
|
|
|
|||
|
|
The goal: make the dev container a self-contained environment where Claude
|
|||
|
|
Code can build the project, launch a real Godot instance in headless Linux
|
|||
|
|
mode, drive it via the automation harness, and read back 1280×720 PNG
|
|||
|
|
screenshots — all without any Windows dependency.
|
|||
|
|
|
|||
|
|
## Design
|
|||
|
|
|
|||
|
|
### Pieces required
|
|||
|
|
|
|||
|
|
1. **Godot 4.6.2-stable Mono for Linux** — matches the Windows editor the
|
|||
|
|
project already uses. Installed once at image build time to `/opt/godot/`
|
|||
|
|
with a symlink `/opt/godot/godot`.
|
|||
|
|
2. **.NET SDK 9.0** — the project targets `net9.0`. Installed via the
|
|||
|
|
upstream `dot.net` install script to `/usr/local/dotnet/`.
|
|||
|
|
3. **Xvfb + Mesa software GL** — a virtual framebuffer at `:99` so Godot's
|
|||
|
|
GL-compatibility renderer has somewhere to draw. `xvfb-run` wraps any
|
|||
|
|
command transparently.
|
|||
|
|
4. **Python 3** — the automation harness is stdlib-only Python.
|
|||
|
|
5. **Minimal X / audio runtime deps** — `libx11`, `libxcursor`, `libxrandr`,
|
|||
|
|
`libxi`, `libgl1`, `libgles2`, `libasound2`, `libxkbcommon0`, etc.
|
|||
|
|
Without these, Godot exits on startup with `libXext not found`-style errors.
|
|||
|
|
|
|||
|
|
### How Godot reaches the framebuffer
|
|||
|
|
|
|||
|
|
Two options considered:
|
|||
|
|
|
|||
|
|
- **(A)** Run `Xvfb :99 -screen 0 1280x720x24 &` as a background process,
|
|||
|
|
export `DISPLAY=:99`, launch Godot normally. Persistent display, shared by
|
|||
|
|
many Godot runs.
|
|||
|
|
- **(B)** Use `xvfb-run -a --server-args="-screen 0 1280x720x24"` as a prefix
|
|||
|
|
on every Godot invocation. A fresh display per launch; cleans up
|
|||
|
|
automatically on exit.
|
|||
|
|
|
|||
|
|
**Chosen: (B)**, because the automation harness already spawns Godot once per
|
|||
|
|
`Harness.launch()` and cleans up on context exit — matches the per-launch
|
|||
|
|
lifecycle naturally, no daemon to keep alive, no race on the display number.
|
|||
|
|
|
|||
|
|
A tiny wrapper `/usr/local/bin/godot-xvfb` wraps `xvfb-run … $GODOT_BIN
|
|||
|
|
"$@"`, so the harness (or a human) only has to invoke one path.
|
|||
|
|
|
|||
|
|
### Integration with the existing harness
|
|||
|
|
|
|||
|
|
`tools/automation/harness.py` currently hardcodes the Windows Godot path. We
|
|||
|
|
teach it two things:
|
|||
|
|
|
|||
|
|
1. Read `GODOT_BIN` from the environment first; fall back to the platform
|
|||
|
|
default (Windows path on Windows, `/opt/godot/godot` on Linux).
|
|||
|
|
2. On Linux, auto-prepend `["xvfb-run", "-a", "--server-args=-screen 0
|
|||
|
|
1280x720x24"]` to the Godot launch command unless `DISPLAY` is already
|
|||
|
|
set (someone has a real display, skip the wrap).
|
|||
|
|
|
|||
|
|
With those two tweaks, every existing Python helper (`smoke.py`,
|
|||
|
|
`run_game.py`, `solve_*.py`) works unchanged inside the container.
|
|||
|
|
|
|||
|
|
### Firewall considerations
|
|||
|
|
|
|||
|
|
The container's `init-firewall.sh` runs at `postStartCommand`, **after** the
|
|||
|
|
image is built, and drops all outbound traffic except to a small allowlist
|
|||
|
|
(GitHub, npmjs, Anthropic, Sentry, Statsig). Impact on our pieces:
|
|||
|
|
|
|||
|
|
- **Godot binary + .NET SDK**: downloaded during `docker build`, which runs
|
|||
|
|
_before_ the firewall exists → works unconditionally.
|
|||
|
|
- **`dotnet restore`** (runtime, e.g. after a `git pull`): needs
|
|||
|
|
`api.nuget.org`. Added to the allowlist.
|
|||
|
|
- **Godot runtime**: no outbound traffic required — the engine runs fully
|
|||
|
|
offline once installed.
|
|||
|
|
|
|||
|
|
### Build sequence inside the Dockerfile
|
|||
|
|
|
|||
|
|
As `root`, before the existing `USER node` switch:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
# 1. X/GL/audio runtime + python + xvfb
|
|||
|
|
apt-get install xvfb xauth libx11-6 libxcursor1 libxinerama1 libxrandr2 \
|
|||
|
|
libxi6 libxext6 libgl1 libglx-mesa0 libgl1-mesa-dri libglu1-mesa \
|
|||
|
|
libasound2 libxkbcommon0 libxkbcommon-x11-0 libfontconfig1 libdbus-1-3 \
|
|||
|
|
python3 python3-pip
|
|||
|
|
|
|||
|
|
# 2. .NET SDK 9.0 via upstream install script
|
|||
|
|
curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- \
|
|||
|
|
--channel 9.0 --install-dir /usr/local/dotnet
|
|||
|
|
ln -s /usr/local/dotnet/dotnet /usr/local/bin/dotnet
|
|||
|
|
|
|||
|
|
# 3. Godot 4.6.2-stable mono, Linux x86_64
|
|||
|
|
wget https://github.com/godotengine/godot/releases/download/${VERSION}/\
|
|||
|
|
Godot_v${VERSION}_mono_linux_x86_64.zip
|
|||
|
|
unzip … -d /opt/godot
|
|||
|
|
ln -s /opt/godot/Godot_v…/Godot_v…_mono_linux.x86_64 /opt/godot/godot
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Then:
|
|||
|
|
- `ENV GODOT_BIN=/opt/godot/godot`
|
|||
|
|
- `ENV PATH=$PATH:/opt/godot:/usr/local/dotnet`
|
|||
|
|
- Drop in `/usr/local/bin/godot-xvfb` wrapper
|
|||
|
|
|
|||
|
|
## File-by-file change list
|
|||
|
|
|
|||
|
|
| File | Change |
|
|||
|
|
|------|--------|
|
|||
|
|
| `.devcontainer/Dockerfile` | Add Godot / dotnet / xvfb installs before `USER node` |
|
|||
|
|
| `.devcontainer/init-firewall.sh` | Append `api.nuget.org` to the domain allowlist |
|
|||
|
|
| `.devcontainer/godot-xvfb.sh` *(new)* | `exec xvfb-run -a … "$GODOT_BIN" "$@"` |
|
|||
|
|
| `tools/automation/harness.py` | Env-aware Godot path + Linux xvfb auto-wrap |
|
|||
|
|
| `README.md` *(new)* | Windows / WSL2 launch instructions for the dev container |
|
|||
|
|
|
|||
|
|
Nothing inside `Scripts/` or `chessistics-engine/` changes. The harness
|
|||
|
|
contract (inbox/outbox/screens) is platform-agnostic already.
|
|||
|
|
|
|||
|
|
## Verification
|
|||
|
|
|
|||
|
|
After rebuild:
|
|||
|
|
|
|||
|
|
1. `docker build .devcontainer -t chessistics-dev` succeeds.
|
|||
|
|
2. `devcontainer up --workspace-folder .` starts the container and
|
|||
|
|
post-start firewall passes.
|
|||
|
|
3. Inside: `dotnet --version` → `9.0.x`, `godot --version` →
|
|||
|
|
`4.6.2.stable.mono.official.*`.
|
|||
|
|
4. `dotnet build Chessistics.csproj` → green.
|
|||
|
|
5. `dotnet test chessistics-tests/` → 102 / 102.
|
|||
|
|
6. `python3 tools/automation/smoke.py` → loads mission 1, takes PNG
|
|||
|
|
screenshots that are non-black, quits cleanly.
|
|||
|
|
7. `Read` one of the PNGs — it should show the same mission 1 UI as the
|
|||
|
|
Windows run (title bar, board, objectives, stock panel).
|
|||
|
|
|
|||
|
|
## Out of scope (explicit non-goals)
|
|||
|
|
|
|||
|
|
- **GPU acceleration**: we use Mesa software rendering. Xvfb + llvmpipe is
|
|||
|
|
enough for 1280×720 at a few FPS, which is what the harness needs.
|
|||
|
|
- **Real display forwarding** (X11 forwarding, VNC, noVNC): doable but
|
|||
|
|
unnecessary — Claude reads PNGs, not a live video feed.
|
|||
|
|
- **Multi-arch images**: we ship x86_64 only. ARM (Apple Silicon via
|
|||
|
|
Docker Desktop emulation) would need `Godot_v…_mono_linux_arm64.zip` —
|
|||
|
|
straightforward to add if needed, not done here.
|
|||
|
|
- **Shrinking the image**: Godot + .NET SDK adds ~500 MB. Worth it;
|
|||
|
|
multi-stage builds could trim later.
|
|||
|
|
- **Keeping Xvfb warm across launches**: the single-launch pattern is clean
|
|||
|
|
enough. If someone ever scripts dozens of rapid Godot starts and the
|
|||
|
|
xvfb-run startup cost shows up, revisit approach (A).
|