From 62c56cf886d7e7ffc91cf55a221cf319a8fd712f Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Sat, 10 Oct 2026 11:22:09 -0400 Subject: [PATCH] ci+tools: macOS arm64 release package (scripts/package-macos.sh) + macos-build CI job, Mach-O symbol tooling, Deck levelbench/deckrunner harness, verify.sh ini extras --- .github/workflows/ci.yml | 98 ++++++++++++++- scripts/gen_macho_syms.py | 71 +++++++++++ scripts/package-macos.sh | 217 ++++++++++++++++++++++++++++++++++ scripts/strip_weak_pragmas.py | 25 ++++ tools_pc/README.md | 2 + tools_pc/abi_baseline.json | 33 +++--- tools_pc/deckrunner.sh | 34 ++++++ tools_pc/levelbench.sh | 124 +++++++++++++++++++ tools_pc/updatecheck_test.c | 114 ++++++++++++++++++ tools_pc/verify.sh | 7 +- 10 files changed, 703 insertions(+), 22 deletions(-) create mode 100644 scripts/gen_macho_syms.py create mode 100755 scripts/package-macos.sh create mode 100644 scripts/strip_weak_pragmas.py create mode 100644 tools_pc/deckrunner.sh create mode 100644 tools_pc/levelbench.sh create mode 100644 tools_pc/updatecheck_test.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa3194f0..addabf21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -294,6 +294,93 @@ jobs: if-no-files-found: error retention-days: 14 + # Experimental macOS (Apple Silicon) build. Compiles + links with Homebrew's + # real GNU GCC (Apple Clang cannot compile the decomp's Plan 9 struct + # extensions) and packages the bundle with scripts/package-macos.sh: the + # non-system dylibs (sdl2-compat + SDL3, libstdc++, libgcc_s) are copied + # next to the exe with @executable_path install names, re-signed ad-hoc. + # The bundle is not notarized; the README inside it gives the one-line + # `xattr -dr com.apple.quarantine .` unblock. Like linux-build this is a + # "compiles + links + packages" signal, not a runtime claim. Not a required + # check. Mirrors the maintainer's local build-mac configuration (Unix + # Makefiles, RelWithDebInfo, newest Homebrew gcc-N). + macos-build: + runs-on: macos-14 + env: + CCACHE_MAXSIZE: 500M + HOMEBREW_NO_AUTO_UPDATE: "1" + HOMEBREW_NO_INSTALL_CLEANUP: "1" + HOMEBREW_NO_ANALYTICS: "1" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install dependencies + run: | + echo "CCACHE_DIR=$GITHUB_WORKSPACE/.ccache" >> "$GITHUB_ENV" + brew install gcc cmake sdl2 sdl3 ccache + ls "$(brew --prefix)"/bin/gcc-[0-9]* "$(brew --prefix)"/bin/g++-[0-9]* + + - name: Restore ccache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .ccache + key: ccache-macos-arm64-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }} + restore-keys: | + ccache-macos-arm64-${{ hashFiles('CMakeLists.txt') }}- + ccache-macos-arm64- + + - name: Regenerate ROM-asset symbol file (US) + run: python3 scripts/gen_romassets.py u + + # Same frozen first-run converter as linux-build. The runner's python3 + # is Homebrew's (PEP 668, no system pip installs), so use a venv. + - name: Freeze asset converter (PyInstaller) + run: | + python3 -m venv "$RUNNER_TEMP/venv" + "$RUNNER_TEMP/venv/bin/pip" install --quiet pyinstaller==6.22.3 + "$RUNNER_TEMP/venv/bin/python" -m PyInstaller --onefile --noconfirm --clean --name ge007-convert --distpath tools_pc/dist/prepare-assets --workpath build-pyinst --specpath build-pyinst tools_pc/dist/prepare-assets/prepare-assets.py + ls -la tools_pc/dist/prepare-assets/ge007-convert + + - name: Configure (ntsc-final) + run: | + GCC="$(ls "$(brew --prefix)"/bin/gcc-[0-9]* | sort -t- -k2 -n | tail -1)" + GXX="$(ls "$(brew --prefix)"/bin/g++-[0-9]* | sort -t- -k2 -n | tail -1)" + echo "using $GCC / $GXX" + cmake -S . -B build-mac -DROMID=ntsc-final -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_COMPILER="$GCC" -DCMAKE_CXX_COMPILER="$GXX" + + - name: Build (ntsc-final) + run: cmake --build build-mac -j"$(sysctl -n hw.ncpu)" + + - name: ccache stats + run: ccache -s + + - name: Show binary + run: | + ls -la build-mac/ge007* || true + file build-mac/ge007* || true + otool -L build-mac/ge007.aarch64 + + - name: Package macOS bundle + run: | + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + V="${GITHUB_REF_NAME}" + else + V="0.0.0-ci.${GITHUB_SHA::7}" + fi + bash scripts/package-macos.sh build-mac dist "$V" + + - name: Upload bundle + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: goldeneye-pc-port-macos-arm64 + path: | + dist/*.tar.gz + dist/*.tar.gz.sha256 + if-no-files-found: error + retention-days: 14 + # Real Windows (MSYS2 MINGW64) build — the only platform the port is # developed and tested on. This compiles and LINKS the full executable. # @@ -417,14 +504,14 @@ jobs: # -level_09 soak, a golden-frame diff) would run on a self-hosted runner # instead. Not planned. - # Tags only. Attach the packaged Windows + Linux bundles to a DRAFT + # Tags only. Attach the packaged Windows + Linux + macOS bundles to a DRAFT # pre-release so a human can do the ROM smoke-test on each platform before # publishing. This job — and only this job — gets contents: write, and it # is also the only job that attests build provenance (id-token: write + # attestations: write; attest-build-provenance needs both). release: if: github.ref_type == 'tag' - needs: [ changes, validate, windows-build, linux-build ] + needs: [ changes, validate, windows-build, linux-build, macos-build ] runs-on: ubuntu-22.04 permissions: contents: write @@ -454,6 +541,11 @@ jobs: name: goldeneye-pc-port-linux-x86_64 path: dist + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: goldeneye-pc-port-macos-arm64 + path: dist + # SLSA build provenance for the release artifacts (in-toto, signed by # GitHub's OIDC key for this repo's workflow at the tagged commit). # Anyone can check a downloaded file: @@ -474,7 +566,7 @@ jobs: ASSETS=(dist/*.zip dist/*.zip.sha256 dist/*.tar.gz dist/*.tar.gz.sha256) NOTES="$(mktemp)" # Asset names are built from the tag with the leading v stripped - # (bundle-{linux,win}.sh), so substitute the same form -- a raw + # (bundle-{linux,win}.sh, package-macos.sh), so substitute the same form -- a raw # TAG would make the notes reference goldeneye-pc-port-v*.* assets # that don't exist. NOTES_VERSION="${TAG#v}" diff --git a/scripts/gen_macho_syms.py b/scripts/gen_macho_syms.py new file mode 100644 index 00000000..30081ed8 --- /dev/null +++ b/scripts/gen_macho_syms.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Convert ELF/PE GAS absolute symbols to Mach-O spelling. + +Also accepts C-preprocessed input (`cc -E` on a .S, the dram_syms path on +Apple): gcc's `# N "file"` line markers pass through as harmless `#` +comments, and C-style integer suffixes in `.set` expressions are stripped +(`0x70000000UL` -> `0x70000000`) so the Mach-O assembler never has to parse +them.""" + +import pathlib +import re +import sys + + +def main() -> int: + argv = sys.argv[1:] + base = 0 + if argv and argv[0] == "--base": + if len(argv) < 2: + print(f"usage: {sys.argv[0]} [--base N] INPUT OUTPUT", file=sys.stderr) + return 2 + base = int(argv[1], 0) + argv = argv[2:] + if len(argv) != 2: + print(f"usage: {sys.argv[0]} [--base N] INPUT OUTPUT", file=sys.stderr) + return 2 + + source = pathlib.Path(argv[0]) + output = pathlib.Path(argv[1]) + lines = [] + + for line in source.read_text(encoding="utf-8").splitlines(): + if re.match(r"^\s*\.section\s+\.data\s*$", line): + continue + + match = re.match(r"^(\s*)\.global\s+([A-Za-z_]\w*)\s*$", line) + if match: + lines.append(f"{match.group(1)}.globl _{match.group(2)}") + continue + + match = re.match( + r"^(\s*)\.set\s+([A-Za-z_]\w*)(\s*,.*)$", line + ) + if match: + expr = re.sub( + r"\b(0[xX][0-9a-fA-F]+|\d+)(?:[uU][lL]{1,2}|[uUlL])\b", r"\1", match.group(3) + ) + if base: + # Add the window base to the expression's first numeric + # literal (romassets lines are a single literal; dram lines + # never take --base — see the docstring). + expr = re.sub( + r"(0[xX][0-9a-fA-F]+|\d+)", + lambda m, _b=base: f"0x{(int(m.group(1), 0) + _b):X}", + expr, + count=1, + ) + lines.append( + f"{match.group(1)}.set _{match.group(2)}{expr}" + ) + continue + + lines.append(line) + + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text("\n".join(lines) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/package-macos.sh b/scripts/package-macos.sh new file mode 100755 index 00000000..f5feb479 --- /dev/null +++ b/scripts/package-macos.sh @@ -0,0 +1,217 @@ +#!/usr/bin/env bash +# +# Package a macOS (Apple Silicon) GoldenEye 007 PC port build for distribution. +# Runs ON a Mac, after the build: +# +# scripts/package-macos.sh BUILD_DIR OUT_DIR [VERSION] +# +# BUILD_DIR cmake build directory containing ge007.aarch64 +# OUT_DIR where to write the bundle + tarball (created if missing) +# VERSION defaults to `git describe` +# +# Produces, under OUT_DIR: +# goldeneye-pc-port--macos-arm64/ the unpacked bundle +# goldeneye-pc-port--macos-arm64.tar.gz + .tar.gz.sha256 +# +# Same shape as tools_pc/bundle-linux.sh: engine exe, the non-system dylibs it +# needs (copied next to the exe, install names rewritten to @executable_path / +# @loader_path so nothing resolves into Homebrew), README, license texts, the +# prepare-assets tool and tools/eep_convert.py. NO ROM and NO game data. +# Every touched Mach-O is re-signed ad-hoc (mandatory on arm64 after +# install_name_tool). The bundle is NOT notarized; see the README it writes. +# +# Env: GE_REPO_ROOT overrides the repo root (default: parent of this script). +# GE_ALLOW_NO_CONVERTER=1 permits packaging without the PyInstaller-frozen +# ge007-convert (local testing only; CI always freezes it). +set -euo pipefail + +[ "$(uname -s)" = "Darwin" ] || { echo "error: this script runs on macOS only" >&2; exit 1; } +[ $# -ge 2 ] || { echo "usage: $0 BUILD_DIR OUT_DIR [VERSION]" >&2; exit 1; } +BUILD_DIR="$1"; OUT_DIR="$2" +[ -d "$BUILD_DIR" ] || { echo "error: build dir not found: $BUILD_DIR" >&2; exit 1; } +mkdir -p "$OUT_DIR" +BUILD_DIR="$(cd "$BUILD_DIR" && pwd)"; OUT_DIR="$(cd "$OUT_DIR" && pwd)" + +REPO_ROOT="${GE_REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +cd "$REPO_ROOT" + +VERSION="${3:-$(git describe --tags --always --dirty 2>/dev/null || echo 0.0.0-dev)}" +VERSION="${VERSION#v}" +NAME="goldeneye-pc-port-${VERSION}-macos-arm64" +OUT="${OUT_DIR}/${NAME}" + +EXE="" +for cand in "$BUILD_DIR"/ge007.aarch64 "$BUILD_DIR"/ge007*; do + if [ -f "$cand" ] && [ -x "$cand" ]; then EXE="$cand"; break; fi +done +[ -n "$EXE" ] || { echo "error: no ge007 binary in $BUILD_DIR - build first" >&2; exit 1; } +EXE_NAME="$(basename "$EXE")" +file "$EXE" | grep -q arm64 || { echo "error: $EXE is not an arm64 Mach-O" >&2; exit 1; } + +echo "==> Bundling $EXE -> $NAME" +rm -rf "$OUT" +mkdir -p "$OUT/licenses" +cp "$EXE" "$OUT/$EXE_NAME" +chmod 755 "$OUT/$EXE_NAME" + +# --- dylib bundling ---------------------------------------------------- +is_system() { case "$1" in /usr/lib/*|/System/*) return 0 ;; *) return 1 ;; esac; } + +deps_of() { otool -L "$1" | tail -n +2 | awk '{print $1}'; } +rpaths_of() { otool -l "$1" | awk '/cmd LC_RPATH/ {f=1} f && /path / {print $2; f=0}'; } + +# Resolve an install name to a real file, searching the dependent's rpaths and +# the Homebrew gcc runtime dir (libgcc_s / libstdc++ are @rpath-linked there). +resolve() { + local dep="$1" from="$2" r cand + case "$dep" in + @rpath/*) + for r in $(rpaths_of "$from"); do + cand="${r/@loader_path/$(dirname "$from")}" + cand="${cand/@executable_path/$(dirname "$from")}/${dep#@rpath/}" + [ -f "$cand" ] && { echo "$cand"; return 0; } + done + for cand in /opt/homebrew/opt/gcc/lib/gcc/current/"${dep#@rpath/}" \ + /opt/homebrew/lib/"${dep#@rpath/}" /usr/local/lib/"${dep#@rpath/}"; do + [ -f "$cand" ] && { echo "$cand"; return 0; } + done + return 1 ;; + @loader_path/*) + cand="$(dirname "$from")/${dep#@loader_path/}" + [ -f "$cand" ] && { echo "$cand"; return 0; } + return 1 ;; + /*) [ -f "$dep" ] && { echo "$dep"; return 0; }; return 1 ;; + esac + return 1 +} + +BUNDLED=" " +SIGN_LIST=() +bundle_dep() { # $1 = file (already inside $OUT) whose deps to process; $2 = new-name prefix + local file="$1" prefix="$2" dep real base + while IFS= read -r dep; do + is_system "$dep" && continue + case "$dep" in "@executable_path/"*|"@loader_path/"*) continue ;; esac + real="$(resolve "$dep" "$file")" || { echo "error: cannot resolve $dep (needed by $file)" >&2; exit 1; } + real="$(cd "$(dirname "$real")" && pwd -P)/$(basename "$real")" + base="$(basename "$dep")" + if [[ "$BUNDLED" != *" $base "* ]]; then + BUNDLED+="$base " + cp -L "$real" "$OUT/$base"; chmod 644 "$OUT/$base" + install_name_tool -id "@loader_path/$base" "$OUT/$base" 2>/dev/null + echo " + $base (from $real)" + SIGN_LIST+=("$OUT/$base") + bundle_dep "$OUT/$base" "@loader_path" + fi + install_name_tool -change "$dep" "$prefix/$base" "$file" 2>/dev/null + done < <(deps_of "$file") +} + +# Drop any rpath that points outside the bundle (Homebrew, build tree). +strip_rpaths() { local r; for r in $(rpaths_of "$1"); do install_name_tool -delete_rpath "$r" "$1" 2>/dev/null; done; } + +# Homebrew's sdl2 is sdl2-compat: libSDL2 dlopen()s @loader_path/libSDL3.dylib at runtime, so it +# is not in otool -L. Bundle it too; sdl2-compat looks next to itself first. +bundle_sdl3_if_needed() { + local sdl2 sdl3="" d + sdl2="$(ls "$OUT"/libSDL2-*.dylib 2>/dev/null | head -1 || true)" + [ -n "$sdl2" ] || return 0 + strings -a "$sdl2" | grep -q 'libSDL3' || return 0 + for d in /opt/homebrew/opt/sdl3/lib /opt/homebrew/lib /usr/local/lib; do + if [ -f "$d/libSDL3.0.dylib" ]; then sdl3="$d/libSDL3.0.dylib"; break; fi + done + [ -n "$sdl3" ] || { echo "error: sdl2-compat found but libSDL3.0.dylib is not installed" >&2; exit 1; } + cp -L "$sdl3" "$OUT/libSDL3.dylib"; chmod 644 "$OUT/libSDL3.dylib" + install_name_tool -id "@loader_path/libSDL3.dylib" "$OUT/libSDL3.dylib" 2>/dev/null + echo " + libSDL3.dylib (sdl2-compat runtime dependency, from $sdl3)" + SIGN_LIST+=("$OUT/libSDL3.dylib") + bundle_dep "$OUT/libSDL3.dylib" "@loader_path" +} + +bundle_dep "$OUT/$EXE_NAME" "@executable_path" +bundle_sdl3_if_needed +strip_rpaths "$OUT/$EXE_NAME" +for f in ${SIGN_LIST[@]+"${SIGN_LIST[@]}"}; do strip_rpaths "$f"; done + +# Ad-hoc sign everything we rewrote (libs first, exe last). Without this an +# arm64 binary is killed on launch ("Killed: 9"). +for f in ${SIGN_LIST[@]+"${SIGN_LIST[@]}"} "$OUT/$EXE_NAME"; do + codesign --force -s - "$f" 2>&1 | sed 's/^/ /' +done + +# Guard: nothing may still point into Homebrew / the build machine. +for f in ${SIGN_LIST[@]+"${SIGN_LIST[@]}"} "$OUT/$EXE_NAME"; do + if otool -L "$f" | tail -n +2 | awk '{print $1}' | grep -Eq '^(/opt/|/usr/local/|/Users/|/private/|@rpath/)'; then + echo "error: $f still references a non-system path:" >&2; otool -L "$f" >&2; exit 1 + fi + codesign --verify "$f" || { echo "error: bad signature on $f" >&2; exit 1; } +done +echo " remaining system dependencies:" +otool -L "$OUT/$EXE_NAME" | tail -n +2 | awk '{print " " $1}' | grep -v '@executable_path' || true + +# --- docs + licenses --------------------------------------------------- +DEPS_BLOCK=$'## macOS (Apple Silicon, experimental)\n\nThis is an experimental build for Macs with Apple Silicon (M1 or later,\nmacOS @MINOS@ or newer). SDL2 and the C++ runtime are bundled in this folder and\nfound automatically; nothing needs installing, and Homebrew is not required.\n\nThe app is not signed with an Apple Developer ID or notarized, so Gatekeeper\nblocks it when this folder came from a download. After unpacking, open Terminal\nin this folder and run, once:\n\n xattr -dr com.apple.quarantine .\n\nThen run `./@EXE@` from Terminal.\n' +MINOS="$(for f in ${SIGN_LIST[@]+"${SIGN_LIST[@]}"} "$OUT/$EXE_NAME"; do otool -l "$f" | awk '/LC_BUILD_VERSION/ {f=1} f && /minos/ {print $2; exit}'; done | sort -V | tail -1)" +echo " minimum macOS (highest minos of the bundled Mach-Os): $MINOS" +DEPS_BLOCK="${DEPS_BLOCK//@EXE@/$EXE_NAME}" +DEPS_BLOCK="${DEPS_BLOCK//@MINOS@/$MINOS}" +sed -e "s|@VERSION@|${VERSION}|g" \ + -e "s|@PLATFORM@|macOS Apple Silicon (arm64, experimental)|g" \ + -e "s|@EXE@|${EXE_NAME}|g" \ + -e "s|@LICENSE_EXTRA@||g" \ + tools_pc/dist/README.md.in > "$OUT/README.md.tmp" +# BSD awk rejects newlines in -v; pass the block via the environment. +DEPS_BLOCK="$DEPS_BLOCK" awk '{ if ($0 == "@DEPS@") print ENVIRON["DEPS_BLOCK"]; else if ($0 == "@DECK@") print ""; else print }' \ + "$OUT/README.md.tmp" > "$OUT/README.md" +rm -f "$OUT/README.md.tmp" + +cp NOTICE "$OUT/licenses/NOTICE" +cp LICENSE "$OUT/licenses/LICENSE-port-MIT.txt" +[ -f port/fast3d/LICENSE.txt ] && cp port/fast3d/LICENSE.txt "$OUT/licenses/LICENSE-fast3d.txt" +# SDL2 is zlib-licensed; carry its text when the Homebrew prefix has one. +for d in /opt/homebrew/opt/sdl2/LICENSE.txt /opt/homebrew/opt/sdl2-compat/LICENSE.txt /opt/homebrew/opt/sdl2/share/licenses/*/LICENSE.txt; do + if [ -f "$d" ]; then cp "$d" "$OUT/licenses/SDL2.txt"; break; fi +done + +# --- save converter + asset-prep tool (same assembly as bundle-linux.sh) --- +mkdir -p "$OUT/tools" +cp tools_pc/eep_convert.py "$OUT/tools/" +PREP="$OUT/prepare-assets" +mkdir -p "$PREP/vendor/scripts" "$PREP/vendor/assets/obseg" +cp tools_pc/dist/prepare-assets/prepare-assets.py "$PREP/" +cp tools_pc/d43_emit.py tools_pc/d69_emit.py tools_pc/d88_emit.py tools_pc/d88_propdefs.py "$PREP/" +cp scripts/filelist.u.csv "$PREP/vendor/scripts/" +cp assets/obseg/file_resource_table.inc.c "$PREP/vendor/assets/obseg/" +# BSD cp has no --parents; cpio -pdm recreates the relative paths. +find assets -iname 'modelfileheader.inc.c' -print | cpio -pdm "$PREP/vendor" 2>/dev/null +nmh="$(find "$PREP/vendor/assets" -iname 'modelfileheader.inc.c' | wc -l | tr -d ' ')" +echo " + prepare-assets/ (emit scripts + $nmh model headers)" +[ "$nmh" -gt 400 ] || { echo "error: prepare-assets vendored only $nmh model headers - expected ~512" >&2; exit 1; } + +CV="$REPO_ROOT/tools_pc/dist/prepare-assets/ge007-convert" +if [ -f "$CV" ]; then + cp "$CV" "$PREP/"; chmod 755 "$PREP/ge007-convert" + codesign --force -s - "$PREP/ge007-convert" 2>&1 | sed 's/^/ /' || true + echo " + prepare-assets/ge007-convert ($(du -h "$CV" | cut -f1))" +elif [ "${GE_ALLOW_NO_CONVERTER:-0}" = "1" ]; then + echo " ! ge007-convert missing (allowed by GE_ALLOW_NO_CONVERTER=1)" +else + echo "error: $CV not found - freeze it with PyInstaller first (see ci.yml macos-build)" >&2; exit 1 +fi + +# --- guards ------------------------------------------------------------ +if find "$OUT" -type f \( -iname '*.z64' -o -iname '*.n64' -o -iname '*.v64' \) | grep -q .; then + echo "error: bundle contains a ROM image - aborting" >&2; exit 1 +fi +KIB="$(du -sk "$OUT" | cut -f1)" +[ "$KIB" -le $((120 * 1024)) ] || { echo "error: bundle is ${KIB} KiB (> 120 MB) - likely contains game data" >&2; exit 1; } + +# --- tarball + checksum ------------------------------------------------ +# COPYFILE_DISABLE keeps AppleDouble ._* files out of the archive. +( cd "$OUT_DIR" && rm -f "${NAME}.tar.gz" "${NAME}.tar.gz.sha256" \ + && COPYFILE_DISABLE=1 tar -czf "${NAME}.tar.gz" "$NAME" \ + && shasum -a 256 "${NAME}.tar.gz" > "${NAME}.tar.gz.sha256" ) + +echo "==> ${OUT_DIR}/${NAME}.tar.gz ($(du -h "${OUT_DIR}/${NAME}.tar.gz" | cut -f1))" +cat "${OUT_DIR}/${NAME}.tar.gz.sha256" +( cd "$OUT_DIR" && find "$NAME" -type f | sort ) diff --git a/scripts/strip_weak_pragmas.py b/scripts/strip_weak_pragmas.py new file mode 100644 index 00000000..335a1631 --- /dev/null +++ b/scripts/strip_weak_pragmas.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Remove ELF-style weak aliases from a C source copied for Mach-O.""" + +import pathlib +import re +import sys + + +def main() -> int: + if len(sys.argv) != 3: + print(f"usage: {sys.argv[0]} INPUT OUTPUT", file=sys.stderr) + return 2 + + source = pathlib.Path(sys.argv[1]) + output = pathlib.Path(sys.argv[2]) + text = source.read_text(encoding="utf-8") + text = re.sub(r"^[ \t]*#pragma[ \t]+weak[^\n]*\n", "", text, + flags=re.MULTILINE) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(text, encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools_pc/README.md b/tools_pc/README.md index efb4f23b..083955c4 100644 --- a/tools_pc/README.md +++ b/tools_pc/README.md @@ -31,6 +31,8 @@ investigation artifacts. | `framediff.py` | Visual regression: candidate `GE_PCDUMP` frames vs `tools_pc/golden/`, per-region divergence. Structural/tolerant by default; `--exact` after `GE_DETERM`. | | `pixcount.py` | Count non-black pixels in a PPM dump — "did the scene render anything" as a number. | | `level_sweep.sh` | Bare `-level_XX` boot of all 21 solo levels → PASS / NO-FRAMES / CRASH. Predates `verify.sh sweep`; kept as the battle-tested full-21-level runner until `verify.sh sweep` has done an equivalent soak. | +| `levelbench.sh` | Boot one solo level, run a scripted input pattern (`fight` / `turn` / `idle` / `raw:`) or a recorded demo (`--record` / `--replay`, pad input incl. dedicated use, pinned `GE_RSEED`), quit via `GE_QUITFRAME`, keep `ge007.log` + a cadence/SLOW summary. Windows, Linux, Deck. | +| `deckrunner.sh` | Steam Deck Game Mode job runner: add it to Steam as a non-Steam game and start it; it runs `levelbench.sh` jobs dropped into `~/lb/queue/` over ssh (gamescope hides ssh-launched windows). | | `playtest.sh` | Launch a level for `docs/dev/LEVEL-PLAYTEST.md` human validation (WS6). | | `audiodebug.ps1` | Audio counterpart to `debug.ps1` (that one is for crashes; this one is for audio behaviour). Drives the `GE_D204` health monitor + `GE_AUDIOTRACE`/`GE_AUDIODUMP` and prints a pass/fail verdict: real-time ratio, queue starvation, dropped blocks, oversized blocks, plus a soundIndex histogram. `-Play` for an instrumented interactive playtest (the D202 workflow), `-Soak` for a 5-min stability run, `-SyncData` to mirror `./data` first. | | `debug.ps1` / `repro_gdb.sh` / `attach_animgen.sh` | Launch (or attach to) the game under gdb so a crash always leaves a backtrace. | diff --git a/tools_pc/abi_baseline.json b/tools_pc/abi_baseline.json index 68268318..16488044 100644 --- a/tools_pc/abi_baseline.json +++ b/tools_pc/abi_baseline.json @@ -2,10 +2,12 @@ "assets/animationtable_data.h": { "-Wint-conversion": 25 }, + "port/include/portaddr.h": { + "-Wpointer-to-int-cast": 1 + }, "port/shim/PR/os.h": { "-Wint-conversion": 3, - "-Wint-to-pointer-cast": 2, - "-Wpointer-to-int-cast": 1 + "-Wint-to-pointer-cast": 1 }, "port/src/romdata.c": { "-Wpointer-to-int-cast": 7 @@ -26,16 +28,15 @@ }, "src/game/bg.c": { "-Wint-conversion": 3, - "-Wpointer-to-int-cast": 6 + "-Wpointer-to-int-cast": 7 }, "src/game/blood_decrypt.c": { "-Wint-conversion": 2, "-Wpointer-to-int-cast": 2 }, "src/game/bondview2.c": { - "-Wint-conversion": 1, "-Wint-to-pointer-cast": 2, - "-Wpointer-to-int-cast": 8 + "-Wpointer-to-int-cast": 11 }, "src/game/bondview_r.c": { "-Wpointer-to-int-cast": 1 @@ -52,9 +53,8 @@ "src/game/chrai.c": { "-Wint-conversion": 2 }, - "src/game/cleanup_objects.c": { - "-Wint-conversion": 1, - "-Wpointer-to-int-cast": 1 + "src/game/front.c": { + "-Wpointer-to-int-cast": 2 }, "src/game/glass.c": { "-Wpointer-to-int-cast": 2 @@ -63,7 +63,7 @@ "-Wint-conversion": 2 }, "src/game/image.c": { - "-Wint-conversion": 3, + "-Wint-conversion": 1, "-Wpointer-to-int-cast": 3 }, "src/game/image_bank.c": { @@ -107,7 +107,7 @@ }, "src/game/model.c": { "-Wint-conversion": 9, - "-Wint-to-pointer-cast": 3, + "-Wint-to-pointer-cast": 1, "-Wpointer-to-int-cast": 3 }, "src/game/mpmenu.c": { @@ -117,12 +117,9 @@ "-Wint-to-pointer-cast": 1, "-Wpointer-to-int-cast": 18 }, - "src/game/othermodemicrocode.c": { - "-Wint-conversion": 1 - }, "src/game/prop.c": { "-Wint-conversion": 2, - "-Wpointer-to-int-cast": 21 + "-Wpointer-to-int-cast": 20 }, "src/game/propobj.c": { "-Wint-conversion": 1, @@ -141,7 +138,7 @@ }, "src/game/stan.c": { "-Wint-conversion": 3, - "-Wpointer-to-int-cast": 1 + "-Wpointer-to-int-cast": 4 }, "src/game/tex.c": { "-Wpointer-to-int-cast": 2 @@ -150,9 +147,9 @@ "-Wpointer-to-int-cast": 4 }, "src/game/title.c": { - "-Wint-conversion": 6, - "-Wint-to-pointer-cast": 2, - "-Wpointer-to-int-cast": 7 + "-Wint-conversion": 3, + "-Wint-to-pointer-cast": 1, + "-Wpointer-to-int-cast": 6 }, "src/game/viewport.c": { "-Wint-conversion": 1 diff --git a/tools_pc/deckrunner.sh b/tools_pc/deckrunner.sh new file mode 100644 index 00000000..82d34552 --- /dev/null +++ b/tools_pc/deckrunner.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# deckrunner.sh -- Game Mode job runner for tools_pc/levelbench.sh. +# Gamescope only shows windows Steam launched, so a harness started over ssh +# runs hidden (and throttled). Add this script to Steam once as a non-Steam +# game and start it; it then runs queued benches as its own children, which +# gamescope shows and paces like a normal game. +# +# Queue a job over ssh: one line of levelbench.sh arguments per file, e.g. +# echo '--secs 45 bunker1' > ~/lb/queue/010-bunker1.job +# Jobs run in name order; results land in ~/lb// (summary.txt), +# the job file moves to ~/lb/done/. `touch ~/lb/queue/stop` exits the runner +# (back to Steam). Idle-exits after 30 min with no jobs. +set -u +Q=~/lb/queue; D=~/lb/done +mkdir -p "$Q" "$D" +GAME=${GE_GAMEDIR:-$HOME/ge007-v050} +idle=0 +echo "deckrunner: up $(date)" >> ~/lb/runner.log +while [ $idle -lt 1800 ]; do + if [ -e "$Q/stop" ]; then rm -f "$Q/stop"; break; fi + job=$(ls "$Q"/*.job 2>/dev/null | sort | head -1) + if [ -z "$job" ]; then sleep 2; idle=$((idle + 2)); continue; fi + idle=0 + name=$(basename "$job" .job) + args=$(head -1 "$job") + mv "$job" "$D/" + echo "deckrunner: $name: $args" >> ~/lb/runner.log + rm -rf ~/lb/"$name" + # shellcheck disable=SC2086 # job line is word-split on purpose + bash ~/levelbench.sh --dir "$GAME" --out ~/lb/"$name" $args > ~/lb/"$name".console 2>&1 + echo "deckrunner: $name done" >> ~/lb/runner.log + touch ~/lb/"$name".finished +done +echo "deckrunner: exit $(date)" >> ~/lb/runner.log diff --git a/tools_pc/levelbench.sh b/tools_pc/levelbench.sh new file mode 100644 index 00000000..917722f2 --- /dev/null +++ b/tools_pc/levelbench.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# levelbench.sh -- boot one solo level, run a scripted input pattern, quit +# cleanly, and keep the log + a short summary. Runs on the machine with the +# game (Windows Git Bash/MSYS2, Linux, Steam Deck). For the Deck from the PC +# use tools_pc/deckbench.sh, which copies this script over and runs it. +# +# levelbench.sh [options] [ENV=VAL ...] +# --secs N run length in game seconds (default 45; GE_QUITFRAME = 60*N) +# --script NAME fight (default) | idle | turn | raw: +# --exe PATH game binary (default: ./ge007.x86_64[.exe] in --dir) +# --dir DIR game dir holding data/ (default: cwd) +# --out DIR results dir (default: /levelbench/-