1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00

ci: add headless Mesen2 .dbg validation workflow

Run Mesen2's `--testRunner` mode in CI, point it at a NEScript-built
ROM + auto-loaded .dbg, and assert via Lua that the four entry-point
labels (`nmi`, `irq`, `Main_frame`, `main_loop`) we promised to emit
actually resolve. Failures encode which assertion broke into the exit
code so CI can report it without stdout (Mesen's emu.log is internal).

The setup needed three workarounds, each documented inline in the
workflow:
  * `touch settings.json` to skip Mesen's first-run GUI wizard, which
    blocks the --testRunner dispatch in Program.cs:74. Contents don't
    matter — Configuration.Deserialize falls back to defaults on parse
    error.
  * `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1` to keep .NET from loading
    system libstdc++ via libicuuc. On Ubuntu 24.04 the dual libstdc++
    presence (system + MesenCore.so's bundled static copy) crashes
    MesenCore's static regex initialiser with std::bad_cast before any
    user code runs.
  * `xvfb-run` because Mesen's Avalonia UI calls XOpenDisplay before
    --testRunner is dispatched.

This is a separate workflow file from ci.yml because it depends on the
40 MB Mesen2 binary download + xvfb + sdl2, none of which the existing
jobs need. Cached by Mesen version so reruns are fast.

https://claude.ai/code/session_01DfN3pKJLryr7vvNFBpcqmC
This commit is contained in:
Claude 2026-04-17 01:03:23 +00:00
parent e4751df143
commit d075bc2c43
No known key found for this signature in database
2 changed files with 165 additions and 0 deletions

120
.github/workflows/mesen.yml vendored Normal file
View file

@ -0,0 +1,120 @@
name: Mesen .dbg Smoke Test
# Validates that the `.dbg` debug-info file `nescript build --dbg` emits
# is actually consumable by Mesen2 — the same emulator NES developers
# use for source-level debugging. The job:
# 1. Builds the compiler.
# 2. Builds an example ROM with `--dbg`.
# 3. Launches the Mesen2 release binary in `--testRunner` mode.
# 4. Mesen auto-loads the co-located .dbg, runs `tests/mesen/probe.lua`,
# and exits with a status code the script picks.
# 5. CI fails if any code label we promised is missing or unresolved.
#
# This is a separate workflow from `ci.yml` because it depends on the
# Mesen2 binary download (40 MB) + xvfb + a couple of Mesen-specific
# environment workarounds that aren't worth threading through the main
# CI matrix.
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"
# Pin the Mesen2 release we test against. Bump this when the upstream
# binary's behaviour changes in a way that affects our probe (new
# label-name normalisation, .dbg format additions, etc.). See
# https://github.com/SourMesen/Mesen2/releases for the source URLs.
MESEN_VERSION: "2.1.1"
jobs:
mesen-dbg:
name: Mesen2 .dbg validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install Mesen runtime deps
# libsdl2-2.0-0: Mesen links to it for audio/video even though
# we're running headless. Without it MesenCore.so fails to
# load with `cannot open shared object file: libSDL2-2.0.so.0`.
# xvfb: Mesen's Avalonia UI initialises an X11 connection at
# startup before checking `--testRunner`, so even the headless
# path needs *some* display.
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libsdl2-2.0-0 \
xvfb
- name: Cache Mesen2 binary
id: mesen-cache
uses: actions/cache@v4
with:
path: mesen2
key: mesen2-${{ env.MESEN_VERSION }}-linux-x64
- name: Download Mesen2
if: steps.mesen-cache.outputs.cache-hit != 'true'
run: |
mkdir -p mesen2
cd mesen2
curl -sL -o Mesen.zip \
"https://github.com/SourMesen/Mesen2/releases/download/${MESEN_VERSION}/Mesen_${MESEN_VERSION}_Linux_x64.zip"
unzip -q Mesen.zip
chmod +x Mesen
rm Mesen.zip
- name: Build NEScript
run: cargo build --release
- name: Build test ROM with .dbg
# `hello_sprite` is the smallest example with a state handler,
# so its .dbg exercises every record kind render_dbg emits
# (file, mod, seg, scope, span, line, sym) without a long
# compile.
run: |
./target/release/nescript build examples/hello_sprite.ne \
--dbg /tmp/hello.dbg \
--output /tmp/hello.nes
- name: Validate .dbg via Mesen2 testRunner
# Three workarounds are baked into this single command — see the
# comments inside for *why* each is needed. Removing any of them
# turns this into a multi-hour debugging exercise (it took
# several hours of investigation to find the GLOBALIZATION
# workaround in particular; see commit message of the change
# that added this workflow).
run: |
# 1. `touch settings.json`: Mesen's first-run check
# (`Program.cs:56`) launches the GUI setup wizard
# *before* the `--testRunner` dispatch when no settings
# file exists. The wizard never closes in headless CI,
# blocking forever. The file's *contents* don't matter:
# Configuration.Deserialize catches any JSON parse error
# and falls back to defaults, so a 0-byte file is fine.
touch mesen2/settings.json
# 2. `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1`: .NET's
# globalization layer pulls in libicuuc, which loads
# system libstdc++. MesenCore.so is built with a
# statically-bundled libstdc++; on Ubuntu 24.04 the
# interposition causes a `std::bad_cast` from a static
# regex initialiser in `Base6502Assembler.cpp` long
# before any user code runs. Invariant mode skips ICU
# entirely, which means system libstdc++ is never
# loaded and MesenCore's bundled copy wins.
#
# 3. `xvfb-run -a`: Mesen's Avalonia UI runs `XOpenDisplay`
# during AppBuilder setup before the `--testRunner`
# dispatch in `Program.cs:74`. xvfb-run gives it a
# throwaway X server; -a auto-allocates a display number.
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 \
xvfb-run -a ./mesen2/Mesen \
--testRunner /tmp/hello.nes tests/mesen/probe.lua \
--timeout=15