1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
nescript/.github/workflows/ci.yml
Claude 90e01bd197
ci: fold mesen-dbg job into the main CI workflow
Move the Mesen2 .dbg validation job from its own workflow file into
the existing ci.yml so all CI lives in one place. Same job content
and same workarounds (settings.json shim, GLOBALIZATION_INVARIANT,
xvfb-run) — only the file location changed. The MESEN_VERSION env
var moves up to the workflow-level env block.

https://claude.ai/code/session_01DfN3pKJLryr7vvNFBpcqmC
2026-04-17 01:10:24 +00:00

307 lines
12 KiB
YAML

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"
# Pin the Mesen2 release the `mesen-dbg` job tests against. Bump
# this when the upstream binary's behaviour changes in a way that
# affects the 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:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo check --all-targets
fmt:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --all-targets -- -D warnings
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --all-targets
examples:
name: Build Examples
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo build --release
- name: Verify committed ROMs match their .ne sources
# Every example has a committed .nes file under examples/. The
# compiler is deterministic (verified: same input → byte-identical
# output), so rebuilding each example into a tmp directory and
# diffing against the committed ROM catches "someone edited a .ne
# file but forgot to regenerate the ROM" at PR time.
run: |
mkdir -p /tmp/reprotest
fail=0
for f in examples/*.ne; do
name=$(basename "$f" .ne)
./target/release/nescript build "$f" --output "/tmp/reprotest/$name.nes"
if ! cmp -s "examples/$name.nes" "/tmp/reprotest/$name.nes"; then
echo "::error file=examples/$name.ne::committed examples/$name.nes is stale; rerun \`cargo run --release -- build examples/$name.ne\` and commit the new ROM"
fail=1
fi
done
if [ $fail -ne 0 ]; then
exit 1
fi
- name: Verify ROMs are valid iNES
run: |
for f in examples/*.nes; do
echo "==> Checking $f"
# iNES magic: first 4 bytes must be "NES\x1a"
header=$(od -A n -t x1 -N 4 "$f" | tr -d ' ')
if [ "$header" != "4e45531a" ]; then
echo "FAIL: $f is not a valid iNES ROM (header: $header)"
exit 1
fi
size=$(stat -c%s "$f")
echo " OK: $size bytes"
done
emulator:
name: Emulator Smoke Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install Chrome runtime deps for puppeteer
# puppeteer ships its own Chrome for Testing binary via the
# package's postinstall hook, but the runner image is missing
# a handful of shared libs that Chrome needs to launch.
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libnss3 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libatspi2.0-0 \
libcups2t64 \
libdbus-1-3 \
libdrm2 \
libgbm1 \
libpango-1.0-0 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxkbcommon0 \
libxrandr2 \
libasound2t64
- name: Build compiler
run: cargo build --release
- name: Compile all .ne examples
# Overwrites the committed examples/*.nes in the CI workspace
# (the workspace is ephemeral; the committed ROMs aren't
# touched). This is load-bearing: the goldens must be diffed
# against what *this* compiler produces, not against the ROMs
# an earlier commit happened to ship, otherwise a broken
# compiler change can still pass the emulator job as long as
# the contributor forgot to rebuild the ROMs. The `examples`
# job catches that second case via its reproducibility diff.
run: |
for f in examples/*.ne; do
echo "==> Compiling $f"
./target/release/nescript build "$f"
done
- name: Install emulator harness dependencies
working-directory: tests/emulator
run: npm ci
- name: Diff each ROM's framebuffer against its golden
working-directory: tests/emulator
run: node run_examples.mjs
- name: Verify docs/platformer.gif is up to date
# The README embeds docs/platformer.gif as the project demo.
# gifenc + jsnes are deterministic, so rebuilding the gif from
# the current compiler should byte-match the committed copy.
# If it doesn't, the contributor changed the compiler / source
# / harness in a way that affects the observable gameplay of
# platformer.ne but forgot to regenerate the gif — the fix is
# the exact command printed in the error.
working-directory: tests/emulator
run: |
node record_gif.mjs platformer 360 2 /tmp/platformer.gif
if ! cmp -s ../../docs/platformer.gif /tmp/platformer.gif; then
echo "::error file=docs/platformer.gif::committed docs/platformer.gif is stale; rerun \`node tests/emulator/record_gif.mjs platformer 360 2 docs/platformer.gif\` and commit the new gif"
exit 1
fi
- name: Verify docs/war.gif is up to date
# Same contract as docs/platformer.gif: the README embeds
# docs/war.gif as the second project demo, gifenc + jsnes are
# deterministic, so any change to the compiler / runtime /
# harness / war source that affects the first ~6 seconds of
# the war.ne gameplay must be followed by regenerating the
# gif. The 5th positional arg (`4`) is the warmup; war's
# title menu is the gif thumbnail so we don't skip past it.
working-directory: tests/emulator
run: |
node record_gif.mjs war 360 2 /tmp/war.gif 4
if ! cmp -s ../../docs/war.gif /tmp/war.gif; then
echo "::error file=docs/war.gif::committed docs/war.gif is stale; rerun \`node tests/emulator/record_gif.mjs war 360 2 docs/war.gif 4\` and commit the new gif"
exit 1
fi
- name: Verify docs/pong.gif is up to date
# Same contract as the platformer and war gifs: the README
# embeds docs/pong.gif as the third project demo. Pong's
# title screen (CPU VS CPU / 1 PLAYER / 2 PLAYERS menu) is
# the gif thumbnail, so warmup = 4 keeps it in frame before
# the autopilot advances to gameplay.
working-directory: tests/emulator
run: |
node record_gif.mjs pong 360 2 /tmp/pong.gif 4
if ! cmp -s ../../docs/pong.gif /tmp/pong.gif; then
echo "::error file=docs/pong.gif::committed docs/pong.gif is stale; rerun \`node tests/emulator/record_gif.mjs pong 360 2 docs/pong.gif 4\` and commit the new gif"
exit 1
fi
- name: Upload actual + diff PNGs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: emulator-diff
# `actual/` holds the run's output and red-highlighted diff
# PNGs for any ROM that didn't match its golden. `report.json`
# has the per-example pass/fail/diff counts. `goldens/` is
# included too so reviewers can compare the three side by side
# without cloning the repo.
path: |
tests/emulator/actual/
tests/emulator/goldens/
tests/emulator/report.json
mesen-dbg:
# 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. Builds the compiler,
# builds an example ROM with `--dbg`, then launches the Mesen2
# release binary in `--testRunner` mode. Mesen auto-loads the
# co-located .dbg, runs `tests/mesen/probe.lua`, and exits with a
# status code the script picks. CI fails if any code label we
# promised to emit is missing or unresolved.
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 the commit
# message for d075bc2).
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