ci: macOS arm64 build + port_addr self-test on PRs into macos

Adds a GitHub-hosted macos-15 (Apple Silicon) workflow for pushes and PRs
into the macos branch: build with Homebrew GCC, once with PORT_ADDR_STRICT
off and once on, then run the ROM-free address-model self-test.

- main.c: new --selftest flag, handled right after portAddrInit() and
  before crashInit/config/ROM/SDL. Exits 0 if the self-test passed, 1 if
  not. Listed in --help.
- port_addr.c/.h: portAddrSelfTest() now returns its result and
  portAddrSelfTestPassed() exposes it; no change to normal runs.
- macos.yml: contents: read only, no secrets, no ROM, persist-credentials
  off, so it is safe for fork PRs. The strict entry also requires the
  GE_ADDRSTRICT_SELFTEST report, proving the validator is live.
- docs: building.md CI note; plan M4 row no longer lists macOS CI as
  outstanding (ROM sweeps stay with the self-hosted job in #131).
This commit is contained in:
Dan Turner committed 2026-10-04 11:21:37 +01:00
1 parent 92986a07a9
commit 7b08a04f54
6 files changed
+123 -3

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
name: macOS
on:
push:
branches: [ macos ]
pull_request:
branches: [ macos ]
workflow_dispatch:
# Minimal token: this job only reads the repo. It runs on GitHub-hosted
# runners only, never receives secrets, and never sees a ROM, so it is safe
# for fork PRs (untrusted PR code must not reach a box that holds a ROM).
permissions:
contents: read
concurrency:
group: macos-${{ github.ref }}
cancel-in-progress: true
# What this proves: the native Apple Silicon (arm64) port BUILDS and LINKS
# with Homebrew GCC, and the ROM-free address-model self-test passes
# (`ge007.aarch64 --selftest`: window round-trips, absolute-symbol base,
# image-relative encoding, portAddrIsInWindow). It does NOT run the game.
#
# Matrix:
# strict=OFF the default build.
# strict=ON -DPORT_ADDR_STRICT=ON; additionally proves the validator is
# live by feeding it a deliberately unmapped address
# (GE_ADDRSTRICT_SELFTEST=1) and requiring its report.
#
# Level runs need a ROM, so they live elsewhere: the self-hosted sweep
# (`platform=macos`, tools_pc/level_sweep_mac.sh) from #131, on a runner with
# its own ROM that never runs PR code.
jobs:
build:
runs-on: macos-15
strategy:
fail-fast: false
matrix:
strict: [ OFF, ON ]
env:
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 500M
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Install dependencies
run: brew install cmake gcc sdl2 ccache
- name: Restore ccache
uses: actions/cache@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ccache-macos-${{ matrix.strict }}-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
restore-keys: |
ccache-macos-${{ matrix.strict }}-${{ hashFiles('CMakeLists.txt') }}-
ccache-macos-${{ matrix.strict }}-
- name: Regenerate ROM-asset symbol file (US)
run: python3 scripts/gen_romassets.py u
- name: Configure (ntsc-final)
run: cmake -S . -B build-pc -DROMID=ntsc-final -DPORT_ADDR_STRICT=${{ matrix.strict }}
- name: Build
run: cmake --build build-pc -j"$(sysctl -n hw.ncpu)"
- name: ccache stats
run: ccache -s
- name: Version
run: ./build-pc/ge007.aarch64 --version
- name: Address-model self-test
run: ./build-pc/ge007.aarch64 --selftest
- name: PORT_ADDR_STRICT detector is live
if: matrix.strict == 'ON'
run: |
set -o pipefail
GE_ADDRSTRICT_SELFTEST=1 ./build-pc/ge007.aarch64 --selftest 2>&1 | tee selftest.log
grep -F 'PORT_ADDR_STRICT: 0x30000000 is not a mapped N64 address' selftest.log
+17
View File
@@ -213,6 +213,23 @@ GE_ADDRSTRICT_SELFTEST=1 ./build-strict/ge007.aarch64 -level_09
See findings MACOS-SFX-BOUND through MACOS-ADDR-STRICT for the bug family this exists to catch.
### macOS CI and the ROM-free self-test
`.github/workflows/macos.yml` runs on GitHub-hosted Apple Silicon for pushes and
PRs into the `macos` branch. It builds with Homebrew GCC (once with
`PORT_ADDR_STRICT` off, once on) and runs the address-model self-test, which
needs no ROM:
```sh
./build-pc/ge007.aarch64 --selftest # exit 0 = pass, 1 = fail
```
The self-test covers the window round-trips, the generated absolute-symbol base,
the image-relative encoding and `portAddrIsInWindow()`. With a `PORT_ADDR_STRICT`
build, `GE_ADDRSTRICT_SELFTEST=1` additionally proves the validator fires. Level
runs need a ROM and are covered by the self-hosted sweep
(PR #131, `tools_pc/level_sweep_mac.sh`), not by this job.
### Packaging a macOS app
```sh
+1 -1
View File
@@ -581,7 +581,7 @@ Record as a `Dxx`.
| M1 | **done** | shifted-window address model (MACOS-ADDR-WINDOW); self-test ALL PASS on the #88 base |
| M2 | **done** | game-code re-basing sweep complete; **21/21 solo levels boot, render and run crash-free**, including with full controller input |
| M3 | **done (core)** | #88's AppKit main-thread guard; `tools_pc/level_sweep_mac.sh` (crash + rendered-pixel check); the two Core-profile GL nits are cosmetic and parked in `docs/dev/GRAPHICS-BACKLOG.md` |
| M4 | **done (core)** | `tools_pc/bundle-mac.sh` → signed, double-clickable `GoldenEye.app` (+ zip/sha256); `docs/building.md` macOS section + `tools_pc/README.md`. Outstanding: CI for macOS, and a live run of the `.app` from Finder |
| M4 | **done (core)** | `tools_pc/bundle-mac.sh` → signed, double-clickable `GoldenEye.app` (+ zip/sha256); `docs/building.md` macOS section + `tools_pc/README.md`. CI: GitHub-hosted build + `--selftest` (`.github/workflows/macos.yml`); ROM sweeps via the self-hosted job (#131). Outstanding: a live run of the `.app` from Finder |
**Verified state (2026-09-18).** `build-pc/ge007.aarch64` (Homebrew GCC 16.2)
opens a window, renders, plays music **and sound effects**, and runs every solo
+4
View File
@@ -86,6 +86,10 @@ void portAddrStrictCheck(uint32_t a);
* any of the port's fixed mappings and before large allocations. */
void portAddrInit(void);
/* Result of the ROM-free self-test portAddrInit() ran (1 = all checks passed).
* Used by `--selftest` so CI can gate on it without a ROM. */
int portAddrSelfTestPassed(void);
#ifdef __cplusplus
}
#endif
+6
View File
@@ -68,6 +68,8 @@ static void portPrintHelp(const char *argv0)
printf("\nusage: %s [options] [-level_XX]\n\n"
" --help this message\n"
" --version build id only\n"
" --selftest run the ROM-free address-model self-test and\n"
" exit (0 = pass, 1 = fail); needs no ROM\n"
" -fresh wipe playtest data + config before starting\n"
" (removes ge007.eep save and ge007.ini; the\n"
" ini is re-written with defaults on exit)\n"
@@ -109,6 +111,10 @@ int main(int argc, char **argv)
* failure here is reported before SDL/GL are involved. */
portAddrInit();
if (sysArgCheck("--selftest")) {
return portAddrSelfTestPassed() ? 0 : 1;
}
/* Crash handler first, so any failure below is debuggable. */
crashInit();
+10 -2
View File
@@ -42,7 +42,7 @@ static int portAddrCheck(const char *what, int ok)
return ok;
}
static void portAddrSelfTest(void)
static int portAddrSelfTest(void)
{
const uintptr_t B = (uintptr_t)PORT_ADDR_BASE;
int ok = 1;
@@ -74,6 +74,14 @@ static void portAddrSelfTest(void)
}
sysLogPrintf(ok ? LOG_INFO : LOG_ERROR, "portAddr: selftest %s",
ok ? "ALL PASS" : "FAILURES");
return ok;
}
static int g_portAddrSelfTestOk = 0;
int portAddrSelfTestPassed(void)
{
return g_portAddrSelfTestOk;
}
#ifdef PORT_ADDR_STRICT
@@ -199,7 +207,7 @@ void portAddrInit(void)
(unsigned long long)PORT_ADDR_BASE,
(unsigned long long)g_portImageBase, g_portUseImageRel);
portAddrSelfTest();
g_portAddrSelfTestOk = portAddrSelfTest();
#ifdef PORT_ADDR_STRICT
portAddrStrictSelfTest();