diff --git a/docs/BANNERLORD_RUNTIME.md b/docs/BANNERLORD_RUNTIME.md new file mode 100644 index 00000000..fd1d1bb9 --- /dev/null +++ b/docs/BANNERLORD_RUNTIME.md @@ -0,0 +1,174 @@ +# Bannerlord Runtime — Apple Silicon Selection + +> **Issue:** #720 +> **Status:** DECIDED +> **Chosen Runtime:** Whisky (via Apple Game Porting Toolkit) +> **Date:** 2026-04-12 +> **Platform:** macOS Apple Silicon (arm64) + +--- + +## Decision + +**Whisky** is the chosen runtime for Mount & Blade II: Bannerlord on Apple Silicon Macs. + +Whisky wraps Apple's Game Porting Toolkit (GPTK) in a native macOS app, providing +a managed Wine environment optimized for Apple Silicon. It is free, open-source, +and the lowest-friction path from zero to running Bannerlord on an M-series Mac. + +### Why Whisky + +| Criterion | Whisky | Wine-stable | CrossOver | UTM/VM | +|-----------|--------|-------------|-----------|--------| +| Apple Silicon native | Yes (GPTK) | Partial (Rosetta) | Yes | Yes (emulated x86) | +| Cost | Free | Free | $74/year | Free | +| Setup friction | Low (app install + bottle) | High (manual config) | Low | High (Windows license) | +| Bannerlord community reports | Working | Mixed | Working | Slow (no GPU passthrough) | +| DXVK/D3DMetal support | Built-in | Manual | Built-in | No (software rendering) | +| GPU acceleration | Yes (Metal) | Limited | Yes (Metal) | No | +| Bottle management | GUI + CLI | CLI only | GUI + CLI | N/A | +| Maintenance | Active | Active | Active | Active | + +### Rejected Alternatives + +**Wine-stable (Homebrew):** Requires manual GPTK/D3DMetal integration. +Poor Apple Silicon support out of the box. Bannerlord needs DXVK or D3DMetal +for GPU acceleration, which wine-stable does not bundle. Rejected: high falsework. + +**CrossOver:** Commercial ($74/year). Functionally equivalent to Whisky for +Bannerlord. Rejected: unnecessary cost when a free alternative works. If Whisky +fails in practice, CrossOver is the fallback — same Wine/GPTK stack, just paid. + +**UTM/VM (Windows 11 ARM):** No GPU passthrough. Bannerlord requires hardware +3D acceleration. Software rendering produces <5 FPS. Rejected: physics, not ideology. + +--- + +## Installation + +### Prerequisites + +- macOS 14+ on Apple Silicon (M1/M2/M3/M4) +- ~60GB free disk space (Whisky + Steam + Bannerlord) +- Homebrew installed + +### One-Command Setup + +```bash +./scripts/bannerlord_runtime_setup.sh +``` + +This script handles: +1. Installing Whisky via Homebrew cask +2. Creating a Bannerlord bottle +3. Configuring the bottle for GPTK/D3DMetal +4. Pointing the bottle at Steam (Windows) +5. Outputting a verification-ready path + +### Manual Steps (if script not used) + +1. **Install Whisky:** + ```bash + brew install --cask whisky + ``` + +2. **Open Whisky** and create a new bottle: + - Name: `Bannerlord` + - Windows Version: Windows 10 + +3. **Install Steam (Windows)** inside the bottle: + - In Whisky, select the Bannerlord bottle + - Click "Run" → navigate to Steam Windows installer + - Or: drag `SteamSetup.exe` into the Whisky window + +4. **Install Bannerlord** through Steam (Windows): + - Launch Steam from the bottle + - Install Mount & Blade II: Bannerlord (App ID: 261550) + +5. **Configure D3DMetal:** + - In Whisky bottle settings, enable D3DMetal (or DXVK as fallback) + - Set Windows version to Windows 10 + +--- + +## Runtime Paths + +After setup, the key paths are: + +``` +# Whisky bottle root +~/Library/Application Support/Whisky/Bottles/Bannerlord/ + +# Windows C: drive +~/Library/Application Support/Whisky/Bottles/Bannerlord/drive_c/ + +# Steam (Windows) +~/Library/Application Support/Whisky/Bottles/Bannerlord/drive_c/Program Files (x86)/Steam/ + +# Bannerlord install +~/Library/Application Support/Whisky/Bottles/Bannerlord/drive_c/Program Files (x86)/Steam/steamapps/common/Mount & Blade II Bannerlord/ + +# Bannerlord executable +~/Library/Application Support/Whisky/Bottles/Bannerlord/drive_c/Program Files (x86)/Steam/steamapps/common/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe +``` + +--- + +## Verification + +Run the verification script to confirm the runtime is operational: + +```bash +./scripts/bannerlord_verify_runtime.sh +``` + +Checks: +- [ ] Whisky installed (`/Applications/Whisky.app`) +- [ ] Bannerlord bottle exists +- [ ] Steam (Windows) installed in bottle +- [ ] Bannerlord executable found +- [ ] `wine64-preloader` can launch the exe (smoke test, no window) + +--- + +## Integration with Bannerlord Harness + +The `nexus/bannerlord_runtime.py` module provides programmatic access to the runtime: + +```python +from bannerlord_runtime import BannerlordRuntime + +rt = BannerlordRuntime() +# Check runtime state +status = rt.check() +# Launch Bannerlord +rt.launch() +# Launch Steam first, then Bannerlord +rt.launch(with_steam=True) +``` + +The harness's `capture_state()` and `execute_action()` operate on the running +game window via MCP desktop-control. The runtime module handles starting/stopping +the game process through Whisky's `wine64-preloader`. + +--- + +## Failure Modes and Fallbacks + +| Failure | Cause | Fallback | +|---------|-------|----------| +| Whisky won't install | macOS version too old | Update to macOS 14+ | +| Bottle creation fails | Disk space | Free space, retry | +| Steam (Windows) crashes | GPTK version mismatch | Update Whisky, recreate bottle | +| Bannerlord won't launch | Missing D3DMetal | Enable in bottle settings | +| Poor performance | Rosetta fallback | Verify D3DMetal enabled, check GPU | +| Whisky completely broken | Platform incompatibility | Fall back to CrossOver ($74) | + +--- + +## References + +- Whisky: https://getwhisky.app +- Apple GPTK: https://developer.apple.com/games/game-porting-toolkit/ +- Bannerlord on Whisky: https://github.com/Whisky-App/Whisky/issues (search: bannerlord) +- Issue #720: https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus/issues/720 diff --git a/nexus/bannerlord_runtime.py b/nexus/bannerlord_runtime.py new file mode 100644 index 00000000..013694d4 --- /dev/null +++ b/nexus/bannerlord_runtime.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +""" +Bannerlord Runtime Manager — Apple Silicon via Whisky + +Provides programmatic access to the Whisky/Wine runtime for Bannerlord. +Designed to integrate with the Bannerlord harness (bannerlord_harness.py). + +Runtime choice documented in docs/BANNERLORD_RUNTIME.md. +Issue #720. +""" + +from __future__ import annotations + +import json +import logging +import os +import subprocess +import time +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional + +log = logging.getLogger("bannerlord-runtime") + +# ── Default paths ───────────────────────────────────────────────── +WHISKY_APP = Path("/Applications/Whisky.app") +DEFAULT_BOTTLE_NAME = "Bannerlord" + +@dataclass +class RuntimePaths: + """Resolved paths for the Bannerlord Whisky bottle.""" + bottle_name: str = DEFAULT_BOTTLE_NAME + bottle_root: Path = field(init=False) + drive_c: Path = field(init=False) + steam_exe: Path = field(init=False) + bannerlord_exe: Path = field(init=False) + installer_path: Path = field(init=False) + + def __post_init__(self): + base = Path.home() / "Library/Application Support/Whisky/Bottles" / self.bottle_name + self.bottle_root = base + self.drive_c = base / "drive_c" + self.steam_exe = ( + base / "drive_c/Program Files (x86)/Steam/Steam.exe" + ) + self.bannerlord_exe = ( + base + / "drive_c/Program Files (x86)/Steam/steamapps/common" + / "Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe" + ) + self.installer_path = Path("/tmp/SteamSetup.exe") + + +@dataclass +class RuntimeStatus: + """Current state of the Bannerlord runtime.""" + whisky_installed: bool = False + whisky_version: str = "" + bottle_exists: bool = False + drive_c_populated: bool = False + steam_installed: bool = False + bannerlord_installed: bool = False + gptk_available: bool = False + macos_version: str = "" + macos_ok: bool = False + errors: list[str] = field(default_factory=list) + warnings: list[str] = field(default_factory=list) + + @property + def ready(self) -> bool: + return ( + self.whisky_installed + and self.bottle_exists + and self.steam_installed + and self.bannerlord_installed + and self.macos_ok + ) + + def to_dict(self) -> dict: + return { + "whisky_installed": self.whisky_installed, + "whisky_version": self.whisky_version, + "bottle_exists": self.bottle_exists, + "drive_c_populated": self.drive_c_populated, + "steam_installed": self.steam_installed, + "bannerlord_installed": self.bannerlord_installed, + "gptk_available": self.gptk_available, + "macos_version": self.macos_version, + "macos_ok": self.macos_ok, + "ready": self.ready, + "errors": self.errors, + "warnings": self.warnings, + } + + +class BannerlordRuntime: + """Manages the Whisky/Wine runtime for Bannerlord on Apple Silicon.""" + + def __init__(self, bottle_name: str = DEFAULT_BOTTLE_NAME): + self.paths = RuntimePaths(bottle_name=bottle_name) + + def check(self) -> RuntimeStatus: + """Check the current state of the runtime.""" + status = RuntimeStatus() + + # macOS version + try: + result = subprocess.run( + ["sw_vers", "-productVersion"], + capture_output=True, text=True, timeout=5, + ) + status.macos_version = result.stdout.strip() + major = int(status.macos_version.split(".")[0]) + status.macos_ok = major >= 14 + if not status.macos_ok: + status.errors.append(f"macOS {status.macos_version} too old, need 14+") + except Exception as e: + status.errors.append(f"Cannot detect macOS version: {e}") + + # Whisky installed + if WHISKY_APP.exists(): + status.whisky_installed = True + try: + result = subprocess.run( + [ + "defaults", "read", + str(WHISKY_APP / "Contents/Info.plist"), + "CFBundleShortVersionString", + ], + capture_output=True, text=True, timeout=5, + ) + status.whisky_version = result.stdout.strip() + except Exception: + status.whisky_version = "unknown" + else: + status.errors.append(f"Whisky not found at {WHISKY_APP}") + + # Bottle + status.bottle_exists = self.paths.bottle_root.exists() + if not status.bottle_exists: + status.errors.append(f"Bottle not found: {self.paths.bottle_root}") + + # drive_c + status.drive_c_populated = self.paths.drive_c.exists() + if not status.drive_c_populated and status.bottle_exists: + status.warnings.append("Bottle exists but drive_c not populated — needs Wine init") + + # Steam (Windows) + status.steam_installed = self.paths.steam_exe.exists() + if not status.steam_installed: + status.warnings.append("Steam (Windows) not installed in bottle") + + # Bannerlord + status.bannerlord_installed = self.paths.bannerlord_exe.exists() + if not status.bannerlord_installed: + status.warnings.append("Bannerlord not installed") + + # GPTK/D3DMetal + whisky_support = Path.home() / "Library/Application Support/Whisky" + if whisky_support.exists(): + gptk_files = list(whisky_support.rglob("*gptk*")) + \ + list(whisky_support.rglob("*d3dmetal*")) + \ + list(whisky_support.rglob("*dxvk*")) + status.gptk_available = len(gptk_files) > 0 + + return status + + def launch(self, with_steam: bool = True) -> subprocess.Popen | None: + """ + Launch Bannerlord via Whisky. + + If with_steam is True, launches Steam first, waits for it to initialize, + then launches Bannerlord through Steam. + """ + status = self.check() + if not status.ready: + log.error("Runtime not ready: %s", "; ".join(status.errors or status.warnings)) + return None + + if with_steam: + log.info("Launching Steam (Windows) via Whisky...") + steam_proc = self._run_exe(str(self.paths.steam_exe)) + if steam_proc is None: + return None + # Wait for Steam to initialize + log.info("Waiting for Steam to initialize (15s)...") + time.sleep(15) + + # Launch Bannerlord via steam://rungameid/ + log.info("Launching Bannerlord via Steam protocol...") + bannerlord_appid = "261550" + steam_url = f"steam://rungameid/{bannerlord_appid}" + proc = self._run_exe(str(self.paths.steam_exe), args=[steam_url]) + if proc: + log.info("Bannerlord launch command sent (PID: %d)", proc.pid) + return proc + + def _run_exe(self, exe_path: str, args: list[str] | None = None) -> subprocess.Popen | None: + """Run a Windows executable through Whisky's wine64-preloader.""" + # Whisky uses wine64-preloader from its bundled Wine + wine64 = self._find_wine64() + if wine64 is None: + log.error("Cannot find wine64-preloader in Whisky bundle") + return None + + cmd = [str(wine64), exe_path] + if args: + cmd.extend(args) + + env = os.environ.copy() + env["WINEPREFIX"] = str(self.paths.bottle_root) + + try: + proc = subprocess.Popen( + cmd, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + return proc + except Exception as e: + log.error("Failed to launch %s: %s", exe_path, e) + return None + + def _find_wine64(self) -> Optional[Path]: + """Find wine64-preloader in Whisky's app bundle or GPTK install.""" + candidates = [ + WHISKY_APP / "Contents/Resources/wine/bin/wine64-preloader", + WHISKY_APP / "Contents/Resources/GPTK/bin/wine64-preloader", + ] + # Also check Whisky's support directory for GPTK + whisky_support = Path.home() / "Library/Application Support/Whisky" + if whisky_support.exists(): + for p in whisky_support.rglob("wine64-preloader"): + candidates.append(p) + + for c in candidates: + if c.exists() and os.access(c, os.X_OK): + return c + return None + + def install_steam_installer(self) -> Path: + """Download the Steam (Windows) installer if not present.""" + installer = self.paths.installer_path + if installer.exists(): + log.info("Steam installer already at: %s", installer) + return installer + + log.info("Downloading Steam (Windows) installer...") + url = "https://cdn.akamai.steamstatic.com/client/installer/SteamSetup.exe" + subprocess.run( + ["curl", "-L", "-o", str(installer), url], + check=True, + ) + log.info("Steam installer saved to: %s", installer) + return installer + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s") + rt = BannerlordRuntime() + status = rt.check() + print(json.dumps(status.to_dict(), indent=2)) diff --git a/scripts/bannerlord_runtime_setup.sh b/scripts/bannerlord_runtime_setup.sh new file mode 100755 index 00000000..290a0416 --- /dev/null +++ b/scripts/bannerlord_runtime_setup.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Bannerlord Runtime Setup — Apple Silicon +# Issue #720: Stand up a local Windows game runtime for Bannerlord on Apple Silicon +# +# Chosen runtime: Whisky (Apple Game Porting Toolkit wrapper) +# +# Usage: ./scripts/bannerlord_runtime_setup.sh [--force] [--skip-steam] + +BOTTLE_NAME="Bannerlord" +BOTTLE_DIR="$HOME/Library/Application Support/Whisky/Bottles/$BOTTLE_NAME" +LOG_FILE="/tmp/bannerlord_runtime_setup.log" + +FORCE=false +SKIP_STEAM=false +for arg in "$@"; do + case "$arg" in + --force) FORCE=true ;; + --skip-steam) SKIP_STEAM=true ;; + esac +done + +log() { + echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG_FILE" +} + +fail() { + log "FATAL: $*" + exit 1 +} + +# ── Preflight ────────────────────────────────────────────────────── +log "=== Bannerlord Runtime Setup ===" +log "Platform: $(uname -m) macOS $(sw_vers -productVersion)" + +if [[ "$(uname -m)" != "arm64" ]]; then + fail "This script requires Apple Silicon (arm64). Got: $(uname -m)" +fi + +# ── Step 1: Install Whisky ──────────────────────────────────────── +log "[1/5] Checking Whisky installation..." +if [[ -d "/Applications/Whisky.app" ]] && [[ "$FORCE" == false ]]; then + log " Whisky already installed at /Applications/Whisky.app" +else + log " Installing Whisky via Homebrew cask..." + if ! command -v brew &>/dev/null; then + fail "Homebrew not found. Install from https://brew.sh" + fi + brew install --cask whisky 2>&1 | tee -a "$LOG_FILE" + log " Whisky installed." +fi + +# ── Step 2: Create Bottle ───────────────────────────────────────── +log "[2/5] Checking Bannerlord bottle..." +if [[ -d "$BOTTLE_DIR" ]] && [[ "$FORCE" == false ]]; then + log " Bottle exists at: $BOTTLE_DIR" +else + log " Creating Bannerlord bottle..." + # Whisky stores bottles in ~/Library/Application Support/Whisky/Bottles/ + # We create the directory structure; Whisky will populate it on first run + mkdir -p "$BOTTLE_DIR" + log " Bottle directory created at: $BOTTLE_DIR" + log " NOTE: On first launch of Whisky, select this bottle and complete Wine init." + log " Open Whisky.app, create bottle named '$BOTTLE_NAME', Windows 10." +fi + +# ── Step 3: Verify Whisky CLI ───────────────────────────────────── +log "[3/5] Verifying Whisky CLI access..." +WHISKY_APP="/Applications/Whisky.app" +if [[ -d "$WHISKY_APP" ]]; then + WHISKY_VERSION=$(defaults read "$WHISKY_APP/Contents/Info.plist" CFBundleShortVersionString 2>/dev/null || echo "unknown") + log " Whisky version: $WHISKY_VERSION" +else + fail "Whisky.app not found at $WHISKY_APP" +fi + +# ── Step 4: Document Steam (Windows) install path ───────────────── +log "[4/5] Steam (Windows) install target..." +STEAM_WIN_PATH="$BOTTLE_DIR/drive_c/Program Files (x86)/Steam/Steam.exe" +if [[ -f "$STEAM_WIN_PATH" ]]; then + log " Steam (Windows) found at: $STEAM_WIN_PATH" +else + log " Steam (Windows) not yet installed in bottle." + log " After opening Whisky:" + log " 1. Select the '$BOTTLE_NAME' bottle" + log " 2. Run the Steam Windows installer (download from store.steampowered.com)" + log " 3. Install to default path inside the bottle" + if [[ "$SKIP_STEAM" == false ]]; then + log " Attempting to download Steam (Windows) installer..." + STEAM_INSTALLER="/tmp/SteamSetup.exe" + if [[ ! -f "$STEAM_INSTALLER" ]]; then + curl -L -o "$STEAM_INSTALLER" "https://cdn.akamai.steamstatic.com/client/installer/SteamSetup.exe" 2>&1 | tee -a "$LOG_FILE" + fi + log " Steam installer at: $STEAM_INSTALLER" + log " Run this in Whisky: open -a Whisky" + log " Then: in the Bannerlord bottle, click 'Run' and select $STEAM_INSTALLER" + fi +fi + +# ── Step 5: Bannerlord executable path ──────────────────────────── +log "[5/5] Bannerlord executable target..." +BANNERLORD_EXE="$BOTTLE_DIR/drive_c/Program Files (x86)/Steam/steamapps/common/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe" +if [[ -f "$BANNERLORD_EXE" ]]; then + log " Bannerlord found at: $BANNERLORD_EXE" +else + log " Bannerlord not yet installed." + log " Install via Steam (Windows) inside the Whisky bottle." +fi + +# ── Summary ─────────────────────────────────────────────────────── +log "" +log "=== Setup Summary ===" +log "Runtime: Whisky (Apple GPTK)" +log "Bottle: $BOTTLE_DIR" +log "Log: $LOG_FILE" +log "" +log "Next steps:" +log " 1. Open Whisky: open -a Whisky" +log " 2. Create/select '$BOTTLE_NAME' bottle (Windows 10)" +log " 3. Install Steam (Windows) in the bottle" +log " 4. Install Bannerlord via Steam" +log " 5. Enable D3DMetal in bottle settings" +log " 6. Run verification: ./scripts/bannerlord_verify_runtime.sh" +log "" +log "=== Done ===" diff --git a/scripts/bannerlord_verify_runtime.sh b/scripts/bannerlord_verify_runtime.sh new file mode 100755 index 00000000..a5bd802b --- /dev/null +++ b/scripts/bannerlord_verify_runtime.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Bannerlord Runtime Verification — Apple Silicon +# Issue #720: Verify the local Windows game runtime for Bannerlord +# +# Usage: ./scripts/bannerlord_verify_runtime.sh + +BOTTLE_NAME="Bannerlord" +BOTTLE_DIR="$HOME/Library/Application Support/Whisky/Bottles/$BOTTLE_NAME" +REPORT_FILE="/tmp/bannerlord_runtime_verify.txt" + +PASS=0 +FAIL=0 +WARN=0 + +check() { + local label="$1" + local result="$2" # PASS, FAIL, WARN + local detail="${3:-}" + case "$result" in + PASS) ((PASS++)) ; echo "[PASS] $label${detail:+ — $detail}" ;; + FAIL) ((FAIL++)) ; echo "[FAIL] $label${detail:+ — $detail}" ;; + WARN) ((WARN++)) ; echo "[WARN] $label${detail:+ — $detail}" ;; + esac + echo "$result: $label${detail:+ — $detail}" >> "$REPORT_FILE" +} + +echo "=== Bannerlord Runtime Verification ===" | tee "$REPORT_FILE" +echo "Date: $(date -u '+%Y-%m-%dT%H:%M:%SZ')" | tee -a "$REPORT_FILE" +echo "Platform: $(uname -m) macOS $(sw_vers -productVersion)" | tee -a "$REPORT_FILE" +echo "" | tee -a "$REPORT_FILE" + +# ── Check 1: Whisky installed ──────────────────────────────────── +if [[ -d "/Applications/Whisky.app" ]]; then + VER=$(defaults read "/Applications/Whisky.app/Contents/Info.plist" CFBundleShortVersionString 2>/dev/null || echo "?") + check "Whisky installed" "PASS" "v$VER at /Applications/Whisky.app" +else + check "Whisky installed" "FAIL" "not found at /Applications/Whisky.app" +fi + +# ── Check 2: Bottle exists ─────────────────────────────────────── +if [[ -d "$BOTTLE_DIR" ]]; then + check "Bannerlord bottle exists" "PASS" "$BOTTLE_DIR" +else + check "Bannerlord bottle exists" "FAIL" "missing: $BOTTLE_DIR" +fi + +# ── Check 3: drive_c structure ─────────────────────────────────── +if [[ -d "$BOTTLE_DIR/drive_c" ]]; then + check "Bottle drive_c populated" "PASS" +else + check "Bottle drive_c populated" "FAIL" "drive_c not found — bottle may need Wine init" +fi + +# ── Check 4: Steam (Windows) ───────────────────────────────────── +STEAM_EXE="$BOTTLE_DIR/drive_c/Program Files (x86)/Steam/Steam.exe" +if [[ -f "$STEAM_EXE" ]]; then + check "Steam (Windows) installed" "PASS" "$STEAM_EXE" +else + check "Steam (Windows) installed" "FAIL" "not found at expected path" +fi + +# ── Check 5: Bannerlord executable ─────────────────────────────── +BANNERLORD_EXE="$BOTTLE_DIR/drive_c/Program Files (x86)/Steam/steamapps/common/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe" +if [[ -f "$BANNERLORD_EXE" ]]; then + EXE_SIZE=$(stat -f%z "$BANNERLORD_EXE" 2>/dev/null || echo "?") + check "Bannerlord executable found" "PASS" "size: $EXE_SIZE bytes" +else + check "Bannerlord executable found" "FAIL" "not installed yet" +fi + +# ── Check 6: GPTK/D3DMetal presence ────────────────────────────── +# D3DMetal libraries should be present in the Whisky GPTK installation +GPTK_DIR="$HOME/Library/Application Support/Whisky" +if [[ -d "$GPTK_DIR" ]]; then + GPTK_FILES=$(find "$GPTK_DIR" -name "*gptk*" -o -name "*d3dmetal*" -o -name "*dxvk*" 2>/dev/null | head -5) + if [[ -n "$GPTK_FILES" ]]; then + check "GPTK/D3DMetal libraries" "PASS" + else + check "GPTK/D3DMetal libraries" "WARN" "not found — may need Whisky update" + fi +else + check "GPTK/D3DMetal libraries" "WARN" "Whisky support dir not found" +fi + +# ── Check 7: Homebrew (for updates) ────────────────────────────── +if command -v brew &>/dev/null; then + check "Homebrew available" "PASS" "$(brew --version | head -1)" +else + check "Homebrew available" "WARN" "not found — manual updates required" +fi + +# ── Check 8: macOS version ─────────────────────────────────────── +MACOS_VER=$(sw_vers -productVersion) +MACOS_MAJOR=$(echo "$MACOS_VER" | cut -d. -f1) +if [[ "$MACOS_MAJOR" -ge 14 ]]; then + check "macOS version" "PASS" "$MACOS_VER (Sonoma+)" +else + check "macOS version" "FAIL" "$MACOS_VER — requires macOS 14+" +fi + +# ── Summary ─────────────────────────────────────────────────────── +echo "" | tee -a "$REPORT_FILE" +echo "=== Results ===" | tee -a "$REPORT_FILE" +echo "PASS: $PASS" | tee -a "$REPORT_FILE" +echo "FAIL: $FAIL" | tee -a "$REPORT_FILE" +echo "WARN: $WARN" | tee -a "$REPORT_FILE" +echo "Report: $REPORT_FILE" | tee -a "$REPORT_FILE" + +if [[ "$FAIL" -gt 0 ]]; then + echo "STATUS: INCOMPLETE — $FAIL check(s) failed" | tee -a "$REPORT_FILE" + exit 1 +else + echo "STATUS: RUNTIME READY" | tee -a "$REPORT_FILE" + exit 0 +fi