From 5b40be127f6f06a56b183339a42b965590d3e4e4 Mon Sep 17 00:00:00 2001
From: licarto <334195767+licarto@users.noreply.github.com>
Date: Sun, 27 Sep 2026 13:14:06 +0200
Subject: [PATCH 1/3] Start native macOS setup support
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
.../NodToolProvider.cs | 13 +-
.../WiiCompiled.Setup.Linux/BuildRunner.cs | 28 ++++-
Launcher/WiiCompiled.Setup.Linux/Models.cs | 1 +
Launcher/WiiCompiled.Setup.Linux/Program.cs | 115 ++++++++++++++++--
4 files changed, 135 insertions(+), 22 deletions(-)
diff --git a/Launcher/WiiCompiled.Setup.Common/NodToolProvider.cs b/Launcher/WiiCompiled.Setup.Common/NodToolProvider.cs
index f58769565..b0ae14d18 100644
--- a/Launcher/WiiCompiled.Setup.Common/NodToolProvider.cs
+++ b/Launcher/WiiCompiled.Setup.Common/NodToolProvider.cs
@@ -8,9 +8,8 @@ namespace WiiCompiled.Setup.Common;
/// caller can supply one directly; otherwise this downloads the matching prebuilt release binary
/// from encounter/nod and caches it at Launcher/artifacts/nodtool[.exe].
///
-/// Shared by: WiiCompiled.Setup.Linux/DiscTool.cs (falls back to this at end-user install time on
-/// a plain git checkout), and WiiCompiled.Setup.Common.Cli (invoked once at packaging time by both
-/// build-appimage.sh and Build-Installer.ps1 to acquire the copy each bundles).
+/// Shared by the Linux/macOS setup hosts (which fall back to this at end-user install time on a
+/// plain git checkout), and WiiCompiled.Setup.Common.Cli (invoked at packaging time).
///
public static class NodToolProvider
{
@@ -56,6 +55,14 @@ private static string AssetName()
var other => throw new PlatformNotSupportedException($"No prebuilt nodtool release for Windows {other}"),
};
}
+ if (OperatingSystem.IsMacOS())
+ {
+ return RuntimeInformation.OSArchitecture switch
+ {
+ Architecture.Arm64 => "nodtool-macos-arm64",
+ var other => throw new PlatformNotSupportedException($"No prebuilt nodtool release for macOS {other}"),
+ };
+ }
return RuntimeInformation.OSArchitecture switch
{
Architecture.X64 => "nodtool-linux-x86_64",
diff --git a/Launcher/WiiCompiled.Setup.Linux/BuildRunner.cs b/Launcher/WiiCompiled.Setup.Linux/BuildRunner.cs
index 284310a12..4f1e514ae 100644
--- a/Launcher/WiiCompiled.Setup.Linux/BuildRunner.cs
+++ b/Launcher/WiiCompiled.Setup.Linux/BuildRunner.cs
@@ -3,7 +3,7 @@
namespace WiiCompiled.Setup.Linux;
///
-/// Invokes Launcher/local-build.sh and turns its stdout into progress reports. Replaces
+/// Invokes the native platform build script and turns its stdout into progress reports. Replaces
/// LocalBuildService.cs's hardcoded Windows PowerShell 5.1 invocation - there is no PowerShell
/// dependency here at all, just bash.
///
@@ -17,8 +17,13 @@ public static async Task RunAsync(
IInstallReporter reporter,
CancellationToken cancellationToken)
{
- var script = Path.Combine(workspace, "Launcher", "local-build.sh");
- if (!File.Exists(script)) throw new FileNotFoundException("local-build.sh is missing", script);
+ var macOS = OperatingSystem.IsMacOS();
+ if (macOS && sysroot is not null)
+ throw new ArgumentException("--sysroot is not supported by the macOS build.");
+
+ var script = Path.Combine(workspace, "Launcher",
+ macOS ? "local-build-macos.command" : "local-build.sh");
+ if (!File.Exists(script)) throw new FileNotFoundException($"{Path.GetFileName(script)} is missing", script);
var startInfo = new ProcessStartInfo("bash")
{
@@ -36,8 +41,6 @@ public static async Task RunAsync(
}
if (!string.IsNullOrEmpty(retroDir))
{
- // Still forwarded to local-build.sh under its own internal name -
- // --retro-rewind-package-dir - matching LocalBuild.ps1's own -RetroRewindPackageDirectory.
startInfo.ArgumentList.Add("--retro-rewind-package-dir"); startInfo.ArgumentList.Add(retroDir);
}
if (!string.IsNullOrEmpty(retroWfcOfflineDir))
@@ -50,6 +53,18 @@ public static async Task RunAsync(
{
startInfo.ArgumentList.Add("--translator-bin"); startInfo.ArgumentList.Add(translatorBin);
}
+ if (macOS)
+ {
+ if (!string.IsNullOrEmpty(cmakeBin))
+ {
+ startInfo.ArgumentList.Add("--cmake"); startInfo.ArgumentList.Add(cmakeBin);
+ }
+ if (!string.IsNullOrEmpty(ninjaBin))
+ {
+ startInfo.ArgumentList.Add("--ninja"); startInfo.ArgumentList.Add(ninjaBin);
+ }
+ goto StartBuild;
+ }
// Forwarded by AppRun so the AppImage's bundled clang/lld (see prepare-portable-clang.sh)
// is used instead of local-build.sh's own default of whatever clang is on $PATH.
if (!string.IsNullOrEmpty(ccBin))
@@ -83,6 +98,7 @@ public static async Task RunAsync(
startInfo.ArgumentList.Add("--sysroot"); startInfo.ArgumentList.Add(sysroot);
}
+ StartBuild:
using var process = new Process { StartInfo = startInfo };
var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96);
@@ -105,7 +121,7 @@ public static async Task RunAsync(
if (process.ExitCode != 0)
{
- throw new InvalidOperationException($"local-build.sh failed (exit {process.ExitCode}). See diagnostics above.");
+ throw new InvalidOperationException($"{Path.GetFileName(script)} failed (exit {process.ExitCode}). See diagnostics above.");
}
}
diff --git a/Launcher/WiiCompiled.Setup.Linux/Models.cs b/Launcher/WiiCompiled.Setup.Linux/Models.cs
index ebe9964ee..9a68e123e 100644
--- a/Launcher/WiiCompiled.Setup.Linux/Models.cs
+++ b/Launcher/WiiCompiled.Setup.Linux/Models.cs
@@ -15,6 +15,7 @@ internal sealed class ProductInstallRecord
public string DolSha256 { get; set; } = "";
public string RelSha256 { get; set; } = "";
public string BuiltUtc { get; set; } = "";
+ public string? RetroRewindDirectory { get; set; }
}
///
diff --git a/Launcher/WiiCompiled.Setup.Linux/Program.cs b/Launcher/WiiCompiled.Setup.Linux/Program.cs
index 9595398a5..f362d203c 100644
--- a/Launcher/WiiCompiled.Setup.Linux/Program.cs
+++ b/Launcher/WiiCompiled.Setup.Linux/Program.cs
@@ -38,16 +38,24 @@ private static async Task RunAsync(string[] args, CancellationTokenSource c
switch (command)
{
case "install":
+ case "silent":
await InstallAsync(flags, reporter, cts.Token);
break;
+ case "repair-products":
+ await RepairProductsAsync(flags, reporter, cts.Token);
+ break;
case "uninstall":
+ case "silent-uninstall":
Uninstall();
break;
case "launch-base":
+ case "launch_base":
return Launch("base", flags);
case "launch-retro":
+ case "launch_retro":
return Launch("retro-rewind", flags);
case "check-products":
+ case "check_products":
CheckProducts();
break;
default:
@@ -94,7 +102,7 @@ private static async Task InstallAsync(Dictionary flags, IInsta
// check further down is a simpler backstop, not the primary validation anymore.
if (installsRetro) retroDir = RetroRewindSource.ResolveRetroRewind6(retroDir!);
- var workspace = flags.GetValueOrDefault("workspace") ?? WorkspaceLocator.FindFrom(AppContext.BaseDirectory);
+ var workspace = flags.GetValueOrDefault("workspace") ?? FindWorkspace();
var manifest = ProjectManifest.Load(Path.Combine(workspace, "projects", "mkwii", "recomp.yml"));
var assetsDir = Path.Combine(workspace, "Assets");
@@ -123,6 +131,15 @@ await DiscTool.ValidateAndExtractAsync(isoPath, manifest, assetsDir, workspace,
var profiles = installsRetro ? new[] { "base", "retro-rewind" } : new[] { "base" };
var baseInstallDir = installsRetro ? DefaultInstallDir("base") : null;
var installDir = flags.GetValueOrDefault("install-dir") ?? DefaultInstallDir(installsRetro ? "retro-rewind" : "base");
+ var translatorBin = flags.GetValueOrDefault("translator-bin");
+ var cmakeBin = flags.GetValueOrDefault("cmake");
+ var ninjaBin = flags.GetValueOrDefault("ninja");
+ if (OperatingSystem.IsMacOS())
+ {
+ translatorBin ??= BundledTool("Translator.Cli");
+ cmakeBin ??= BundledTool(Path.Combine("cmake", "bin", "cmake"));
+ ninjaBin ??= BundledTool("ninja");
+ }
string? retroWfcOfflineDir = null;
if (downloadPayload)
@@ -158,12 +175,12 @@ await BuildRunner.RunAsync(
retroWfcOfflineDir,
skipPayload,
flags.ContainsKey("force-clean-build"),
- flags.GetValueOrDefault("translator-bin"),
+ translatorBin,
flags.GetValueOrDefault("cc"),
flags.GetValueOrDefault("cxx"),
flags.GetValueOrDefault("fuse-ld"),
- flags.GetValueOrDefault("cmake"),
- flags.GetValueOrDefault("ninja"),
+ cmakeBin,
+ ninjaBin,
flags.GetValueOrDefault("native-prebuilt-dir"),
sysroot,
reporter, token);
@@ -175,7 +192,10 @@ await BuildRunner.RunAsync(
foreach (var p in profiles)
{
var dir = p == "base" ? (baseInstallDir ?? installDir) : installDir;
- var exeName = p == "base" ? "WiiCompiled" : "RetroRewind";
+ var exeName = OperatingSystem.IsMacOS()
+ ? Path.Combine(p == "base" ? "WiiCompiled.app" : "RetroRewind.app", "Contents", "MacOS",
+ p == "base" ? "WiiCompiled" : "RetroRewind")
+ : p == "base" ? "WiiCompiled" : "RetroRewind";
var displayName = p == "base" ? "WiiCompiled (base game)" : "WiiCompiled (Retro Rewind)";
state.Products.RemoveAll(r => r.Profile == p);
state.Products.Add(new ProductInstallRecord
@@ -187,7 +207,8 @@ await BuildRunner.RunAsync(
RelSha256 = relSha,
BuiltUtc = DateTime.UtcNow.ToString("O"),
});
- DesktopEntry.Create(p, displayName, Path.Combine(dir, exeName));
+ if (!OperatingSystem.IsMacOS())
+ DesktopEntry.Create(p, displayName, Path.Combine(dir, exeName));
}
JsonState.Write(StatePath, state);
@@ -196,7 +217,9 @@ await BuildRunner.RunAsync(
// that isn't main.dol/StaticR.rel. Linux has no --portable flag, so this is always the
// per-user Config.toml (RuntimeConfiguration.ResolveConfigPath's Windows-only portable-root
// lookup has nothing to find here either way).
- var configPath = RuntimeConfiguration.ApplicationDataConfigPath;
+ var configPath = OperatingSystem.IsMacOS()
+ ? Path.Combine(MacApplicationDataDirectory, "Config.toml")
+ : RuntimeConfiguration.ApplicationDataConfigPath;
var dataDir = Path.Combine(assetsDir, "DATA");
if (Directory.Exists(dataDir))
{
@@ -210,6 +233,26 @@ await BuildRunner.RunAsync(
reporter.Progress(InstallStages.Shortcuts, "Install complete", 99);
}
+ private static async Task RepairProductsAsync(
+ Dictionary flags, IInstallReporter reporter, CancellationToken token)
+ {
+ var state = JsonState.TryRead(StatePath);
+ if (state is null || state.Products.Count == 0)
+ throw new InvalidOperationException("Nothing is installed to repair. Run 'install' first.");
+
+ var retro = flags.GetValueOrDefault("retro-dir") ??
+ state.Products.FirstOrDefault(record => record.Profile == "retro-rewind")?.RetroRewindDirectory;
+ if (retro is not null)
+ {
+ flags["retro-dir"] = retro;
+ flags["skip-retro-wfc-payload"] = null;
+ }
+ flags["install-dir"] = flags.GetValueOrDefault("install-dir") ??
+ state.Products.FirstOrDefault(record => record.Profile == "retro-rewind")?.InstallDirectory ??
+ state.Products[0].InstallDirectory;
+ await InstallAsync(flags, reporter, token);
+ }
+
private static void Uninstall()
{
// Matches Windows: UninstallService.cs removes the whole install directory unconditionally -
@@ -217,11 +260,19 @@ private static void Uninstall()
var state = JsonState.TryRead(StatePath) ?? new InstallState();
foreach (var record in state.Products.ToList())
{
- if (Directory.Exists(record.InstallDirectory))
+ if (OperatingSystem.IsMacOS())
{
- Directory.Delete(record.InstallDirectory, recursive: true);
+ var executable = Path.Combine(record.InstallDirectory, record.ExecutableName);
+ var appBundle = Directory.GetParent(Path.GetDirectoryName(executable)!)?.Parent?.FullName;
+ if (appBundle is not null && Directory.Exists(appBundle))
+ Directory.Delete(appBundle, recursive: true);
+ }
+ else
+ {
+ if (Directory.Exists(record.InstallDirectory))
+ Directory.Delete(record.InstallDirectory, recursive: true);
+ DesktopEntry.Remove(record.Profile);
}
- DesktopEntry.Remove(record.Profile);
state.Products.Remove(record);
Console.WriteLine($"Removed {record.Profile} from {record.InstallDirectory}");
}
@@ -286,12 +337,39 @@ private static string Sha256Of(string path)
private static string? Sha256IfExists(string path) => File.Exists(path) ? Sha256Of(path) : null;
private static string StatePath => Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WiiCompiled", "install-state.json");
+ OperatingSystem.IsMacOS() ? MacApplicationDataDirectory :
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ OperatingSystem.IsMacOS() ? "install-state.json" : Path.Combine("WiiCompiled", "install-state.json"));
private static string DefaultInstallDir(string profile) => Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WiiCompiled", "Install",
+ OperatingSystem.IsMacOS() ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Applications", "WiiCompiled") :
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WiiCompiled", "Install"),
profile == "base" ? "Base" : "RetroRewind");
+ private static string MacApplicationDataDirectory => Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "WiiCompiled");
+
+ private static string? BundledTool(string relativePath)
+ {
+ var path = Path.Combine(AppContext.BaseDirectory, "tools", relativePath);
+ return File.Exists(path) ? path : null;
+ }
+
+ private static string FindWorkspace()
+ {
+ var marker = OperatingSystem.IsMacOS() ? "local-build-macos.command" : "local-build.sh";
+ var current = new DirectoryInfo(AppContext.BaseDirectory);
+ for (var level = 0; level <= 8 && current is not null; level++, current = current.Parent)
+ {
+ var packagedWorkspace = Path.Combine(current.FullName, "workspace");
+ if (File.Exists(Path.Combine(packagedWorkspace, "Launcher", marker)))
+ return packagedWorkspace;
+ if (File.Exists(Path.Combine(current.FullName, "Launcher", marker)))
+ return current.FullName;
+ }
+ throw new InvalidOperationException($"Could not find Launcher/{marker}. Pass --workspace explicitly.");
+ }
+
///
/// A single pass that finds both the command word and every --flag[=value] pair, regardless
/// of order - a --flag may appear before or after the command (see the AppRun caller note in
@@ -308,6 +386,16 @@ private static (string? Command, Dictionary Flags) ParseArgs(st
if (arg.StartsWith("--", StringComparison.Ordinal))
{
var name = arg[2..];
+ name = name switch
+ {
+ "silent" => "silent",
+ "uninstall" => "uninstall",
+ "launch-base" => "launch-base",
+ "launch-retro" => "launch-retro",
+ "check-products" => "check-products",
+ "repair-products" => "repair-products",
+ _ => name
+ };
if (i + 1 < args.Length && !args[i + 1].StartsWith("--", StringComparison.Ordinal))
{
flags[name] = args[++i];
@@ -319,7 +407,7 @@ private static (string? Command, Dictionary Flags) ParseArgs(st
}
else if (command is null)
{
- command = arg;
+ command = arg.StartsWith("--", StringComparison.Ordinal) ? arg[2..] : arg;
}
}
return (command, flags);
@@ -339,6 +427,7 @@ install [--game ISO_PATH] [--install-dir DIR] [--retro-dir DIR
launch-base
launch-retro
check-products
+ repair-products
--version
""");
}
From b2be6ba2ef7c58860c2a52895a561e8d7f793378 Mon Sep 17 00:00:00 2001
From: licarto <334195767+licarto@users.noreply.github.com>
Date: Sun, 27 Sep 2026 15:23:56 +0200
Subject: [PATCH 2/3] Add native macOS arm64 setup distribution
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
.github/workflows/build.yml | 15 ++
.github/workflows/package.yml | 34 ++-
.../WiiCompiled.Setup.Linux/BuildRunner.cs | 65 ++---
Launcher/WiiCompiled.Setup.Linux/Models.cs | 4 +-
Launcher/WiiCompiled.Setup.Linux/Program.cs | 250 ++++++++++++++++--
.../WiiCompiled.Setup.Linux.csproj | 2 +-
Launcher/package-macos-setup.sh | 137 ++++++++++
README.md | 7 +-
docs/building-macos.md | 29 ++
9 files changed, 478 insertions(+), 65 deletions(-)
create mode 100644 Launcher/package-macos-setup.sh
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a156710a5..bc7234e0d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -46,3 +46,18 @@ jobs:
- name: Test
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
+
+ macos-setup:
+ name: macOS arm64 setup package
+ runs-on: macos-14
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ persist-credentials: false
+
+ - uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Build and smoke-test setup archive
+ run: bash Launcher/package-macos-setup.sh --output-dir Launcher/dist
diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml
index b97c62a13..683926ab1 100644
--- a/.github/workflows/package.yml
+++ b/.github/workflows/package.yml
@@ -1,9 +1,7 @@
name: Package installers
-# Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe /
-# WiiCompiled-Setup-x86_64.AppImage) via Launcher/Build-Installer.ps1 and
-# Launcher/build-appimage.sh respectively - the same scripts a maintainer runs by hand today to
-# produce a GitHub Release asset. This does NOT build the actual translated game executable:
+# Builds the per-platform installer/setup tools (WiiCompiled-Setup.exe,
+# WiiCompiled-Setup-.AppImage, and WiiCompiled-Setup-macos-arm64.zip). This does NOT build the actual translated game executable:
# that step requires the end user's own Mario Kart Wii dump (Assets/main.dol, Assets/StaticR.rel),
# which is proprietary and not present in this repository or in CI.
on:
@@ -90,6 +88,28 @@ jobs:
if-no-files-found: error
archive: false
+ macos-setup:
+ name: macOS setup (arm64)
+ runs-on: macos-14
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ persist-credentials: false
+
+ - uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Build and smoke-test setup archive
+ run: bash Launcher/package-macos-setup.sh --output-dir Launcher/dist
+
+ - uses: actions/upload-artifact@v7
+ with:
+ name: WiiCompiled-Setup-macos-arm64
+ path: Launcher/dist/WiiCompiled-Setup-macos-arm64.zip
+ if-no-files-found: error
+ archive: false
+
# Publishes the packaged installers as a GitHub Release whenever a v* tag is pushed. Wheel Wizard
# discovers updates from these releases, so the contract it relies on is enforced here: a full
# (non-prerelease) release whose tag is v, carrying an asset named exactly
@@ -97,7 +117,7 @@ jobs:
release:
name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
- needs: [linux-appimage, windows-installer, recompilation]
+ needs: [linux-appimage, windows-installer, macos-setup, recompilation]
runs-on: ubuntu-latest
permissions:
contents: write
@@ -125,6 +145,8 @@ jobs:
check Launcher/WiiCompiled.Setup.Windows/Program.cs "public const string Version = \"$version\";"
check Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj "$version"
check Launcher/Build-Installer.ps1 "ProductVersion = '$version'"
+ check Launcher/WiiCompiled.Setup.Linux/Models.cs "public const string Version = \"$version\";"
+ check Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj "$version"
exit $status
# The build jobs upload with `archive: false`, which stores each installer as a raw file
@@ -143,7 +165,7 @@ jobs:
set -euo pipefail
ls -lR artifacts
assets=()
- for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage; do
+ for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage WiiCompiled-Setup-macos-arm64.zip; do
found="$(find artifacts -type f -name "$name" | head -n 1)"
[ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; }
assets+=("$found")
diff --git a/Launcher/WiiCompiled.Setup.Linux/BuildRunner.cs b/Launcher/WiiCompiled.Setup.Linux/BuildRunner.cs
index 4f1e514ae..463966bda 100644
--- a/Launcher/WiiCompiled.Setup.Linux/BuildRunner.cs
+++ b/Launcher/WiiCompiled.Setup.Linux/BuildRunner.cs
@@ -63,42 +63,43 @@ public static async Task RunAsync(
{
startInfo.ArgumentList.Add("--ninja"); startInfo.ArgumentList.Add(ninjaBin);
}
- goto StartBuild;
}
- // Forwarded by AppRun so the AppImage's bundled clang/lld (see prepare-portable-clang.sh)
- // is used instead of local-build.sh's own default of whatever clang is on $PATH.
- if (!string.IsNullOrEmpty(ccBin))
+ else
{
- startInfo.ArgumentList.Add("--cc"); startInfo.ArgumentList.Add(ccBin);
- }
- if (!string.IsNullOrEmpty(cxxBin))
- {
- startInfo.ArgumentList.Add("--cxx"); startInfo.ArgumentList.Add(cxxBin);
- }
- if (!string.IsNullOrEmpty(fuseLd))
- {
- startInfo.ArgumentList.Add("--fuse-ld"); startInfo.ArgumentList.Add(fuseLd);
- }
- if (!string.IsNullOrEmpty(cmakeBin))
- {
- startInfo.ArgumentList.Add("--cmake"); startInfo.ArgumentList.Add(cmakeBin);
- }
- if (!string.IsNullOrEmpty(ninjaBin))
- {
- startInfo.ArgumentList.Add("--ninja"); startInfo.ArgumentList.Add(ninjaBin);
- }
- // Forwarded by AppRun so the AppImage's bundled precompiled aurora/third-party package (see
- // Prepare-NativePrebuilt.sh) is used instead of local-build.sh compiling aurora-main itself.
- if (!string.IsNullOrEmpty(nativePrebuiltDir))
- {
- startInfo.ArgumentList.Add("--native-prebuilt-dir"); startInfo.ArgumentList.Add(nativePrebuiltDir);
- }
- if (!string.IsNullOrEmpty(sysroot))
- {
- startInfo.ArgumentList.Add("--sysroot"); startInfo.ArgumentList.Add(sysroot);
+ // Forwarded by AppRun so the AppImage's bundled clang/lld (see prepare-portable-tools.sh)
+ // is used instead of local-build.sh's own default of whatever clang is on $PATH.
+ if (!string.IsNullOrEmpty(ccBin))
+ {
+ startInfo.ArgumentList.Add("--cc"); startInfo.ArgumentList.Add(ccBin);
+ }
+ if (!string.IsNullOrEmpty(cxxBin))
+ {
+ startInfo.ArgumentList.Add("--cxx"); startInfo.ArgumentList.Add(cxxBin);
+ }
+ if (!string.IsNullOrEmpty(fuseLd))
+ {
+ startInfo.ArgumentList.Add("--fuse-ld"); startInfo.ArgumentList.Add(fuseLd);
+ }
+ if (!string.IsNullOrEmpty(cmakeBin))
+ {
+ startInfo.ArgumentList.Add("--cmake"); startInfo.ArgumentList.Add(cmakeBin);
+ }
+ if (!string.IsNullOrEmpty(ninjaBin))
+ {
+ startInfo.ArgumentList.Add("--ninja"); startInfo.ArgumentList.Add(ninjaBin);
+ }
+ // Forwarded by AppRun so the AppImage's bundled precompiled aurora/third-party package
+ // is used instead of local-build.sh compiling aurora-main itself.
+ if (!string.IsNullOrEmpty(nativePrebuiltDir))
+ {
+ startInfo.ArgumentList.Add("--native-prebuilt-dir"); startInfo.ArgumentList.Add(nativePrebuiltDir);
+ }
+ if (!string.IsNullOrEmpty(sysroot))
+ {
+ startInfo.ArgumentList.Add("--sysroot"); startInfo.ArgumentList.Add(sysroot);
+ }
}
- StartBuild:
using var process = new Process { StartInfo = startInfo };
var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96);
diff --git a/Launcher/WiiCompiled.Setup.Linux/Models.cs b/Launcher/WiiCompiled.Setup.Linux/Models.cs
index 9a68e123e..e2d9fe63c 100644
--- a/Launcher/WiiCompiled.Setup.Linux/Models.cs
+++ b/Launcher/WiiCompiled.Setup.Linux/Models.cs
@@ -16,10 +16,12 @@ internal sealed class ProductInstallRecord
public string RelSha256 { get; set; } = "";
public string BuiltUtc { get; set; } = "";
public string? RetroRewindDirectory { get; set; }
+ public bool UsesRetroWfcPayload { get; set; }
+ public string? CodePulSha256 { get; set; }
}
///
-/// The whole flat state document this tool keeps at ~/.local/share/WiiCompiled/install-state.json.
+/// The whole flat per-user state document this tool keeps.
/// Deliberately not a fingerprint tree: local-build.sh already does its own incremental-rebuild
/// caching, so this only needs to remember where things were installed and what they were built
/// against, not decide when to rebuild.
diff --git a/Launcher/WiiCompiled.Setup.Linux/Program.cs b/Launcher/WiiCompiled.Setup.Linux/Program.cs
index f362d203c..b150e5229 100644
--- a/Launcher/WiiCompiled.Setup.Linux/Program.cs
+++ b/Launcher/WiiCompiled.Setup.Linux/Program.cs
@@ -1,4 +1,5 @@
using System.Security.Cryptography;
+using System.Runtime.Versioning;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Linux;
@@ -56,6 +57,8 @@ private static async Task RunAsync(string[] args, CancellationTokenSource c
return Launch("retro-rewind", flags);
case "check-products":
case "check_products":
+ if (OperatingSystem.IsMacOS())
+ return CheckMacProducts(flags, reporter);
CheckProducts();
break;
default:
@@ -82,6 +85,7 @@ private static async Task RunAsync(string[] args, CancellationTokenSource c
private static async Task InstallAsync(Dictionary flags, IInstallReporter reporter, CancellationToken token)
{
+ EnsureSupportedHost();
var retroDir = flags.GetValueOrDefault("retro-dir");
var installsRetro = !string.IsNullOrEmpty(retroDir);
var downloadPayload = flags.ContainsKey("download-retro-wfc-payload");
@@ -137,8 +141,8 @@ await DiscTool.ValidateAndExtractAsync(isoPath, manifest, assetsDir, workspace,
if (OperatingSystem.IsMacOS())
{
translatorBin ??= BundledTool("Translator.Cli");
- cmakeBin ??= BundledTool(Path.Combine("cmake", "bin", "cmake"));
- ninjaBin ??= BundledTool("ninja");
+ if (BundledTool("nodtool") is { } nodtool)
+ flags.TryAdd("disc-tool-bin", nodtool);
}
string? retroWfcOfflineDir = null;
@@ -206,6 +210,11 @@ await BuildRunner.RunAsync(
DolSha256 = dolSha,
RelSha256 = relSha,
BuiltUtc = DateTime.UtcNow.ToString("O"),
+ RetroRewindDirectory = p == "retro-rewind" ? retroDir : null,
+ UsesRetroWfcPayload = p == "retro-rewind" && downloadPayload,
+ CodePulSha256 = p == "retro-rewind" && retroDir is not null
+ ? Sha256IfExists(Path.Combine(retroDir, "Binaries", "Code.pul"))
+ : null,
});
if (!OperatingSystem.IsMacOS())
DesktopEntry.Create(p, displayName, Path.Combine(dir, exeName));
@@ -217,9 +226,7 @@ await BuildRunner.RunAsync(
// that isn't main.dol/StaticR.rel. Linux has no --portable flag, so this is always the
// per-user Config.toml (RuntimeConfiguration.ResolveConfigPath's Windows-only portable-root
// lookup has nothing to find here either way).
- var configPath = OperatingSystem.IsMacOS()
- ? Path.Combine(MacApplicationDataDirectory, "Config.toml")
- : RuntimeConfiguration.ApplicationDataConfigPath;
+ var configPath = OperatingSystem.IsMacOS() ? MacConfigPath : RuntimeConfiguration.ApplicationDataConfigPath;
var dataDir = Path.Combine(assetsDir, "DATA");
if (Directory.Exists(dataDir))
{
@@ -240,12 +247,23 @@ private static async Task RepairProductsAsync(
if (state is null || state.Products.Count == 0)
throw new InvalidOperationException("Nothing is installed to repair. Run 'install' first.");
+ var retroRecord = state.Products.FirstOrDefault(record => record.Profile == "retro-rewind");
var retro = flags.GetValueOrDefault("retro-dir") ??
- state.Products.FirstOrDefault(record => record.Profile == "retro-rewind")?.RetroRewindDirectory;
+ (retroRecord is null
+ ? null
+ : retroRecord.RetroRewindDirectory ??
+ (OperatingSystem.IsMacOS() ? RuntimeConfiguration.GetRetroRewindRoot(MacConfigPath) : null));
+ if (retroRecord is not null && string.IsNullOrWhiteSpace(retro))
+ throw new InvalidOperationException(
+ "The installed Retro Rewind source directory is not recorded. Supply --retro-dir to repair it.");
if (retro is not null)
{
flags["retro-dir"] = retro;
- flags["skip-retro-wfc-payload"] = null;
+ if (!flags.ContainsKey("download-retro-wfc-payload") &&
+ !flags.ContainsKey("skip-retro-wfc-payload"))
+ flags[retroRecord?.UsesRetroWfcPayload == true
+ ? "download-retro-wfc-payload"
+ : "skip-retro-wfc-payload"] = null;
}
flags["install-dir"] = flags.GetValueOrDefault("install-dir") ??
state.Products.FirstOrDefault(record => record.Profile == "retro-rewind")?.InstallDirectory ??
@@ -255,15 +273,14 @@ private static async Task RepairProductsAsync(
private static void Uninstall()
{
- // Matches Windows: UninstallService.cs removes the whole install directory unconditionally -
- // there is no partial-product uninstall on either platform.
+ // Remove the installed products but keep the build workspace and user-owned disc data.
var state = JsonState.TryRead(StatePath) ?? new InstallState();
foreach (var record in state.Products.ToList())
{
if (OperatingSystem.IsMacOS())
{
- var executable = Path.Combine(record.InstallDirectory, record.ExecutableName);
- var appBundle = Directory.GetParent(Path.GetDirectoryName(executable)!)?.Parent?.FullName;
+ var appBundle = Path.Combine(record.InstallDirectory,
+ record.Profile == "base" ? "WiiCompiled.app" : "RetroRewind.app");
if (appBundle is not null && Directory.Exists(appBundle))
Directory.Delete(appBundle, recursive: true);
}
@@ -287,14 +304,14 @@ private static int Launch(string profile, Dictionary flags)
{
var installHint = profile == "retro-rewind"
? "install --retro-dir {--download-retro-wfc-payload | --skip-retro-wfc-payload}"
- : $"install --profile {profile}";
+ : "install --game ";
Console.Error.WriteLine($"{profile} is not installed. Run '{installHint}' first.");
return 1;
}
var exePath = Path.Combine(record.InstallDirectory, record.ExecutableName);
if (!File.Exists(exePath))
{
- Console.Error.WriteLine($"Installed executable is missing: {exePath}. Run 'install --profile {profile}' again.");
+ Console.Error.WriteLine($"Installed executable is missing: {exePath}. Run 'repair-products' to rebuild it.");
return 1;
}
var startInfo = new System.Diagnostics.ProcessStartInfo(exePath)
@@ -328,6 +345,77 @@ private static void CheckProducts()
}
}
+ private static int CheckMacProducts(Dictionary flags, IInstallReporter reporter)
+ {
+ var state = JsonState.TryRead(StatePath) ?? new InstallState();
+ var assetsDirectory = string.IsNullOrWhiteSpace(state.Workspace)
+ ? ""
+ : Path.Combine(state.Workspace, "Assets");
+ var currentDol = string.IsNullOrEmpty(assetsDirectory)
+ ? null
+ : Sha256IfExists(Path.Combine(assetsDirectory, "main.dol"));
+ var currentRel = string.IsNullOrEmpty(assetsDirectory)
+ ? null
+ : Sha256IfExists(Path.Combine(assetsDirectory, "StaticR.rel"));
+ var baseState = GetMacProductState(state, "base", currentDol, currentRel);
+ var retroState = GetMacProductState(state, "retro-rewind", currentDol, currentRel);
+ var rebuildRequired = NeedsMacRepair(baseState.Status) || NeedsMacRepair(retroState.Status);
+ var installDirectory = Path.GetFullPath(flags.GetValueOrDefault("install-dir") ??
+ DefaultInstallDir("base"));
+
+ Console.Out.WriteLine(System.Text.Json.JsonSerializer.Serialize(new
+ {
+ type = "products",
+ setupVersion = ProductInfo.Version,
+ installDir = installDirectory,
+ rebuildRequired,
+ @base = baseState,
+ retroRewind = retroState,
+ }, new System.Text.Json.JsonSerializerOptions
+ {
+ PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
+ }));
+ Console.Out.Flush();
+ (reporter as NdjsonInstallReporter)?.Success(installDirectory);
+ return rebuildRequired ? 2 : 0;
+ }
+
+ private static MacProductStatus GetMacProductState(
+ InstallState state, string profile, string? currentDol, string? currentRel)
+ {
+ var record = state.Products.FirstOrDefault(product => product.Profile == profile);
+ if (record is null)
+ return new MacProductStatus("absent", profile == "base"
+ ? "WiiCompiled is not installed here."
+ : "Retro Rewind is not installed.");
+
+ var executable = Path.Combine(record.InstallDirectory, record.ExecutableName);
+ if (!File.Exists(executable))
+ return new MacProductStatus("broken", $"Installed executable is missing: {executable}");
+ if (currentDol is null || currentRel is null)
+ return new MacProductStatus("inputs-missing", "The workspace game inputs are missing.");
+ if (currentDol != record.DolSha256 || currentRel != record.RelSha256)
+ return new MacProductStatus("compile-inputs-changed",
+ "The local game inputs changed since this product was built.");
+ if (profile == "retro-rewind")
+ {
+ var codePulPath = record.RetroRewindDirectory is null
+ ? null
+ : Path.Combine(record.RetroRewindDirectory, "Binaries", "Code.pul");
+ var codePulSha = codePulPath is null ? null : Sha256IfExists(codePulPath);
+ if (codePulSha is null)
+ return new MacProductStatus("inputs-missing", "The recorded Retro Rewind Code.pul is missing.");
+ if (record.CodePulSha256 is null || codePulSha != record.CodePulSha256)
+ return new MacProductStatus("code-pul-changed",
+ "Retro Rewind Code.pul changed since this product was built.");
+ }
+ return new MacProductStatus("current", "");
+ }
+
+ private static bool NeedsMacRepair(string status) => status is not "current" and not "absent";
+
+ private sealed record MacProductStatus(string Status, string Detail);
+
private static string Sha256Of(string path)
{
using var stream = File.OpenRead(path);
@@ -348,6 +436,7 @@ private static string DefaultInstallDir(string profile) => Path.Combine(
private static string MacApplicationDataDirectory => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "WiiCompiled");
+ private static string MacConfigPath => Path.Combine(MacApplicationDataDirectory, "Config.toml");
private static string? BundledTool(string relativePath)
{
@@ -355,6 +444,16 @@ private static string DefaultInstallDir(string profile) => Path.Combine(
return File.Exists(path) ? path : null;
}
+ private static void EnsureSupportedHost()
+ {
+ if (!OperatingSystem.IsMacOS()) return;
+ if (System.Runtime.InteropServices.RuntimeInformation.OSArchitecture !=
+ System.Runtime.InteropServices.Architecture.Arm64)
+ throw new PlatformNotSupportedException("The macOS setup requires Apple Silicon (arm64).");
+ if (!OperatingSystem.IsMacOSVersionAtLeast(14))
+ throw new PlatformNotSupportedException("The macOS setup requires macOS 14 (Sonoma) or later.");
+ }
+
private static string FindWorkspace()
{
var marker = OperatingSystem.IsMacOS() ? "local-build-macos.command" : "local-build.sh";
@@ -363,13 +462,107 @@ private static string FindWorkspace()
{
var packagedWorkspace = Path.Combine(current.FullName, "workspace");
if (File.Exists(Path.Combine(packagedWorkspace, "Launcher", marker)))
- return packagedWorkspace;
+ return OperatingSystem.IsMacOS()
+ ? PrepareMacWorkspace(packagedWorkspace)
+ : packagedWorkspace;
if (File.Exists(Path.Combine(current.FullName, "Launcher", marker)))
return current.FullName;
}
throw new InvalidOperationException($"Could not find Launcher/{marker}. Pass --workspace explicitly.");
}
+ [SupportedOSPlatform("macos")]
+ private static string PrepareMacWorkspace(string packagedWorkspace)
+ {
+ var versionFile = Path.Combine(packagedWorkspace, ".setup-source-version");
+ if (!File.Exists(versionFile))
+ throw new InvalidDataException("The packaged macOS setup is missing its source version marker.");
+
+ var persistentWorkspace = Path.Combine(MacApplicationDataDirectory, "BuildWorkspace");
+ var persistentVersionFile = Path.Combine(persistentWorkspace, ".setup-source-version");
+ var version = File.ReadAllText(versionFile).Trim();
+ if (File.Exists(persistentVersionFile) &&
+ string.Equals(File.ReadAllText(persistentVersionFile).Trim(), version, StringComparison.Ordinal) &&
+ File.Exists(Path.Combine(persistentWorkspace, "Launcher", "local-build-macos.command")))
+ return persistentWorkspace;
+
+ Directory.CreateDirectory(persistentWorkspace);
+ foreach (var directory in new[] { "projects", "runtime", "aurora-main" })
+ ReplacePackagedDirectory(Path.Combine(packagedWorkspace, directory),
+ Path.Combine(persistentWorkspace, directory));
+
+ var sourceLauncher = Path.Combine(packagedWorkspace, "Launcher");
+ var targetLauncher = Path.Combine(persistentWorkspace, "Launcher");
+ Directory.CreateDirectory(Path.Combine(targetLauncher, "macos"));
+ foreach (var script in new[]
+ {
+ "local-build-macos.command",
+ Path.Combine("macos", "extract-disc.command"),
+ Path.Combine("macos", "publish-app.command"),
+ })
+ {
+ var source = Path.Combine(sourceLauncher, script);
+ var target = Path.Combine(targetLauncher, script);
+ Directory.CreateDirectory(Path.GetDirectoryName(target)!);
+ File.Copy(source, target, overwrite: true);
+ File.SetUnixFileMode(target, UnixFileMode.UserRead | UnixFileMode.UserWrite |
+ UnixFileMode.UserExecute | UnixFileMode.GroupRead |
+ UnixFileMode.GroupExecute | UnixFileMode.OtherRead |
+ UnixFileMode.OtherExecute);
+ }
+
+ foreach (var name in new[] { "LICENSE", "THIRD-PARTY-NOTICES.md" })
+ File.Copy(Path.Combine(packagedWorkspace, name),
+ Path.Combine(persistentWorkspace, name), overwrite: true);
+
+ File.WriteAllText(persistentVersionFile, version);
+ return persistentWorkspace;
+ }
+
+ [SupportedOSPlatform("macos")]
+ private static void ReplacePackagedDirectory(string source, string destination)
+ {
+ if (!Directory.Exists(source))
+ throw new DirectoryNotFoundException($"The packaged setup source directory is missing: {source}");
+ if (Directory.Exists(destination))
+ Directory.Delete(destination, recursive: true);
+ CopyPackagedDirectory(source, destination);
+ }
+
+ [SupportedOSPlatform("macos")]
+ private static void CopyPackagedDirectory(string source, string destination)
+ {
+ var sourceInfo = new DirectoryInfo(source);
+ if (sourceInfo.LinkTarget is { } linkTarget)
+ {
+ Directory.CreateSymbolicLink(destination, linkTarget);
+ return;
+ }
+
+ Directory.CreateDirectory(destination);
+ foreach (var entry in sourceInfo.EnumerateFileSystemInfos())
+ {
+ var target = Path.Combine(destination, entry.Name);
+ if (entry is DirectoryInfo directory)
+ {
+ if (directory.LinkTarget is { } directoryTarget)
+ Directory.CreateSymbolicLink(target, directoryTarget);
+ else
+ CopyPackagedDirectory(directory.FullName, target);
+ }
+ else if (entry is FileInfo file)
+ {
+ if (file.LinkTarget is { } fileTarget)
+ File.CreateSymbolicLink(target, fileTarget);
+ else
+ {
+ File.Copy(file.FullName, target);
+ File.SetUnixFileMode(target, File.GetUnixFileMode(file.FullName));
+ }
+ }
+ }
+ }
+
///
/// A single pass that finds both the command word and every --flag[=value] pair, regardless
/// of order - a --flag may appear before or after the command (see the AppRun caller note in
@@ -380,22 +573,31 @@ private static (string? Command, Dictionary Flags) ParseArgs(st
{
string? command = null;
var flags = new Dictionary();
+ static string? CommandForOption(string option) => option switch
+ {
+ "--install" or "--silent" => "install",
+ "--uninstall" or "--silent-uninstall" => "uninstall",
+ "--launch-base" or "--launch_base" => "launch-base",
+ "--launch-retro" or "--launch_retro" => "launch-retro",
+ "--check-products" or "--check_products" => "check-products",
+ "--repair-products" => "repair-products",
+ _ => null,
+ };
+
for (var i = 0; i < args.Length; i++)
{
var arg = args[i];
if (arg.StartsWith("--", StringComparison.Ordinal))
{
var name = arg[2..];
- name = name switch
+ var optionCommand = CommandForOption(arg);
+ if (optionCommand is not null)
{
- "silent" => "silent",
- "uninstall" => "uninstall",
- "launch-base" => "launch-base",
- "launch-retro" => "launch-retro",
- "check-products" => "check-products",
- "repair-products" => "repair-products",
- _ => name
- };
+ if (command is not null)
+ throw new ArgumentException($"Only one command may be specified (already selected '{command}').");
+ command = optionCommand;
+ continue;
+ }
if (i + 1 < args.Length && !args[i + 1].StartsWith("--", StringComparison.Ordinal))
{
flags[name] = args[++i];
@@ -407,7 +609,7 @@ private static (string? Command, Dictionary Flags) ParseArgs(st
}
else if (command is null)
{
- command = arg.StartsWith("--", StringComparison.Ordinal) ? arg[2..] : arg;
+ command = arg;
}
}
return (command, flags);
diff --git a/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj b/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj
index 639ec228c..702e0752c 100644
--- a/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj
+++ b/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj
@@ -9,7 +9,7 @@
0.2.32
patchzy
WiiCompiled
- Command-line installer and launcher for WiiCompiled on Linux
+ Command-line installer and launcher for WiiCompiled on Linux and macOS
embedded
en
true
diff --git a/Launcher/package-macos-setup.sh b/Launcher/package-macos-setup.sh
new file mode 100644
index 000000000..094038198
--- /dev/null
+++ b/Launcher/package-macos-setup.sh
@@ -0,0 +1,137 @@
+#!/usr/bin/env bash
+# Build the distributable macOS arm64 setup CLI and a clean source workspace.
+set -euo pipefail
+
+fail() { printf 'package-macos-setup.sh: error: %s\n' "$*" >&2; exit 1; }
+usage() {
+ cat <<'EOF'
+Usage: package-macos-setup.sh [--output-dir DIR]
+
+Creates WiiCompiled-Setup-macos-arm64.zip. Requires an Apple Silicon Mac and
+.NET 8. The archive contains no game dump, translated game, or Retro Rewind data.
+EOF
+}
+
+script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+repo_root=$(cd "$script_dir/.." && pwd)
+output_dir="$repo_root/Launcher/dist"
+while (($#)); do
+ case "$1" in
+ --output-dir) output_dir=${2:-}; shift 2 ;;
+ -h|--help) usage; exit 0 ;;
+ *) fail "unknown argument: $1" ;;
+ esac
+done
+
+[[ $(uname -s) == Darwin ]] || fail 'packaging must run on macOS'
+[[ $(uname -m) == arm64 ]] || fail 'packaging requires an Apple Silicon arm64 runner'
+command -v dotnet >/dev/null || fail '.NET 8 SDK is required'
+command -v git >/dev/null || fail 'git is required'
+command -v ditto >/dev/null || fail 'ditto is required'
+
+version=$(sed -n 's/.*\([^<]*\)<\/Version>.*/\1/p' \
+ "$repo_root/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj" | head -n1)
+[[ "$version" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || fail 'could not read a valid setup version'
+[[ -f "$repo_root/Launcher/WiiCompiled.Setup.Linux/Models.cs" ]] || fail 'setup product version source is missing'
+grep -Fq "public const string Version = \"$version\";" \
+ "$repo_root/Launcher/WiiCompiled.Setup.Linux/Models.cs" || fail 'setup project and host versions differ'
+git -C "$repo_root" diff --quiet HEAD -- projects runtime aurora-main \
+ Launcher/local-build-macos.command Launcher/macos/extract-disc.command Launcher/macos/publish-app.command ||
+ fail 'tracked packaged sources have changes not present in HEAD; commit them before packaging'
+source_revision=$(git -C "$repo_root" rev-parse HEAD)
+
+stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-setup-macos.XXXXXX")
+trap 'rm -rf "$stage"' EXIT
+package="$stage/WiiCompiled-Setup-macos-arm64"
+mkdir -p "$package/tools" "$package/workspace"
+
+echo "Publishing WiiCompiled Setup $version for osx-arm64..."
+dotnet publish "$repo_root/Launcher/WiiCompiled.Setup.Linux" -c Release -r osx-arm64 \
+ --self-contained true -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
+ -o "$stage/setup"
+cp "$stage/setup/WiiCompiled.Setup.Linux" "$package/WiiCompiled-Setup-macos-arm64"
+chmod 755 "$package/WiiCompiled-Setup-macos-arm64"
+
+echo "Publishing the self-contained translator..."
+dotnet publish "$repo_root/translator/src/Translator.Cli" -c Release -r osx-arm64 \
+ --self-contained true -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
+ -o "$stage/translator"
+cp "$stage/translator/Translator.Cli" "$package/tools/Translator.Cli"
+chmod 755 "$package/tools/Translator.Cli"
+
+echo "Resolving the pinned nodtool release..."
+nodtool=$(dotnet run --project "$repo_root/Launcher/WiiCompiled.Setup.Common.Cli" -c Release \
+ --verbosity quiet -- --workspace "$repo_root" | tail -n1)
+[[ -f "$nodtool" ]] || fail "nodtool resolver did not produce a binary: $nodtool"
+cp "$nodtool" "$package/tools/nodtool"
+chmod 755 "$package/tools/nodtool"
+
+echo "Staging tracked, game-code-free build sources..."
+git -C "$repo_root" archive --format=tar HEAD projects/mkwii runtime aurora-main |
+ tar -xf - -C "$package/workspace"
+git -C "$repo_root" archive --format=tar HEAD \
+ Launcher/local-build-macos.command \
+ Launcher/macos/extract-disc.command \
+ Launcher/macos/publish-app.command |
+ tar -xf - -C "$package/workspace"
+printf '%s+%s\n' "$version" "$source_revision" > "$package/workspace/.setup-source-version"
+chmod 755 "$package/workspace/Launcher/local-build-macos.command" \
+ "$package/workspace/Launcher/macos/extract-disc.command" \
+ "$package/workspace/Launcher/macos/publish-app.command"
+cp "$repo_root/LICENSE" "$package/LICENSE"
+cp "$repo_root/THIRD-PARTY-NOTICES.md" "$package/THIRD-PARTY-NOTICES.md"
+cat > "$package/README.txt" </dev/null ||
+ fail 'packaged setup --version smoke test failed'
+mkdir -p "$stage/home"
+check_output=$(HOME="$stage/home" "$package/WiiCompiled-Setup-macos-arm64" --check-products --progress-json)
+grep -Fq '"type":"products"' <<< "$check_output" &&
+ grep -Fq '"type":"result","success":true' <<< "$check_output" ||
+ fail 'packaged check-products smoke test failed'
+if HOME="$stage/home" "$package/WiiCompiled-Setup-macos-arm64" --silent --progress-json \
+ > "$stage/install-smoke.log" 2>&1; then
+ fail 'install-without-disc smoke test unexpectedly succeeded'
+fi
+grep -Fq '"type":"result","success":false' "$stage/install-smoke.log" &&
+ grep -Fq 'No --game ISO was given' "$stage/install-smoke.log" ||
+ fail 'packaged install CLI contract smoke test failed'
+[[ -x "$package/tools/Translator.Cli" && -x "$package/tools/nodtool" ]] ||
+ fail 'a bundled command-line tool is not executable'
+if find "$package" \( -type d -name Assets -o -type f \( -name main.dol -o -name StaticR.rel -o -name '*.iso' -o -name '*.wbfs' \) \) -print -quit |
+ grep -q .; then
+ fail 'staging unexpectedly contains proprietary game data'
+fi
+
+mkdir -p "$output_dir"
+output_dir=$(cd "$output_dir" && pwd)
+output="$output_dir/WiiCompiled-Setup-macos-arm64.zip"
+rm -f "$output"
+ditto -c -k --norsrc --noqtn --keepParent "$package" "$output"
+[[ -s "$output" ]] || fail 'archive was not created'
+echo "Created unsigned macOS arm64 setup: $output"
diff --git a/README.md b/README.md
index aceca104f..d5bc3b262 100644
--- a/README.md
+++ b/README.md
@@ -97,7 +97,7 @@ Known limitations of the Wii Remote path:
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
- About 20 GB of free disk space during installation (Final game size ~5 GB)
- macOS 14 (Sonoma) or later on Apple Silicon
-- On macOS, Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
+- On macOS, Apple Xcode Command Line Tools, CMake, and Ninja
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted.
@@ -118,6 +118,11 @@ image under Settings, turn on **WiiCompiled (beta)**, and hit install from the H
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
+On macOS 14 or later with Apple Silicon, download `WiiCompiled-Setup-macos-arm64.zip` from this
+repository's [latest release](https://github.com/patchzyy/Wiicompiled/releases/latest), extract it,
+and follow the included `README.txt` to run the setup CLI. The release setup is not signed or
+notarized; see the [macOS build guide](docs/building-macos.md) for the quarantine and prerequisite
+instructions.
> [!CAUTION]
> Only take builds from this repository's
diff --git a/docs/building-macos.md b/docs/building-macos.md
index f1084c809..99205e80c 100644
--- a/docs/building-macos.md
+++ b/docs/building-macos.md
@@ -2,6 +2,35 @@
This guide covers building **WiiCompiled** (base game) and **Retro Rewind** from source on macOS for Apple Silicon (`arm64`). Follow these instructions to compile the native executables directly.
+## Downloadable setup
+
+For a packaged setup CLI rather than building the tools from source, download
+`WiiCompiled-Setup-macos-arm64.zip` from the [latest WiiCompiled release](https://github.com/patchzyy/Wiicompiled/releases/latest).
+The archive includes the self-contained setup and translator executables, the pinned `nodtool`
+binary, and only the tracked source files needed for the local native build. It contains no game
+dump, translated game executable, or Retro Rewind files. The setup compiles the game on your Mac
+from your own clean PAL `RMCP01` disc.
+
+The setup requires macOS 14 or later on Apple Silicon, Xcode Command Line Tools, CMake, and Ninja.
+Install the build tools with `brew install cmake ninja`. It does not require the .NET SDK; that is
+only needed when building the setup or translator from source.
+
+The release archive is currently unsigned and not notarized because release signing credentials
+are not configured. If macOS adds a quarantine attribute to the downloaded executable, verify that
+the archive came from the official release page and then remove quarantine from the setup binary
+before running it:
+
+```bash
+xattr -dr com.apple.quarantine WiiCompiled-Setup-macos-arm64
+cd WiiCompiled-Setup-macos-arm64
+./WiiCompiled-Setup-macos-arm64 --version
+./WiiCompiled-Setup-macos-arm64 --silent --game /path/to/RMCP01.iso
+```
+
+The setup CLI also supports `--check-products`, `--repair-products`, `--launch-base`, and
+`--uninstall`. Its default products are written under `~/Applications/WiiCompiled/`; use
+`--help` for the full command syntax.
+
> [!NOTE]
> If you only want to build the base game (**WiiCompiled**), look for sections marked **`(Skip if only building WiiCompiled)`** to bypass Retro Rewind and online payload steps.
From 380c2a3bb9082e8d1f0d55562d307bfdcc0db837 Mon Sep 17 00:00:00 2001
From: licarto <334195767+licarto@users.noreply.github.com>
Date: Sun, 27 Sep 2026 15:26:42 +0200
Subject: [PATCH 3/3] Clarify future macOS release asset availability
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
README.md | 10 +++++-----
docs/building-macos.md | 3 ++-
2 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index d5bc3b262..5989f12c5 100644
--- a/README.md
+++ b/README.md
@@ -118,11 +118,11 @@ image under Settings, turn on **WiiCompiled (beta)**, and hit install from the H
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
-On macOS 14 or later with Apple Silicon, download `WiiCompiled-Setup-macos-arm64.zip` from this
-repository's [latest release](https://github.com/patchzyy/Wiicompiled/releases/latest), extract it,
-and follow the included `README.txt` to run the setup CLI. The release setup is not signed or
-notarized; see the [macOS build guide](docs/building-macos.md) for the quarantine and prerequisite
-instructions.
+For macOS 14 or later on Apple Silicon, a tagged release that includes the macOS setup package
+publishes `WiiCompiled-Setup-macos-arm64.zip` on this repository's
+[Releases](https://github.com/patchzyy/Wiicompiled/releases) page. Extract it and follow the
+included `README.txt` to run the setup CLI. The release setup is not signed or notarized; see the
+[macOS build guide](docs/building-macos.md) for quarantine and prerequisite instructions.
> [!CAUTION]
> Only take builds from this repository's
diff --git a/docs/building-macos.md b/docs/building-macos.md
index 99205e80c..462fea924 100644
--- a/docs/building-macos.md
+++ b/docs/building-macos.md
@@ -5,7 +5,8 @@ This guide covers building **WiiCompiled** (base game) and **Retro Rewind** from
## Downloadable setup
For a packaged setup CLI rather than building the tools from source, download
-`WiiCompiled-Setup-macos-arm64.zip` from the [latest WiiCompiled release](https://github.com/patchzyy/Wiicompiled/releases/latest).
+`WiiCompiled-Setup-macos-arm64.zip` from a [WiiCompiled tagged release](https://github.com/patchzyy/Wiicompiled/releases)
+that includes the macOS setup package.
The archive includes the self-contained setup and translator executables, the pinned `nodtool`
binary, and only the tracked source files needed for the local native build. It contains no game
dump, translated game executable, or Retro Rewind files. The setup compiles the game on your Mac