forked from Ports/goldeneye-pc-port
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
This commit is contained in:
1 parent
859a777d61
commit
62c56cf886
10 files changed
+703
-22
No files matched your search
@@ -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}"
|
||||
|
||||
@@ -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())
|
||||
Executable
+217
@@ -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-<VERSION>-macos-arm64/ the unpacked bundle
|
||||
# goldeneye-pc-port-<VERSION>-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 )
|
||||
@@ -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())
|
||||
@@ -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. |
|
||||
|
||||
+15
-18
@@ -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
|
||||
|
||||
@@ -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/<job-name>/ (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
|
||||
@@ -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] <level-number|name> [ENV=VAL ...]
|
||||
# --secs N run length in game seconds (default 45; GE_QUITFRAME = 60*N)
|
||||
# --script NAME fight (default) | idle | turn | raw:<GE_INPUTSCRIPT string>
|
||||
# --exe PATH game binary (default: ./ge007.x86_64[.exe] in --dir)
|
||||
# --dir DIR game dir holding data/ (default: cwd)
|
||||
# --out DIR results dir (default: <dir>/levelbench/<level>-<time>)
|
||||
# --record FILE human plays (no script); pad input saved to FILE
|
||||
# --replay FILE replay a --record file instead of a script
|
||||
# (both pin GE_RSEED so guards make the same choices)
|
||||
# --gamescope wrap in nested `gamescope -r 90 -w 1280 -h 800 -f`
|
||||
# (Deck desktop mode: approximates Game Mode pacing)
|
||||
#
|
||||
# Script timing is wall-clock ms since SDL init (GE_INPUTSCRIPT_MS=1), so
|
||||
# load-time differences between machines don't shift it. "fight": taps Z
|
||||
# every 1.5 s for the first 20 s (skips the intro cutscene; fires once in
|
||||
# play), then 1.2 s Z holds every 4 s, so guards hear shots and start their
|
||||
# patterns. Z is fire in every control style and harmless in cutscenes.
|
||||
set -u
|
||||
SECS=45; SCRIPT=fight; REC=; REP=; EXE=; DIR=$PWD; OUT=; GSCOPE=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case $1 in
|
||||
--secs) SECS=$2; shift 2;;
|
||||
--script) SCRIPT=$2; shift 2;;
|
||||
--exe) EXE=$2; shift 2;;
|
||||
--dir) DIR=$2; shift 2;;
|
||||
--out) OUT=$2; shift 2;;
|
||||
--record) REC=$2; SCRIPT=none; shift 2;;
|
||||
--replay) REP=$2; SCRIPT=none; shift 2;;
|
||||
--gamescope) GSCOPE=1; shift;;
|
||||
-h|--help) sed -n 2,22p "$0"; exit 0;;
|
||||
*) break;;
|
||||
esac
|
||||
done
|
||||
[ $# -ge 1 ] || { echo "usage: $0 [options] <level> [ENV=VAL ...]" >&2; exit 2; }
|
||||
LVL=$1; shift
|
||||
case ${LVL,,} in
|
||||
dam) LVL=33;; facility) LVL=34;; runway) LVL=35;; surface1) LVL=36;;
|
||||
bunker1) LVL=09;; silo) LVL=20;; frigate) LVL=26;; surface2) LVL=43;;
|
||||
bunker2) LVL=27;; statue) LVL=22;; archives) LVL=24;; streets) LVL=29;;
|
||||
depot) LVL=30;; train) LVL=25;; jungle) LVL=37;; control) LVL=23;;
|
||||
caverns) LVL=39;; cradle) LVL=41;; aztec) LVL=28;; egypt) LVL=32;; cuba) LVL=54;;
|
||||
esac
|
||||
cd "$DIR" || exit 2
|
||||
if [ -z "$EXE" ]; then
|
||||
for c in ./ge007.x86_64 ./ge007.x86_64.exe; do [ -x "$c" ] && EXE=$c && break; done
|
||||
fi
|
||||
[ -n "$EXE" ] || { echo "no game binary in $DIR (use --exe)" >&2; exit 2; }
|
||||
END=$((SECS * 1000))
|
||||
|
||||
gen_script() {
|
||||
local s= t
|
||||
case $SCRIPT in
|
||||
none) ;;
|
||||
idle) s="1000:SNONE";;
|
||||
fight|turn)
|
||||
for ((t = 4000; t < 20000 && t < END; t += 1500)); do s+="$t:Z;"; done
|
||||
for ((t = 20000; t < END; t += 4000)); do
|
||||
s+="$t:ZHOLD;$((t + 1200)):ZREL;"
|
||||
if [ "$SCRIPT" = turn ]; then # sweep the view between bursts
|
||||
s+="$((t + 1500)):SLEFT;$((t + 2300)):SRIGHT;$((t + 3100)):SNONE;"
|
||||
fi
|
||||
done;;
|
||||
raw:*) s=${SCRIPT#raw:};;
|
||||
*) echo "unknown --script $SCRIPT" >&2; exit 2;;
|
||||
esac
|
||||
printf '%s' "${s%;}"
|
||||
}
|
||||
INPUT=$(gen_script)
|
||||
|
||||
[ -n "$OUT" ] || OUT="levelbench/L$LVL-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$OUT"
|
||||
# The game writes $S/ge007.log next to ge007.ini; find it after the run by mtime.
|
||||
STAMP="$OUT/.start"; : > "$STAMP"
|
||||
|
||||
CMD=("$EXE" "-level_$LVL")
|
||||
if [ "$GSCOPE" = 1 ]; then
|
||||
CMD=(gamescope -r 90 -w 1280 -h 800 -W 1280 -H 800 -f -- "${CMD[@]}")
|
||||
fi
|
||||
{
|
||||
echo "level=$LVL secs=$SECS script=$SCRIPT gamescope=$GSCOPE exe=$EXE"
|
||||
echo "binary md5=$(md5sum "$EXE" | cut -c1-8)"
|
||||
echo "extra env: $*"
|
||||
echo "GE_INPUTSCRIPT=$INPUT"
|
||||
} > "$OUT/run.txt"
|
||||
|
||||
IENV=(GE_INPUTSCRIPT_MS=1 "GE_INPUTSCRIPT=$INPUT")
|
||||
[ "$SCRIPT" = none ] && IENV=("GE_RSEED=${GE_RSEED:-0123456789abcdef}")
|
||||
[ -n "$REC" ] && IENV+=("GE_INPUTRECORD=$REC")
|
||||
[ -n "$REP" ] && IENV+=("GE_INPUTREPLAY=$REP")
|
||||
echo "input env: ${IENV[*]}" >> "$OUT/run.txt"
|
||||
env GE_QUITFRAME=$((SECS * 60)) "${IENV[@]}" "$@" \
|
||||
timeout $((SECS + 90)) "${CMD[@]}" > "$OUT/stdout.txt" 2>&1
|
||||
RC=$?
|
||||
echo "exit=$RC" >> "$OUT/run.txt"
|
||||
|
||||
LOG=$(find . -maxdepth 3 -name ge007.log -newer "$STAMP" 2>/dev/null | head -1)
|
||||
[ -z "$LOG" ] && [ -f "${XDG_DATA_HOME:-$HOME/.local/share}/ge007/ge007.log" ] &&
|
||||
LOG="${XDG_DATA_HOME:-$HOME/.local/share}/ge007/ge007.log"
|
||||
[ -n "$LOG" ] && cp "$LOG" "$OUT/ge007.log"
|
||||
for f in ge007.crash.log; do [ -f "$f" ] && [ "$f" -nt "$STAMP" ] && cp "$f" "$OUT/"; done
|
||||
rm -f "$STAMP"
|
||||
|
||||
L="$OUT/ge007.log"; [ -f "$L" ] || L="$OUT/stdout.txt"
|
||||
{
|
||||
cat "$OUT/run.txt"
|
||||
echo "--- exit: $(grep -m1 -E 'quit requested|video: exiting' "$L" || echo 'no clean quit line')"
|
||||
[ -f "$OUT/ge007.crash.log" ] && echo "!!! CRASH LOG PRESENT"
|
||||
echo "--- D578 cadence (one per ~10 s)"
|
||||
grep -E 'D578 cadence' "$L" | sed 's/^.*D578 cadence/cadence/'
|
||||
echo "--- D583 decisions"
|
||||
grep -E 'D583 decisions' "$L" | sed 's/^.*D583 decisions/decisions/' | tail -5
|
||||
n=$(grep -c 'D583 SLOW' "$L")
|
||||
echo "--- D583 SLOW frames: $n (worst 8 by draw time)"
|
||||
grep 'D583 SLOW' "$L" | awk '{ for (i = 1; i <= NF; i++) if ($i == "draw") { print $(i+1), $0; break } }' |
|
||||
sort -rn | head -8 | cut -d' ' -f2- | sed 's/^.*D583 SLOW/SLOW/'
|
||||
} > "$OUT/summary.txt"
|
||||
cat "$OUT/summary.txt"
|
||||
echo "results: $OUT"
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Offline test harness for the update-check parsing helpers
|
||||
* (port/include/updatecheck_parse.h). No network, no SDL: it exercises
|
||||
* versionNewer() and extractTag() on a table of cases and prints PASS/FAIL
|
||||
* per case.
|
||||
*
|
||||
* Build and run:
|
||||
* gcc -O2 -Wall -o <temp>/uctest tools_pc/updatecheck_test.c
|
||||
* <temp>/uctest
|
||||
*
|
||||
* Exits nonzero if any case fails.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../port/include/updatecheck_parse.h"
|
||||
|
||||
static int g_fail = 0;
|
||||
|
||||
static void checkVer(const char *tag, const char *running, int want)
|
||||
{
|
||||
int got = versionNewer(tag, running);
|
||||
if (got == want) {
|
||||
printf("PASS versionNewer(\"%s\", \"%s\") = %d\n", tag, running, got);
|
||||
} else {
|
||||
printf("FAIL versionNewer(\"%s\", \"%s\") = %d, expected %d\n",
|
||||
tag, running, got, want);
|
||||
g_fail++;
|
||||
}
|
||||
}
|
||||
|
||||
static void checkTag(const char *body, size_t outN, int wantOk, const char *want)
|
||||
{
|
||||
char out[64];
|
||||
memset(out, 0x5a, sizeof(out));
|
||||
int got = extractTag(body, out, outN);
|
||||
if (wantOk && got && strcmp(out, want) == 0) {
|
||||
printf("PASS extractTag -> \"%s\"\n", out);
|
||||
} else if (!wantOk && got == 0) {
|
||||
printf("PASS extractTag -> fail\n");
|
||||
} else {
|
||||
printf("FAIL extractTag: returned %d (out \"%s\"), expected %s\n",
|
||||
got, got ? out : "(unwritten)", wantOk ? want : "fail");
|
||||
g_fail++;
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* versionNewer(tag, running) */
|
||||
checkVer("v0.5.1", "0.5.0", 1);
|
||||
checkVer("v0.5.0", "0.5.0", 0);
|
||||
checkVer("v0.4.9", "0.5.0", 0);
|
||||
checkVer("v0.10.0", "0.9.9", 1);
|
||||
checkVer("v1.0", "0.9.9", 1);
|
||||
checkVer("v1.0.0", "1.0", 0);
|
||||
checkVer("V0.6.0", "0.5.1", 1);
|
||||
checkVer("v0.5.1-rc1", "0.5.1", 0);
|
||||
checkVer("garbage", "0.5.0", 0);
|
||||
checkVer("v0.5.1", "", 0);
|
||||
|
||||
/* extractTag(body) */
|
||||
checkTag("{\"tag_name\":\"v0.5.1\"}", 64, 1, "v0.5.1");
|
||||
checkTag("{\"tag_name\" : \"v0.5.1\"}", 64, 1, "v0.5.1");
|
||||
checkTag("{\"tag_name\":\n \"v0.5.1\"}", 64, 1, "v0.5.1");
|
||||
checkTag("{\"name\":\"x\"}", 64, 0, NULL);
|
||||
checkTag("{\"tag_name\":\"v0.5.1\x1b[31m\"}", 64, 0, NULL);
|
||||
checkTag("{\"tag_name\":\"\"}", 64, 0, NULL);
|
||||
checkTag("{\"tag_name\":\"v0.5.1", 64, 0, NULL);
|
||||
|
||||
/* 40-char tag, out size 32: must fail and must not overflow. */
|
||||
{
|
||||
char body[80];
|
||||
snprintf(body, sizeof(body), "{\"tag_name\":\"%040d\"}", 1);
|
||||
char out[64];
|
||||
memset(out, 0x5a, sizeof(out));
|
||||
int got = extractTag(body, out, 32);
|
||||
if (got == 0 && out[32] == (char)0x5a && out[63] == (char)0x5a) {
|
||||
printf("PASS extractTag 40-char tag, out size 32 -> fail, no overflow\n");
|
||||
} else {
|
||||
printf("FAIL extractTag 40-char tag, out size 32: returned %d "
|
||||
"(out[32]=%02x out[63]=%02x)\n",
|
||||
got, (unsigned char)out[32], (unsigned char)out[63]);
|
||||
g_fail++;
|
||||
}
|
||||
}
|
||||
|
||||
/* A ~300 KB body: "tag_name" in the first ~24 bytes, 300 KB of padding
|
||||
* after it. extractTag must still find the tag (and never needs the
|
||||
* tail, which is what the fetch path may have truncated). */
|
||||
{
|
||||
static const char pre[] = "{\"tag_name\":\"v0.5.1\",\"notes\":\"";
|
||||
static const char post[] = "\"}";
|
||||
enum { PAD = 300000 };
|
||||
char *body = malloc(strlen(pre) + PAD + strlen(post) + 1);
|
||||
if (!body) {
|
||||
printf("FAIL out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
strcpy(body, pre);
|
||||
memset(body + strlen(pre), 'x', PAD);
|
||||
strcpy(body + strlen(pre) + PAD, post);
|
||||
checkTag(body, 64, 1, "v0.5.1");
|
||||
free(body);
|
||||
}
|
||||
|
||||
if (g_fail) {
|
||||
printf("%d case(s) FAILED\n", g_fail);
|
||||
return 1;
|
||||
}
|
||||
printf("all cases passed\n");
|
||||
return 0;
|
||||
}
|
||||
+6
-1
@@ -270,7 +270,12 @@ stage_workdir() {
|
||||
cp "$ROOT/data/ge007.$ROMID.z64" "$d/data/" &&
|
||||
cp -r "$ROOT/data/pccg-$ROMID" "$ROOT/data/pcmodels-$ROMID" "$d/data/" &&
|
||||
cp "$CANON_EEP" "$d/data/ge007.eep" &&
|
||||
printf '[Window]\nWidth = 640\nHeight = 480\n' > "$d/data/ge007.ini"
|
||||
# Video.DeckPresetApplied: the D283 Steam Deck preset would otherwise switch
|
||||
# a fresh ini to fullscreen 1280x800 on a Deck, and no frame would compare.
|
||||
printf '[Window]\nWidth = 640\nHeight = 480\n[Video]\nDeckPresetApplied = 1\n' > "$d/data/ge007.ini" &&
|
||||
# VERIFY_INI_EXTRA: extra ini lines for an option-specific gate (D578:
|
||||
# '[Video]\nFpsCap = 120\nVSync = 0'); the goldens are captured without it.
|
||||
{ [ -z "${VERIFY_INI_EXTRA:-}" ] || printf '%b\n' "$VERIFY_INI_EXTRA" >> "$d/data/ge007.ini"; }
|
||||
}
|
||||
# EXIT only (D534): INT/TERM just exit, which fires EXIT.
|
||||
CUR_CAPDIR=""
|
||||
|
||||
Reference in new issue
Block a user