Headless Linux dev container: Godot + .NET + Xvfb for autonomous testing
Claude Code running inside the project's dev container can now build the game, launch a real Godot instance under Xvfb, and drive the automation harness end-to-end — no Windows dependency. Dockerfile adds (as root, before USER node): - X11 / Mesa software GL / audio runtime deps + python3 - .NET SDK 9.0 via upstream dot.net install script -> /usr/local/dotnet - Godot 4.6.2-stable mono Linux x86_64 -> /opt/godot/godot - /usr/local/bin/godot-xvfb wrapper: auto-wraps invocations in xvfb-run -a --server-args="-screen 0 1280x720x24 ..." harness.py picks GODOT_BIN from env, defaults to /opt/godot/godot on Linux, and auto-wraps the subprocess in xvfb-run when DISPLAY is unset. Windows code path unchanged. init-firewall.sh adds api.nuget.org to the allowlist so dotnet restore works post-boot. Godot + .NET SDK are fetched at image build time, before the firewall exists. New docs: - autonomous_plan.md: design rationale, alternatives considered - README.md: launch instructions for Windows terminal / Docker Desktop / VS Code Dev Containers / WSL2 natif - CLAUDE.md already documents the harness (done in previous commit) Validation: docker build succeeds; inside the container, dotnet --version =9.0.313, godot --version=4.6.2.stable.mono, dotnet test=102/102, python3 tools/automation/smoke.py passes end-to-end with 14 non-black 1280x720 PNGs. Mission 1 screenshot is visually identical to the Windows build, and Xvfb determinism is a bonus (det_a.png ≡ det_b.png bytewise).
This commit is contained in:
parent
8f3b1b39e7
commit
c451a50349
6 changed files with 442 additions and 4 deletions
|
|
@ -25,8 +25,80 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
jq \
|
jq \
|
||||||
nano \
|
nano \
|
||||||
vim \
|
vim \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Chessistics: headless Godot + .NET SDK
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# 1. Xvfb + Mesa software GL + X/audio runtime deps for Godot's GL-compatibility renderer
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
xvfb \
|
||||||
|
xauth \
|
||||||
|
x11-utils \
|
||||||
|
libx11-6 \
|
||||||
|
libxcursor1 \
|
||||||
|
libxinerama1 \
|
||||||
|
libxrandr2 \
|
||||||
|
libxi6 \
|
||||||
|
libxext6 \
|
||||||
|
libxrender1 \
|
||||||
|
libxfixes3 \
|
||||||
|
libxss1 \
|
||||||
|
libxkbcommon0 \
|
||||||
|
libxkbcommon-x11-0 \
|
||||||
|
libgl1 \
|
||||||
|
libglx-mesa0 \
|
||||||
|
libgl1-mesa-dri \
|
||||||
|
libglu1-mesa \
|
||||||
|
libegl1 \
|
||||||
|
libgles2 \
|
||||||
|
libasound2 \
|
||||||
|
libpulse0 \
|
||||||
|
libfontconfig1 \
|
||||||
|
libfreetype6 \
|
||||||
|
libdbus-1-3 \
|
||||||
|
libudev1 \
|
||||||
|
fonts-dejavu-core \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 2. .NET SDK 9.0 via the upstream install script (arch-agnostic, no apt repo needed)
|
||||||
|
RUN curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh \
|
||||||
|
&& bash /tmp/dotnet-install.sh --channel 9.0 --install-dir /usr/local/dotnet \
|
||||||
|
&& ln -s /usr/local/dotnet/dotnet /usr/local/bin/dotnet \
|
||||||
|
&& rm /tmp/dotnet-install.sh
|
||||||
|
ENV DOTNET_ROOT=/usr/local/dotnet
|
||||||
|
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
|
ENV DOTNET_NOLOGO=1
|
||||||
|
|
||||||
|
# 3. Godot 4.6.2-stable Mono for Linux x86_64
|
||||||
|
# The zip contains a directory like "Godot_v..._mono_linux_x86_64/" with
|
||||||
|
# an executable whose exact filename has varied across releases
|
||||||
|
# ("Godot_v..._mono_linux.x86_64" on 4.x). Locate it dynamically.
|
||||||
|
ARG GODOT_VERSION=4.6.2-stable
|
||||||
|
RUN mkdir -p /opt/godot \
|
||||||
|
&& cd /tmp \
|
||||||
|
&& wget -q "https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}/Godot_v${GODOT_VERSION}_mono_linux_x86_64.zip" \
|
||||||
|
&& unzip -q "Godot_v${GODOT_VERSION}_mono_linux_x86_64.zip" -d /opt/godot \
|
||||||
|
&& GODOT_EXE="$(find /opt/godot -maxdepth 3 -type f \( -name 'Godot_v*mono_linux*x86_64' -o -name 'Godot_v*mono_linux*x86_64' \) | head -1)" \
|
||||||
|
&& if [ -z "$GODOT_EXE" ]; then echo "Godot executable not found in zip" && ls -R /opt/godot && exit 1; fi \
|
||||||
|
&& chmod +x "$GODOT_EXE" \
|
||||||
|
&& ln -sf "$GODOT_EXE" /opt/godot/godot \
|
||||||
|
&& rm "Godot_v${GODOT_VERSION}_mono_linux_x86_64.zip"
|
||||||
|
ENV GODOT_BIN=/opt/godot/godot
|
||||||
|
ENV PATH=$PATH:/opt/godot:/usr/local/dotnet
|
||||||
|
|
||||||
|
# 4. xvfb wrapper — any Godot invocation gets its own virtual 1280x720x24 display.
|
||||||
|
# Usage: `godot-xvfb --path /workspace ...` or let tools/automation/harness.py
|
||||||
|
# invoke it automatically on Linux.
|
||||||
|
COPY godot-xvfb.sh /usr/local/bin/godot-xvfb
|
||||||
|
RUN chmod +x /usr/local/bin/godot-xvfb
|
||||||
|
|
||||||
# Ensure default node user has access to /usr/local/share
|
# Ensure default node user has access to /usr/local/share
|
||||||
RUN mkdir -p /usr/local/share/npm-global && \
|
RUN mkdir -p /usr/local/share/npm-global && \
|
||||||
chown -R node:node /usr/local/share
|
chown -R node:node /usr/local/share
|
||||||
|
|
|
||||||
15
.devcontainer/godot-xvfb.sh
Normal file
15
.devcontainer/godot-xvfb.sh
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Wrap a Godot invocation in a fresh Xvfb 1280x720x24 display so the GL
|
||||||
|
# renderer has something to draw into. If DISPLAY is already set (real
|
||||||
|
# display / nested X server), skip xvfb-run and exec Godot directly.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${GODOT_BIN:=/opt/godot/godot}"
|
||||||
|
|
||||||
|
if [[ -n "${DISPLAY:-}" ]]; then
|
||||||
|
exec "$GODOT_BIN" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec xvfb-run -a \
|
||||||
|
--server-args="-screen 0 1280x720x24 -ac +extension GLX +render -noreset" \
|
||||||
|
"$GODOT_BIN" "$@"
|
||||||
|
|
@ -69,7 +69,8 @@ for domain in \
|
||||||
"api.anthropic.com" \
|
"api.anthropic.com" \
|
||||||
"sentry.io" \
|
"sentry.io" \
|
||||||
"statsig.anthropic.com" \
|
"statsig.anthropic.com" \
|
||||||
"statsig.com"; do
|
"statsig.com" \
|
||||||
|
"api.nuget.org"; do
|
||||||
echo "Resolving $domain..."
|
echo "Resolving $domain..."
|
||||||
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')
|
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')
|
||||||
if [ -z "$ips" ]; then
|
if [ -z "$ips" ]; then
|
||||||
|
|
|
||||||
174
README.md
Normal file
174
README.md
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
# Chessistics
|
||||||
|
|
||||||
|
Jeu de logistique sur échiquier en Godot 4 / C#. Le joueur place des pièces
|
||||||
|
d'échecs sur un plateau ; elles se déplacent automatiquement et transportent
|
||||||
|
des ressources entre des productions et des demandes.
|
||||||
|
|
||||||
|
Voir [`CLAUDE.md`](CLAUDE.md) pour l'architecture (black-box simulation) et
|
||||||
|
les conventions internes.
|
||||||
|
|
||||||
|
## Arborescence
|
||||||
|
|
||||||
|
```
|
||||||
|
Chessistics/
|
||||||
|
├─ Scripts/ # Code Godot (présentation)
|
||||||
|
│ ├─ Automation/ # Harness autonome (CLI --automation=<dir>)
|
||||||
|
│ ├─ Board/ Input/ UI/ Pieces/ Presentation/
|
||||||
|
│ └─ Main.cs
|
||||||
|
├─ chessistics-engine/ # Moteur pur .NET, sans Godot
|
||||||
|
├─ chessistics-tests/ # Tests unitaires xUnit (sans Godot)
|
||||||
|
├─ Data/
|
||||||
|
│ ├─ campaigns/ # campaign_01.json (7 missions)
|
||||||
|
│ └─ levels/ # niveaux legacy
|
||||||
|
├─ tools/automation/ # Harness Python stdlib pour piloter Godot
|
||||||
|
├─ .devcontainer/ # Image Docker + firewall + Xvfb + Godot Linux
|
||||||
|
├─ Scenes/ icon.svg project.godot …
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lancer le jeu (poste Windows direct)
|
||||||
|
|
||||||
|
Prérequis : Godot 4.6 mono + .NET 9 SDK installés localement.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet build Chessistics.csproj
|
||||||
|
"C:\Apps\godot\Godot_v4.6.2-stable_mono_win64.exe" --path .
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests headless du moteur :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet test chessistics-tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
Smoke test du harness d'automatisation :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python tools/automation/smoke.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dev container autonome (recommandé pour Claude Code)
|
||||||
|
|
||||||
|
Le dossier `.devcontainer/` contient une image Docker qui embarque :
|
||||||
|
|
||||||
|
- **.NET SDK 9.0** (build + tests)
|
||||||
|
- **Godot 4.6.2-stable mono Linux x86_64** sous `/opt/godot/godot`
|
||||||
|
- **Xvfb** + Mesa software GL pour un framebuffer virtuel 1280×720
|
||||||
|
- **Python 3** pour le harness d'automatisation
|
||||||
|
- **Claude Code** installé automatiquement (`@anthropic-ai/claude-code`)
|
||||||
|
- Un firewall `iptables` qui restreint les sorties réseau à une allow-list
|
||||||
|
(GitHub, npm, Anthropic, NuGet, Sentry, Statsig)
|
||||||
|
|
||||||
|
Claude Code, lancé à l'intérieur, peut donc compiler le projet, exécuter
|
||||||
|
les tests, **démarrer une vraie instance de Godot en headless** et lire les
|
||||||
|
captures PNG produites — sans dépendance Windows.
|
||||||
|
|
||||||
|
Voir [`autonomous_plan.md`](autonomous_plan.md) pour le design détaillé.
|
||||||
|
|
||||||
|
### Option 1 — Docker Desktop + terminal Windows (le plus simple)
|
||||||
|
|
||||||
|
1. Installer [Docker Desktop pour Windows](https://www.docker.com/products/docker-desktop/).
|
||||||
|
Docker Desktop utilise WSL2 en coulisses ; aucune configuration WSL
|
||||||
|
manuelle n'est nécessaire.
|
||||||
|
2. Installer la CLI devcontainers :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm install -g @devcontainers/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Depuis PowerShell ou Terminal Windows, à la racine du repo :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd C:\Projets\Chessistics
|
||||||
|
devcontainer up --workspace-folder .
|
||||||
|
devcontainer exec --workspace-folder . zsh
|
||||||
|
```
|
||||||
|
|
||||||
|
La première commande construit l'image (long le premier coup : Godot +
|
||||||
|
.NET à télécharger). La seconde ouvre un shell interactif dans le
|
||||||
|
container.
|
||||||
|
|
||||||
|
4. Dans le container :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build Chessistics.csproj
|
||||||
|
python3 tools/automation/smoke.py
|
||||||
|
claude # lance Claude Code inside the container
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2 — VS Code + extension Dev Containers
|
||||||
|
|
||||||
|
1. Installer Docker Desktop + [VS Code](https://code.visualstudio.com/) +
|
||||||
|
l'extension **Dev Containers**.
|
||||||
|
2. Ouvrir le dossier du repo dans VS Code.
|
||||||
|
3. Palette de commandes → `Dev Containers: Reopen in Container`.
|
||||||
|
4. Le terminal intégré est déjà dans le container. Lancer `claude` pour
|
||||||
|
démarrer Claude Code.
|
||||||
|
|
||||||
|
### Option 3 — WSL2 natif (plus performant si Docker Desktop rame)
|
||||||
|
|
||||||
|
Les bind-mounts Docker Desktop sur `C:\` sont plus lents qu'un fichier
|
||||||
|
stocké directement dans le système de fichiers WSL2. Pour un gros projet
|
||||||
|
c'est négligeable, mais si la lenteur devient visible :
|
||||||
|
|
||||||
|
1. Installer WSL2 + Ubuntu depuis le Microsoft Store.
|
||||||
|
2. Cloner le repo **à l'intérieur** de WSL2 (pas sur `/mnt/c/`) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~ && git clone <url> chessistics && cd chessistics
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Installer Docker dans WSL2 (via `docker.io` apt ou Docker Desktop avec
|
||||||
|
intégration WSL2 activée).
|
||||||
|
4. Installer la CLI devcontainers :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @devcontainers/cli
|
||||||
|
devcontainer up --workspace-folder .
|
||||||
|
devcontainer exec --workspace-folder . zsh
|
||||||
|
```
|
||||||
|
|
||||||
|
Avantage : I/O disque natif Linux. Inconvénient : pas d'accès Explorer
|
||||||
|
direct aux fichiers (`\\wsl$\Ubuntu\home\…`).
|
||||||
|
|
||||||
|
### Vérifier que tout fonctionne dans le container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet --version # -> 9.0.x
|
||||||
|
godot --version # -> 4.6.2.stable.mono.official.*
|
||||||
|
dotnet test chessistics-tests/ # 102/102
|
||||||
|
python3 tools/automation/smoke.py # Godot boots, PNG screenshots written
|
||||||
|
ls .automation_runs/smoke/screens/ # 01_loaded.png, 02_placed.png, ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Le harness Python détecte automatiquement Linux et enveloppe Godot dans
|
||||||
|
`xvfb-run`. Aucune variable d'environnement à positionner.
|
||||||
|
|
||||||
|
### Personnaliser la version de Godot
|
||||||
|
|
||||||
|
L'image par défaut pose Godot 4.6.2-stable. Pour changer, modifier
|
||||||
|
l'argument de build :
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// .devcontainer/devcontainer.json
|
||||||
|
"build": {
|
||||||
|
"args": {
|
||||||
|
"GODOT_VERSION": "4.7.0-stable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis `devcontainer up --workspace-folder . --build-no-cache`.
|
||||||
|
|
||||||
|
## Dépannage
|
||||||
|
|
||||||
|
| Symptôme | Cause probable | Fix |
|
||||||
|
|----------|----------------|-----|
|
||||||
|
| `godot --version` donne *no such file* | PATH non mis à jour | `source /etc/profile` ou relancer le shell |
|
||||||
|
| Screenshot tout noir | Aucun DISPLAY + pas d'xvfb | Vérifier `which xvfb-run` ; utiliser `godot-xvfb` au lieu de `godot` directement |
|
||||||
|
| `dotnet restore` bloque | Firewall bloque `api.nuget.org` | Vérifier que `init-firewall.sh` s'est bien exécuté avec les changements récents |
|
||||||
|
| Build Docker échoue au download de Godot | Réseau restreint côté hôte | Retry, ou installer Godot manuellement et commenter les lignes correspondantes |
|
||||||
|
| Claude Code demande `--dangerously-skip-permissions` | Comportement normal en container sandbox | Accepter si tu es conscient du modèle de confiance |
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
Voir les fichiers du repo.
|
||||||
149
autonomous_plan.md
Normal file
149
autonomous_plan.md
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
# 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).
|
||||||
|
|
@ -36,10 +36,37 @@ from typing import Any
|
||||||
|
|
||||||
# Resolve defaults relative to the repo root (parent of tools/).
|
# Resolve defaults relative to the repo root (parent of tools/).
|
||||||
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||||
_DEFAULT_GODOT = Path(r"C:\Apps\godot\Godot_v4.6.2-stable_mono_win64_console.exe")
|
|
||||||
_DEFAULT_RUNS = _REPO_ROOT / ".automation_runs"
|
_DEFAULT_RUNS = _REPO_ROOT / ".automation_runs"
|
||||||
|
|
||||||
|
|
||||||
|
def _default_godot_exe() -> Path:
|
||||||
|
"""Locate a Godot binary. Env var wins; otherwise platform defaults."""
|
||||||
|
env = os.environ.get("GODOT_BIN")
|
||||||
|
if env:
|
||||||
|
return Path(env)
|
||||||
|
if sys.platform.startswith("linux"):
|
||||||
|
return Path("/opt/godot/godot")
|
||||||
|
return Path(r"C:\Apps\godot\Godot_v4.6.2-stable_mono_win64_console.exe")
|
||||||
|
|
||||||
|
|
||||||
|
_DEFAULT_GODOT = _default_godot_exe()
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_with_xvfb(argv: list[str]) -> list[str]:
|
||||||
|
"""On Linux without an existing DISPLAY, wrap Godot in xvfb-run so the
|
||||||
|
GL-compatibility renderer has a framebuffer to target.
|
||||||
|
"""
|
||||||
|
if not sys.platform.startswith("linux"):
|
||||||
|
return argv
|
||||||
|
if os.environ.get("DISPLAY"):
|
||||||
|
return argv
|
||||||
|
return [
|
||||||
|
"xvfb-run", "-a",
|
||||||
|
"--server-args=-screen 0 1280x720x24 -ac +extension GLX +render -noreset",
|
||||||
|
*argv,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class HarnessError(RuntimeError):
|
class HarnessError(RuntimeError):
|
||||||
"""Raised when a command fails or times out."""
|
"""Raised when a command fails or times out."""
|
||||||
|
|
||||||
|
|
@ -98,11 +125,11 @@ class Harness:
|
||||||
if not self.project_path.exists():
|
if not self.project_path.exists():
|
||||||
raise HarnessError(f"Project path not found: {self.project_path}")
|
raise HarnessError(f"Project path not found: {self.project_path}")
|
||||||
|
|
||||||
args = [
|
args = _wrap_with_xvfb([
|
||||||
str(self.godot_exe),
|
str(self.godot_exe),
|
||||||
"--path", str(self.project_path),
|
"--path", str(self.project_path),
|
||||||
f"--automation={self.root}",
|
f"--automation={self.root}",
|
||||||
]
|
])
|
||||||
print(f"[harness] launching: {' '.join(args)}", file=sys.stderr)
|
print(f"[harness] launching: {' '.join(args)}", file=sys.stderr)
|
||||||
# Inherit stdout/stderr so GD.Print output is visible.
|
# Inherit stdout/stderr so GD.Print output is visible.
|
||||||
self._proc = subprocess.Popen(args)
|
self._proc = subprocess.Popen(args)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue