macOS VM Testing Workflow
September 18, 2026 ยท View on GitHub
This workflow is the macOS counterpart to docs/dev/linux-mint-vmware-workflow.md.
It is intentionally SSH-based instead of VMware-on-Windows based: macOS test VMs
must run on Apple-branded hardware or a compliant Apple-hosted service.
The YouTube guide in issue/debugging discussions (TD3H2J65u90) describes
installing macOS Sequoia in VMware on a Windows PC. Do not use that path for
openQ4 automation. Keep this workflow on Apple hardware through Virtualization
framework, UTM, VMware Fusion, Parallels, or a hosted Mac provider.
Host And Media Layout
- Windows-side macOS installer/restore-image inventory:
E:\ISO\macos\ - Guest VM name recommendation:
openQ4-macOS - Guest user recommendation:
codex - Guest workspace:
~/openq4-work/ - Guest source checkout:
~/openq4-work/openQ4 - Guest GameLibs checkout:
~/openq4-work/openQ4-game - Guest Quake 4 assets:
~/openq4-work/Quake4 - Guest results:
~/openq4-work/results/
The guest scripts resolve ~ through HOME by default. If an SSH service,
launch agent, or hosted runner starts the scripts without HOME, pass
-MacHome /Users/<name> to the host workflow or set OPENQ4_GUEST_HOME when
running a guest script directly. The value must be an absolute macOS home
directory without control characters; otherwise the scripts fail before
creating work directories or launchers.
The host workflow applies the same control-character check inside its remote
archive-expansion and result-collection snippets when -MacHome is omitted and
they fall back to the SSH session's HOME.
E:\ISO\macos\ is only an inventory/staging location on the Windows host. The
actual VM creation still happens on the Apple host with the hypervisor you use
there. Store Apple restore images, IPSW files, installer app archives, or a
short README that points to the Apple host's VM storage in this directory.
Create The VM
On Apple hardware:
-
Create a macOS VM with Virtualization framework, UTM, VMware Fusion, or Parallels.
-
Allocate at least 4 vCPUs, 8 GB RAM, and 96 GB disk. Use 16 GB RAM if shader and packaging validation will run often.
-
Create a sudo-capable
codexuser. -
Enable Remote Login:
sudo systemsetup -setremotelogin on -
Install Xcode Command Line Tools:
xcode-select --install -
Optional but recommended: install Homebrew from
https://brew.sh/.
From the Windows host, check connectivity:
powershell -NoProfile -ExecutionPolicy Bypass -File tools/macos/Invoke-openQ4MacOSWorkflow.ps1 `
-Action Probe `
-MacHost <mac-vm-ip-or-hostname> `
-MacUser codex
Bootstrap, Assets, Build, And Test
Run the full setup from the Windows host:
powershell -NoProfile -ExecutionPolicy Bypass -File tools/macos/Invoke-openQ4MacOSWorkflow.ps1 `
-Action All `
-MacHost <mac-vm-ip-or-hostname> `
-MacUser codex
-Action All does the following:
- verifies the macOS command-line toolchain, including
xcrun,plutil,lipo,otool, andcodesign - installs or updates a user-local Meson/Ninja venv
- copies the Windows Quake 4 install into
~/openq4-work/Quake4 - copies
openQ4andopenQ4-gameinto the guest workspace - configures, builds, and stages openQ4 through
tools/build/meson_setup.sh - runs a renderer smoke profile against the copied Quake 4 assets
- runs a multiplayer
mp/q4dm1listen-server smoke profile against the copied Quake 4 assets - runs the experimental macOS-facing GL 4.1 renderer validation set
- creates
~/Desktop/openQ4.commandpointing at the staged runtime and assets - writes a bridge-specific runtime signoff report when running
AllorSignoff
Run narrower actions as needed:
powershell -NoProfile -ExecutionPolicy Bypass -File tools/macos/Invoke-openQ4MacOSWorkflow.ps1 -Action Bootstrap -MacHost <host>
powershell -NoProfile -ExecutionPolicy Bypass -File tools/macos/Invoke-openQ4MacOSWorkflow.ps1 -Action Assets -MacHost <host>
powershell -NoProfile -ExecutionPolicy Bypass -File tools/macos/Invoke-openQ4MacOSWorkflow.ps1 -Action Sync -MacHost <host>
powershell -NoProfile -ExecutionPolicy Bypass -File tools/macos/Invoke-openQ4MacOSWorkflow.ps1 -Action Build -MacHost <host>
powershell -NoProfile -ExecutionPolicy Bypass -File tools/macos/Invoke-openQ4MacOSWorkflow.ps1 -Action Smoke -MacHost <host>
powershell -NoProfile -ExecutionPolicy Bypass -File tools/macos/Invoke-openQ4MacOSWorkflow.ps1 -Action Renderer -MacHost <host>
powershell -NoProfile -ExecutionPolicy Bypass -File tools/macos/Invoke-openQ4MacOSWorkflow.ps1 -Action Launcher -MacHost <host>
powershell -NoProfile -ExecutionPolicy Bypass -File tools/macos/Invoke-openQ4MacOSWorkflow.ps1 -Action Signoff -MacHost <host> -MacOSOSMatrixRole latest-public-macos
powershell -NoProfile -ExecutionPolicy Bypass -File tools/macos/Invoke-openQ4MacOSWorkflow.ps1 -Action CollectResults -MacHost <host> -MacOSRunId <run-id>
To test only the Metal bridge package path:
powershell -NoProfile -ExecutionPolicy Bypass -File tools/macos/Invoke-openQ4MacOSWorkflow.ps1 `
-Action Sync,Build,Smoke,Renderer `
-MacHost <host> `
-MacOSGraphicsBridge metal
For a self-contained hardware signoff pass after syncing, run both bridge variants:
powershell -NoProfile -ExecutionPolicy Bypass -File tools/macos/Invoke-openQ4MacOSWorkflow.ps1 `
-Action Signoff `
-MacHost <host> `
-MacOSGraphicsBridge both `
-MacOSOSMatrixRole latest-public-macos
The signoff action builds and stages each selected bridge before running the
single-player smoke profile, the multiplayer mp/q4dm1 listen-server smoke
profile, and the experimental macOS-facing renderer matrix, installs the Desktop launcher,
records sw_vers, Xcode/macOS SDK, kernel, display, audio, USB, and Bluetooth inventory, and
writes bridge-specific reports under
~/openq4-work/results/<timestamp>-signoff-opengl/macos-runtime-signoff.md and
~/openq4-work/results/<timestamp>-signoff-metal/macos-runtime-signoff.md.
When -MacOSGraphicsBridge both is selected, OpenGL uses builddir-opengl and
Metal uses builddir-metal so Meson configuration state does not bleed between
variants. Use those reports for the manual hardware checklist: Finder/launcher
startup, real keyboard/mouse/controller input, audio output/device switching,
windowed and fullscreen display modes, HiDPI/Retina behavior, SP gameplay, MP
listen-server gameplay, dedicated-server startup where supported, and in-game
OpenGL or Metal bridge coverage beyond hosted CI before macOS support can move
from preview to first-class. The package UX part of that checklist follows
docs/dev/macos-package-layout-and-release-policy.md: launch openQ4.app from
the mounted signed/notarized DMG when one is available, drag only the app to
/Applications or another user-writable location and launch it there, copy the
whole payload separately to check loose client/server/support-tool sibling
discovery, confirm Terminal launch from an unrelated working directory, verify
embedded Contents/Resources and Contents/Frameworks paths plus
fs_basepath, fs_cdpath, and fs_savepath in runtime logs, and record
Gatekeeper behavior for the exact signed/notarized DMG or unsigned development
archive under test.
The metal bridge remains the OpenGL renderer path hosted through the SDL3/Cocoa
Metal bridge integration; it is not a native Metal renderer. Native
Cocoa/OpenGL backend runs with -Dplatform_backend=native are comparison-only
diagnostics and are not a supported release backend; they do not replace SDL3
OpenGL/Metal bridge package signoff.
Use -MacOSOSMatrixRole floor-candidate when validating the oldest documented
macOS floor, currently macOS 11, and -MacOSOSMatrixRole latest-public-macos
when validating the latest public macOS release. Hosted CI package runs may use
current-hosted-ci-runner, but hosted runner success is not a substitute for
floor/latest runtime signoff on compliant Apple hardware. Always pass an
explicit -MacOSOSMatrixRole on Signoff runs: omitting it leaves the guest
default current-manual-signoff, which counts toward neither the macOS floor
nor latest-public promotion evidence (see
docs/dev/macos-support-matrix-policy.md).
Guest paths passed through -MacWorkspace and -MacBasePath must be absolute POSIX paths or use a leading ~/; the host workflow rejects relative, dot-segment, empty-segment, control-character, and backslash paths, and the guest scripts recheck that the paths are absolute and control-character-free after ~ expansion before syncing source trees, installing assets, building, or collecting results. Keep -MacBasePath out of the workflow-reserved openQ4/, openQ4-game/, incoming-quake4/, and results/ children under -MacWorkspace; when -MacHome is provided, host preflight expands ~/ before that reserved-child comparison, and the guest scripts repeat the check after resolving the actual home directory. The asset installer refuses those targets because it stages extraction under the workspace and installs assets with rsync --delete. Source sync also trims trailing slashes before appending openQ4/ or openQ4-game/, so a workspace written as ~/openq4-work/ does not create double-slash remote extraction targets.
Keep -BuildDir pointed at a dedicated build output directory such as
builddir, builddir-opengl, or builddir-metal; the guest script refuses the
repo root, source/content/tool trees, .install, symlinks, and files as Meson
build directories. Host-side validation now also requires -BuildDir to be a
relative builddir* path or a path under .tmp/, matching the local validation
profile's build-output convention.
Source, GameLibs, and retail asset transfer archives are rechecked on the Apple
host before extraction. The workflow rejects malformed paths, control
characters, duplicate members, case-insensitive member collisions, macOS
metadata/debug sidecars, symlinks, hardlinks, and special files, then extracts
with COPYFILE_DISABLE=1 so AppleDouble sidecars do not appear during sync.
After Signoff or All, the host workflow automatically copies the matching
guest result directories into a compressed archive at
.tmp/macos-vm/results/openq4-macos-results-<run-id>.tar.gz. Use
-MacOSRunId <id> to choose a stable collection prefix, -ResultCollectionDir
to store archives elsewhere, or -SkipResultCollection when the results should
remain only on the Apple host. The local copy-back is downloaded to a
same-directory temporary file, rejected if empty, and published without
overwriting an existing archive for that run ID. On the Apple host, the remote
tarball is also written to a same-directory temporary file, checked with
tar -tzf, and hard-linked into the final copy-back name without replacing an
existing target. The copied archive is validated automatically
unless -SkipResultArchiveValidation is set. The validator also requires the
archive filename to keep the same <run-id> as the result directories, so do
not rename the collected .tar.gz before recording signoff evidence.
Guest result directories must be new or empty before logging starts; if a
previous run left files under <run-id>-<action>-<bridge>, choose a fresh
-MacOSRunId or remove the stale directory before rerunning.
The automatic validation checks structure, both bridge reports, workflow logs,
staged payload evidence, binary architecture evidence, macOS system/device
inventory sections, OS matrix role, Xcode/macOS SDK evidence, asset-basepath evidence, single-player smoke output,
multiplayer renderer-mp-smoke output, and renderer matrix output.
Collection is intentionally limited to the expected
<run-id>-signoff-<bridge> directories so older build/smoke result directories
with the same run ID cannot be mixed into final signoff evidence. To rerun it
manually:
python tools/macos/validate_signoff_archive.py .tmp/macos-vm/results/openq4-macos-results-<run-id>.tar.gz
After the manual hardware checklist has been completed in both bridge reports,
add -RequireCompletedSignoffChecklist to the host workflow, or
--require-completed-checklist to the manual validator command, to fail on any
remaining unchecked item.
If the manual checklist is completed on the Apple host after the original run, copy and validate the existing results without rebuilding:
powershell -NoProfile -ExecutionPolicy Bypass -File tools/macos/Invoke-openQ4MacOSWorkflow.ps1 `
-Action CollectResults `
-MacHost <host> `
-MacOSRunId <run-id> `
-MacOSGraphicsBridge both `
-RequireCompletedSignoffChecklist
After the archive validates, generate and record the evidence index entry from the Windows host:
python tools/macos/record_signoff_evidence.py `
.tmp/macos-vm/results/openq4-macos-results-<run-id>.tar.gz `
--version vX.Y.Z `
--package-artifact openq4-vX.Y.Z-macos-arm64-opengl.dmg `
--package-artifact openq4-vX.Y.Z-macos-arm64-metal.dmg `
--signing-status "signed and notarized DMGs" `
--release-note-limitation "macOS support remains a preview for Apple Silicon/arm64" `
--update-index
Use --artifact-url when the validated .tar.gz evidence lives in an issue,
release candidate, or external artifact store rather than only under .tmp/.
The recorder re-runs tools/macos/validate_signoff_archive.py with
--require-completed-checklist, computes the archive SHA-256, extracts the
bridge reports, verifies that package artifact names are safe openQ4 macOS
arm64 filenames covering every requested graphics bridge, records the openQ4
and openQ4-game commits, updates docs/dev/macos-signoff-evidence.md, and
leaves the release-completion evidence gate open until curated release notes
link to the accepted record.
OpenAL Soft is the signoff and release audio provider. Pass
-MacOSOpenALProvider system and prepare the pinned dependency with
tools/build/prepare_macos_openal_soft.sh; packaged evidence must show the
signed openQ4.app/Contents/Frameworks/libopenal.1.dylib, package-relative
dependency, LGPL notice, and corresponding source archive. The Apple framework
option is compatibility-diagnostic coverage only. The selected provider is
recorded in the signoff report, and the complete provider policy is tracked in
docs/dev/macos-openal-provider-policy.md.
Expected Validation
For macOS debugging, do not stop at static checks. Use this VM workflow for:
renderer_gameplay_benchmark.py --profile smokerenderer_gameplay_benchmark.py --profile smoke --cases mp-q4dm1-listenrenderer_validation_matrix.py --tiers auto,gl41- staged launch from
~/Desktop/openQ4.command macos-runtime-signoff.mdfrom-Action Signoff -MacOSGraphicsBridge bothwhen collecting hardware signoff evidence.tmp/macos-vm/results/openq4-macos-results-<run-id>.tar.gzcopied back by the host workflowtools/macos/validate_signoff_archive.pyrun against the collected archive-RequireCompletedSignoffChecklistonce manual hardware checks are filled in-MacOSOSMatrixRole floor-candidatefor macOS floor evidence and-MacOSOSMatrixRole latest-public-macosfor latest public macOS evidence-Action CollectResults -MacOSRunId <run-id>when re-collecting completed manual evidence- mounted-DMG, independently dragged-app, whole-package loose-tool, terminal, embedded resource/module path, path-resolution log, and Gatekeeper checks from
docs/dev/macos-package-layout-and-release-policy.md - log inspection under the guest
~/openq4-work/results/run directory
The remaining stock Quake 4 duplicate material warnings from retail assets are not macOS VM setup failures. Investigate missing images, shader/link errors, GL errors, PK4 mount errors, app bundle failures, dylib load errors, input/audio device failures, and crashes.
No Persistent Mac Yet
If no Apple VM or hosted Mac is available yet, use the manual GitHub Actions
workflow .github/workflows/macos-debug.yml as the interim macOS debug target.
It builds and stages the macOS OpenGL and/or Metal bridge variants on Apple's
hosted macOS runner, uploads .install, Meson logs, host diagnostics, and
optional assetless renderer-probe logs. Each selected artifact also includes
macos-debug-evidence-scope.txt, which records the requested bridge, the
artifact's actual bridge, exact openQ4 and openQ4-game commits, clean-tree
state, and whether the optional probes were enabled.
It explicitly marks hosted output as build/package evidence rather than a
completed manual Apple-hardware gameplay signoff.
Run it from GitHub Actions as macOS Debug. Use bridge=both for broad
coverage, or pick opengl/metal while chasing a focused issue. Enable
run_runtime_assetless only when startup/runtime logs are needed; hosted macOS
runner display capabilities can vary, so that step is allowed to fail while
still publishing whatever logs were produced. A successful artifact from one
selected bridge does not prove the other bridge; a bridge=both dispatch still
publishes one scoped artifact per bridge.