refactor WiiCompiled.Setup into WiiCompiled.Setup.Windows and add WiiCompiled.Setup.Common

the idea behind this is C# code that is OS agnostic can go in WiiCompiled.Setup.Common to be shared by any OS specific code (eg: WiiCompiled.Setup.Windows and WiiCompiled.Setup.Linux).
This commit is contained in:
theofficialgman
2026-08-26 20:10:20 -04:00
parent f09590aa5d
commit c0ed2bfbeb
48 changed files with 444 additions and 340 deletions
@@ -11,7 +11,7 @@ internal static class BuildRunner
{
public static async Task RunAsync(
string workspace, string profile, string outputDir, string? baseOutputDir,
string? retroRewindPackageDir, string? retroWfcOfflineDir, bool skipRetroWfcPayload,
string? retroDir, string? retroWfcOfflineDir, bool skipRetroWfcPayload,
bool forceCleanBuild, string? translatorBin, IInstallReporter reporter, CancellationToken cancellationToken)
{
var script = Path.Combine(workspace, "Launcher", "local-build.sh");
@@ -31,9 +31,11 @@ internal static class BuildRunner
{
startInfo.ArgumentList.Add("--base-output-dir"); startInfo.ArgumentList.Add(baseOutputDir);
}
if (!string.IsNullOrEmpty(retroRewindPackageDir))
if (!string.IsNullOrEmpty(retroDir))
{
startInfo.ArgumentList.Add("--retro-rewind-package-dir"); startInfo.ArgumentList.Add(retroRewindPackageDir);
// 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))
{
+29 -33
View File
@@ -1,11 +1,11 @@
using System.Security.Cryptography;
using WiiCompiled.NodTool;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Linux;
/// <summary>
/// Validates and extracts the user's own Mario Kart Wii disc via `nodtool` (see
/// WiiCompiled.NodTool/NodToolProvider.cs) - a prebuilt, MIT/Apache-2.0-licensed CLI from
/// WiiCompiled.Setup.Common/NodToolProvider.cs) - a prebuilt, MIT/Apache-2.0-licensed CLI from
/// encounter/nod, replacing the earlier dependency on a system-installed `dolphin-tool`
/// (GPL-2.0-or-later, and not reliably packaged standalone by every distro).
/// </summary>
@@ -30,42 +30,38 @@ internal static class DiscTool
"Only your own legally-owned copy of that exact game/region can be used.");
}
// Extracted straight into Assets/DATA (kept, not a scratch dir) - the runtime reads course/
// texture/audio data from this directory live via [paths] dvd_root, not just at translation
// time, so it has to survive past this install (see Program.cs, which points dvd_root here).
reporter.Progress(InstallStages.ExtractDisc, "Extracting the disc image", 4);
var scratch = Path.Combine(Path.GetTempPath(), "wiicompiled-disc-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(scratch);
try
var dataDir = Path.Combine(assetsDirectory, "DATA");
if (Directory.Exists(dataDir)) Directory.Delete(dataDir, recursive: true);
await RunExtractAsync(nodTool, isoPath, dataDir, cancellationToken);
var dolPath = Path.Combine(dataDir, "sys", "main.dol");
var relPath = Path.Combine(dataDir, "files", "rel", "StaticR.rel");
if (!File.Exists(dolPath)) throw new FileNotFoundException("nodtool did not produce main.dol", dolPath);
if (!File.Exists(relPath)) throw new FileNotFoundException("nodtool did not produce StaticR.rel", relPath);
var dolSha = Sha256Of(dolPath);
var relSha = Sha256Of(relPath);
if (!string.Equals(dolSha, manifest.DolSha256, StringComparison.Ordinal))
{
await RunExtractAsync(nodTool, isoPath, scratch, cancellationToken);
var dolPath = Path.Combine(scratch, "sys", "main.dol");
var relPath = Path.Combine(scratch, "files", "rel", "StaticR.rel");
if (!File.Exists(dolPath)) throw new FileNotFoundException("nodtool did not produce main.dol", dolPath);
if (!File.Exists(relPath)) throw new FileNotFoundException("nodtool did not produce StaticR.rel", relPath);
var dolSha = Sha256Of(dolPath);
var relSha = Sha256Of(relPath);
if (!string.Equals(dolSha, manifest.DolSha256, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"main.dol sha256 mismatch: expected {manifest.DolSha256}, got {dolSha}. " +
"This disc revision does not match what this project's manifest is pinned to.");
}
if (!string.Equals(relSha, manifest.RelSha256, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"StaticR.rel sha256 mismatch: expected {manifest.RelSha256}, got {relSha}. " +
"This disc revision does not match what this project's manifest is pinned to.");
}
Directory.CreateDirectory(assetsDirectory);
File.Copy(dolPath, Path.Combine(assetsDirectory, "main.dol"), overwrite: true);
File.Copy(relPath, Path.Combine(assetsDirectory, "StaticR.rel"), overwrite: true);
reporter.Progress(InstallStages.ExtractDisc, "Disc validated and extracted", 6);
throw new InvalidOperationException(
$"main.dol sha256 mismatch: expected {manifest.DolSha256}, got {dolSha}. " +
"This disc revision does not match what this project's manifest is pinned to.");
}
finally
if (!string.Equals(relSha, manifest.RelSha256, StringComparison.Ordinal))
{
try { Directory.Delete(scratch, recursive: true); } catch { /* best-effort cleanup */ }
throw new InvalidOperationException(
$"StaticR.rel sha256 mismatch: expected {manifest.RelSha256}, got {relSha}. " +
"This disc revision does not match what this project's manifest is pinned to.");
}
Directory.CreateDirectory(assetsDirectory);
File.Copy(dolPath, Path.Combine(assetsDirectory, "main.dol"), overwrite: true);
File.Copy(relPath, Path.Combine(assetsDirectory, "StaticR.rel"), overwrite: true);
reporter.Progress(InstallStages.ExtractDisc, "Disc validated and extracted", 6);
}
private static async Task<string> RunInfoAsync(string nodTool, string isoPath, CancellationToken cancellationToken)
@@ -1,33 +0,0 @@
using System.Text.Json;
namespace WiiCompiled.Setup.Linux;
/// <summary>Reads and atomically writes the small JSON state documents this tool keeps.</summary>
internal static class JsonState
{
private static readonly JsonSerializerOptions ReadOptions = new() { PropertyNameCaseInsensitive = true };
private static readonly JsonSerializerOptions WriteOptions = new() { WriteIndented = true };
public static T? TryRead<T>(string path) where T : class
{
try
{
return File.Exists(path) ? JsonSerializer.Deserialize<T>(File.ReadAllText(path), ReadOptions) : null;
}
catch
{
// A truncated or hand-edited state document must degrade into "unknown", which every
// caller already treats as "assume stale and re-run install", not into a crash.
return null;
}
}
public static void Write<T>(string path, T value)
{
var directory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory);
var tempPath = path + ".tmp-" + Guid.NewGuid().ToString("N");
File.WriteAllText(tempPath, JsonSerializer.Serialize(value, WriteOptions));
File.Move(tempPath, path, overwrite: true);
}
}
+71 -17
View File
@@ -1,4 +1,5 @@
using System.Security.Cryptography;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Linux;
@@ -40,7 +41,7 @@ internal static class Program
await InstallAsync(flags, reporter, cts.Token);
break;
case "uninstall":
Uninstall(flags);
Uninstall();
break;
case "launch-base":
return Launch("base", flags);
@@ -73,9 +74,25 @@ internal static class Program
private static async Task InstallAsync(Dictionary<string, string?> flags, IInstallReporter reporter, CancellationToken token)
{
var profile = flags.GetValueOrDefault("profile") ?? "base";
if (profile is not ("base" or "retro-rewind" or "both"))
throw new ArgumentException("--profile must be base, retro-rewind, or both");
var retroDir = flags.GetValueOrDefault("retro-dir");
var installsRetro = !string.IsNullOrEmpty(retroDir);
var downloadPayload = flags.ContainsKey("download-retro-wfc-payload");
var skipPayload = flags.ContainsKey("skip-retro-wfc-payload");
if (installsRetro)
{
if (downloadPayload == skipPayload)
throw new ArgumentException(
"Choose exactly one Retro-WFC mode: --download-retro-wfc-payload or --skip-retro-wfc-payload.");
}
else if (downloadPayload || skipPayload)
{
throw new ArgumentException("A Retro-WFC payload option is valid only with --retro-dir.");
}
// Canonicalizes to the exact RetroRewind6 folder (accepting a parent folder or a symlink),
// the same validation Windows applies via this same shared method - local-build.sh's own
// 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 manifest = ProjectManifest.Load(Path.Combine(workspace, "projects", "mkwii", "recomp.yml"));
@@ -102,15 +119,35 @@ internal static class Program
var state = JsonState.TryRead<InstallState>(StatePath) ?? new InstallState { Workspace = workspace };
state.Workspace = workspace;
var profiles = profile == "both" ? new[] { "base", "retro-rewind" } : new[] { profile };
var baseInstallDir = profile == "both" ? DefaultInstallDir("base") : null;
var installDir = flags.GetValueOrDefault("install-dir") ?? DefaultInstallDir(profile == "both" ? "retro-rewind" : profile);
var profile = installsRetro ? "both" : "base";
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");
string? retroWfcOfflineDir = null;
if (downloadPayload)
{
// Reused if a previous install already downloaded and it's still valid - matches
// Windows's own reuse-if-valid behavior instead of re-downloading on every install.
var cacheDir = Path.Combine(workspace, "generated", "retro-wfc-payload");
reporter.Progress(InstallStages.Validate, "Preparing the Retro-WFC payload", 1);
try
{
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir);
}
catch (InvalidDataException)
{
await RetroWfcPayload.DownloadRetroWfcPayloadAsync(
RetroWfcPayload.CurrentRetroWfcPayloadUri, cacheDir, token);
}
retroWfcOfflineDir = cacheDir;
}
await BuildRunner.RunAsync(
workspace, profile, installDir, baseInstallDir,
flags.GetValueOrDefault("retro-rewind-package-dir"),
flags.GetValueOrDefault("retro-wfc-offline-dir"),
flags.ContainsKey("skip-retro-wfc-payload"),
retroDir,
retroWfcOfflineDir,
skipPayload,
flags.ContainsKey("force-clean-build"),
flags.GetValueOrDefault("translator-bin"),
reporter, token);
@@ -137,15 +174,32 @@ internal static class Program
DesktopEntry.Create(p, displayName, Path.Combine(dir, exeName));
}
JsonState.Write(StatePath, state);
// The runtime reads course/texture/audio data live from dvd_root at every launch, not just
// at translation time - without this the game fatally errors the instant it needs any file
// 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 dataDir = Path.Combine(assetsDir, "DATA");
if (Directory.Exists(dataDir))
{
RuntimeConfiguration.SetDvdRoot(configPath, dataDir);
}
if (installsRetro)
{
RuntimeConfiguration.SetRetroRewindRoot(configPath, retroDir!);
}
reporter.Progress(InstallStages.Shortcuts, "Install complete", 99);
}
private static void Uninstall(Dictionary<string, string?> flags)
private static void Uninstall()
{
var profile = flags.GetValueOrDefault("profile") ?? "all";
// Matches Windows: UninstallService.cs removes the whole install directory unconditionally -
// there is no partial-product uninstall on either platform.
var state = JsonState.TryRead<InstallState>(StatePath) ?? new InstallState();
var toRemove = profile == "all" ? state.Products.ToList() : state.Products.Where(r => r.Profile == profile).ToList();
foreach (var record in toRemove)
foreach (var record in state.Products.ToList())
{
if (Directory.Exists(record.InstallDirectory))
{
@@ -260,11 +314,11 @@ internal static class Program
Console.WriteLine("""
Usage: wiicompiled-setup <command> [options]
install --profile {base|retro-rewind|both} [--game ISO_PATH] [--install-dir DIR]
[--retro-rewind-package-dir DIR] [--retro-wfc-offline-dir DIR | --skip-retro-wfc-payload]
install [--game ISO_PATH] [--install-dir DIR] [--retro-dir DIR
{--download-retro-wfc-payload | --skip-retro-wfc-payload}]
[--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH]
[--progress-json] [--workspace DIR]
uninstall [--profile {base|retro-rewind|both|all}]
uninstall
launch-base
launch-retro
check-products
@@ -15,6 +15,6 @@
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WiiCompiled.NodTool\WiiCompiled.NodTool.csproj" />
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
</ItemGroup>
</Project>