-
Notifications
You must be signed in to change notification settings - Fork 0
Build and Test
This is the workshop manual: the shortest reliable path from a clean checkout to a real SwanSong app, plus the gates that keep a convenient local build from being mistaken for release evidence.
Product documentation lives in Playing and Library, Translation Lab, SwanSong Studio, and Analogue Pocket SD Setup.
- macOS 14 or later;
- current Apple Command Line Tools or Xcode;
- Swift 6 toolchain support;
- CMake 3.28 or later; and
- Git.
The full Swift/XCTest lane requires the full Xcode developer directory. Command Line Tools alone may not provide XCTest.
From the repository root:
./Scripts/build-engine.sh
export SWAN_ARES_ENGINE_DIR="$PWD/.engine/build"
./Scripts/build-app.sh
open ".build/app/SwanSong.app"For direct SwiftPM execution instead of a Finder-style bundle:
./Scripts/build-engine.sh
export SWAN_ARES_ENGINE_DIR="$PWD/.engine/build"
swift run SwanSongThe local app is ad-hoc signed and is not an official distributable release.
The bundle embeds the WonderSwan-family ares dylib and declares .ws, .wsc,
.pc2, and .pcv2 document types. ZIPs are accepted by the open panel,
drag-and-drop, and folder import without claiming every ZIP in Launch Services.
A plain swift build uses the inspection-only stub backend for UI work. It
must not be presented as gameplay, compatibility, or release evidence.
SWAN_UNIVERSAL=1 ./Scripts/build-app.sh
./Scripts/verify-app-architectures.sh ".build/app/SwanSong.app"The universal engine uses .engine/build-app-universal. Apple-silicon and
Intel Swift slices use separate scratch directories so a host-native cache
cannot leak into the other architecture.
Official signing and notarization are documented in Signing and Notarization.
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer \
./Scripts/swift-package.sh test --package-path .
./Scripts/check-live-engine.sh
./Scripts/check-compatibility-matrix.sh
./Scripts/check-av-soak.sh
./Scripts/check-app-runtime.sh
./Scripts/check-app-bundle.sh
./Scripts/check-release-metadata.sh
./Scripts/prepare-wiki-sync.sh --check
python3 ./Scripts/check-sparkle-dependency-lock.py \
--repository . \
--upstream-package .build/checkouts/Sparkle/Package.swift
./Scripts/check-sparkle-configuration.sh
./Scripts/check-sparkle-framework.sh
./Scripts/selftest-sparkle-dependency-lock.sh
./Scripts/selftest-sparkle-appcast.sh
./Scripts/check-ui-snapshots.sh
./Scripts/check-mcp-server.sh
./Scripts/check-playtest-mcp-server.sh
./Scripts/check-playtest-cli.sh
./Scripts/check-translation-automation-cli.sh
./Scripts/check-translation-lab.sh
./Scripts/check-pcv2-translation-lab.sh
./Scripts/check-homebrew-production-readiness.shFixture results prove bounded execution invariants. They are not commercial-
game compatibility results or original-hardware accuracy evidence.
check-live-engine.sh also pins two clean-room display-provenance fixtures
(introduced with ABI 6 and retained by ABI 9): horizontal
planar and vertical packed output with exact Screen 1, Screen 2, sprite,
palette, raster-width, rotation, and non-unknown CPU-writer assertions.
The same lane runs a clean-room input-frame fixture that samples once per
VBlank and requires repeated A release/press cycles interleaved with X1 and Y3
directional changes, without a stale cached button frame between them.
check-input-frame-bridge.sh exposes that exact regression independently for
release preflight, while check-live-engine.sh retains the broader persistence,
display, audio, and replay suite.
ABI 8 extends those fixtures with raster-only selection, component-complete
consumer discovery, and executed caller/mapper context for the transformed
ROM-resident source table. ABI 9 adds private sprite OAM ownership,
spriteAttribute selection, conservative-origin identity, support for more
than eight disjoint exact ranges, and a 64 MiB private-evidence bound. The same
live lane must continue to pass the monochrome fixture whose palette/control
byte reaches PPU I/O through a real V30 OUT; a failed control makes commercial
monochrome provenance inconclusive.
The
live TranslationDisplaySourceProbeTests lane must prove an exact transformed
selected range, an outside consumer, private sprite/OAM evidence, source-free
public output, legacy-browser compatibility, and intact private artifact
validation. Inspection-only stub runs skip that one live test;
the separate live-engine invocation is mandatory for release evidence.
Pull requests run the complete Swift/XCTest and UI snapshot suite once on the macOS 14 Apple-silicon runner. The macOS 15 Intel runner compiles the native engine/library compatibility target and verifies its x86_64 Mach-O identity; the hosted image does not reliably permit ad-hoc standalone Swift executables. The release-preflight job runs in parallel instead of waiting for those lanes. On pull requests it uses bounded 180-frame compatibility routes, a 20-second A/V sample, and a complete native Apple-silicon app build. The separate Intel lane still proves x86_64 compilation. MCP, runtime engine, and fail-closed production checks run once rather than being duplicated by both matrix entries. Within release preflight, the inspected app builds the pinned ares engine first; compatibility, Translation Lab CLI, and A/V tools reuse that exact build instead of compiling a second engine tree.
Pushes to main and manual workflow runs retain the complete XCTest suite on
the Intel runner, the 360-frame compatibility matrix, the 60-second CI soak,
and the complete universal app inspection including every Intel slice. This
keeps the release-grade gates intact while removing serial wait time and a
second cold SwiftUI compile from every pull-request iteration. UI snapshots
remain part of the complete XCTest suite and are not repeated in the separate
release-preflight job.
The shared SwiftPM wrapper disables login-keychain credential lookup in CI and
uses only Package.resolved. Set SWAN_SWIFTPM_DISABLE_KEYCHAIN=1 for the same
non-interactive behavior in a local automation or clean-scratch smoke run. The
pull-request Intel compile also limits SwiftPM parallelism so the hosted
runner's tighter memory ceiling cannot turn a cold build into an exit-137
failure.
The nested MCP package gives its local Desktop dependency an explicit identity,
so these checks also work from renamed clones and isolated Git worktrees instead
of depending on the checkout folder being named exactly SwanSong-Desktop.
SwanSong Studio's Swift tests cover exact swan arguments—including Doctor,
Optimizer, Fuzzer, Save/RTC Lab, Scenario Recorder, Dev, Profile, Evidence
Diff, and Release—plus checkout and bundled
runtime resolution, process environment/result capture, stable Play Contract
and resource-report decoding, structured JSON/JSONL schema rejection,
evidence/WAV and editable-plan intake, package/schema/toolchain identity,
streamed output, cancellation, and command overlap guards.
Current source resolves the content-verified SDK embedded in the app by default. Use Studio's explicit external SDK override when developing the SDK itself. SDK contributors can run a real smoke project directly from that checkout with:
PYTHONPATH=/path/to/swansong-sdk/python \
SWANSONG_SDK_DIR=/path/to/swansong-sdk \
python3 -m swansong_sdk.cli new smoke-game \
--template menu-puzzle --directory /tmp/smoke-game
PYTHONPATH=/path/to/swansong-sdk/python \
SWANSONG_SDK_DIR=/path/to/swansong-sdk \
python3 -m swansong_sdk.cli assets --project /tmp/smoke-game/swan.tomlContinue with test, build, play, and report --json when the pinned
Wonderful toolchain and SwanSong play executor are available. Desktop must not
replace any failed SDK command with a second parser, converter, builder, or
emulator path.
The trusted project config starts the local STDIO server through:
./Scripts/run-swansong-mcp.shRun the protocol-surface check without enabling live app control:
./Scripts/check-mcp-server.sh
./Scripts/check-playtest-mcp-server.shRun the live-ares playtest, route creation, and paired-evidence checks:
./Scripts/check-playtest-cli.sh
./Scripts/check-translation-automation-cli.shThe latter test proves both write guards, route-v3, empty persistence, fixed RTC, native checkpoint capture, Original/Patched endpoint parity, two Capture Intake runs, and manifest digest revalidation. See Local MCP and Automation for tool schemas and the direct CLI.
For an authorized local game, the headless probe runs the same ares engine and Open IPL path without opening a macOS window:
swift run SwanSongProbe \
--rom "/path/to/game.wsc" \
--frames 600 \
--report probe.json \
--capture probe.ppm \
--require-video-activityThe report records first non-uniform video, distinct native-raster frames, longest flat-color run, final-frame hash, audio activity, state size, and first-batch replay behavior. Its outputs contain rendered pixels and measurements, never ROM, state, persistence, or memory bytes.
Build a matched debug automation app in an isolated output directory, then provide an authorized private input explicitly:
CONFIGURATION=debug \
SWAN_APP_OUTPUT_DIR="$PWD/.build/owned-smoke-app" \
./Scripts/build-app.sh
./Scripts/check-owned-rom-smoke.sh \
--app "$PWD/.build/owned-smoke-app/SwanSong.app" \
--rom "$OWNED_GAME_ZIP"The checked executable and debug runner must share a Mach-O build UUID. The lane uses a unique private home/data directory to test import, Open IPL launch, native frame activity, saves, and states. It removes private artifacts, proves the app bundle is byte-identical before/after, and never prints private paths, names, hashes, frames, or diagnostics.
For a privacy-safe aggregate over an authorized directory:
./Scripts/check-owned-rom-open-ipl.sh \
--rom-dir "/path/to/owned-rom-directory" \
--report .build/compatibility/owned-open-ipl-summary.jsonThis lane accepts direct games and one-game ZIPs, rejects firmware-shaped inputs, binds Open IPL and deterministic RTC, and writes only source-free case counts.
./Scripts/check-compatibility-matrix.shThe matrix builds the Probe in an isolated live-ares scratch directory and
runs every checked-in .ws/.wsc fixture plus a clean-room generated .pc2
fixture. The JSON report records video activity, nonzero audio, state capture,
and first replay behavior. It labels static output and settle-required replay
instead of inflating them into compatibility claims.
The release-default source-free soak runs the checked-in open fixture at strict wall-clock speed for 30 minutes:
./Scripts/check-av-soak.shIts sorted-key JSON tracks sequential/invalid frames, delivery stalls, 48 kHz stereo stability, virtual-queue depth, underruns, dropped batches, transport drift, pacing rate, and bounded host-discontinuity recovery without including source bytes, paths, frames, or timestamps.
Production pacing targets five audio batches (about 66 ms nominal) under a 180 ms hard cap. A transport epoch is recovered only after a primed queue drains beyond the full four-batch horizon: the obsolete schedule is cleared, five batches are re-primed, and the first 5 ms fades in. Ordinary sub-horizon starvation remains an underrun and fails the gate. Reports count recoveries separately and allow at most one per requested minute.
Short local and scheduler-neutral hosted-CI lanes are explicit:
SWAN_AV_SOAK_SECONDS=5 ./Scripts/check-av-soak.sh
SWAN_AV_SOAK_SECONDS=5 SWAN_AV_SOAK_CLOCK_MODE=media-time \
./Scripts/check-av-soak.sh .build/av-soak/ci-integrity.jsonFocused injection proves recovery and its disabled control:
SWAN_AV_SOAK_SECONDS=3 SWAN_AV_SOAK_INJECT_HOST_GAP_MS=120 \
./Scripts/check-av-soak.sh .build/av-soak/recovery.json
SWAN_AV_SOAK_SECONDS=3 SWAN_AV_SOAK_INJECT_HOST_GAP_MS=120 \
SWAN_AV_SOAK_DISABLE_DISCONTINUITY_RECOVERY=1 \
SWAN_AV_SOAK_EXPECT_STATUS=fail \
./Scripts/check-av-soak.sh .build/av-soak/recovery-disabled.jsonThe virtual sink is a queue model, not Core Audio hardware. Physical device latency and owned-game audio remain separate release evidence.
./Scripts/check-app-runtime.sh
./Scripts/check-app-bundle.shThe runtime smoke launches the actual SwiftUI app with isolated data and open fixtures. It exercises Open IPL launch, library import, atomic autosave, versioned visual states, byte-identical preview restore, memory-only rewind, and generated Pocket Challenge V2 flash persistence.
The rewind lane captures frame 90, advances to frame 450, restores the nearest
five-second checkpoint, replays frame 90 exactly, and proves no .state file
was created.
The bundle smoke verifies self-contained dylib resolution, ad-hoc signing, Finder-style document opening, and absence of game/firmware payloads.
./Scripts/check-ui-snapshots.shThe gate renders real AppKit/SwiftUI surfaces offscreen across compact/wide, Light/Dark, horizontal/vertical, player, library, controller, Translation Lab, and Analogue Pocket states. It checks blank regions, framebuffer corners, interaction targets, accessibility labels, scrolling, and reviewed perceptual baselines.
After visually reviewing every generated PNG under .build/ui-regression/,
refresh a deliberately changed baseline with:
./Scripts/check-ui-snapshots.sh --update-baselinesNormal checks are read-only and never approve or rewrite a baseline.
./Scripts/check-translation-lab.sh
./Scripts/check-pcv2-translation-lab.shThe general smoke builds a synthetic private project, records immutable route-v3 tests, captures digest-bound Original/Patched evidence, exercises guarded packing and batch verification, rejects unsafe readiness and legacy routes, and proves the normal library and save store remain untouched.
The Pocket Challenge V2 lane proves project/startup identity, all nine keypad inputs, 16 KiB internal RAM, route replay, and First Visual Change hardware routing.
Signed bundles include a separately gated command-line runner:
/Applications/SwanSong.app/Contents/Helpers/SwanSongRouteRunner \
--enable-debug-tools \
--rom "/path/to/game.wsc" \
--route "/path/to/route.json" \
--output "/path/to/route-report.json" \
--capture "/path/to/final-frame.png"The runner requires the route-bound ROM digest, hardware, Open IPL context, RTC seed, and bundled engine build to agree. It exits nonzero when the final native-raster checkpoint differs.
./Scripts/check-player-input.sh "/path/to/game.wsc"The gate posts a physical keyboard event, requires active gameplay focus and the expected effective WonderSwan input, and proves canonical game-raster fingerprints change. Exit 77 means the host lacks WindowServer or Accessibility permission; it is not a pass. Grant the invoking Terminal or Codex app access under System Settings → Privacy & Security → Accessibility and rerun.
SWAN_ARES_ENGINE_DIR="$PWD/.engine/build" swift run SwanSongDifferential \
--rom testroms/ws-test-suite/80186_quirks/80186_quirks.ws \
--rtl /path/to/swan-song-rtl-frames \
--frames 360 \
--out .build/differential/80186_quirks.jsonThe differential compares the live ares framebuffer with raw 224×144 RTL frames. Mono reports preserve the raw result and add a separately labeled four-luminance structural comparison. Every report states that emulator/RTL agreement is not original-hardware evidence.
Updater acceptance requires the signed production feed, public key, disabled system profiling, off-by-default automation, signed enclosure, immutable URL, and stable/beta behavior in App Updates. A source configuration test cannot replace installation/relaunch from the previous supported app.
Pocket release acceptance includes the adversarial fixture suite plus real cards, readers, filesystems, eject behavior, and hardware; see Analogue Pocket SD Setup. Complete artifact, owned-game, physical-controller, signing, and publication requirements are in Release Gates.
Keep all private ROMs, saves, captures, and Translation Lab evidence outside Git. Never attach them to a public issue or CI artifact.
Start here
Create and translate
Homebrew and hardware
Build and release
- Architecture and Source Ownership
- Build and Test
- Signing and Notarization
- Release Gates
- 0.9 Release Testing
- 0.8 Release Testing
- 0.7 Release Testing
- 0.6 Release Testing
- 0.5 Release Testing
- 0.4 Beta Testing
Help and policies