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
+4 -4
View File
@@ -21,7 +21,7 @@ $dependencySources = [IO.Path]::GetFullPath((Join-Path $repoRoot $DependencySour
$workRoot = Join-Path $PSScriptRoot 'artifacts\installer-build'
$publish = Join-Path $workRoot 'publish'
$payloadRoot = Join-Path $workRoot 'payload'
$setupProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup\WiiCompiled.Setup.csproj'
$setupProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Windows\WiiCompiled.Setup.Windows.csproj'
$translatorProject = Join-Path $repoRoot 'translator\src\Translator.Cli\Translator.Cli.csproj'
$projectFile = Join-Path $repoRoot 'projects\mkwii\recomp.yml'
@@ -163,11 +163,11 @@ $translator = Join-Path $publish 'translator\Translator.Cli.exe'
Assert-File $setupHost 'Published setup host'
Assert-File $translator 'Self-contained translator'
# Resolved via the shared WiiCompiled.NodTool.Cli helper (also used by build-appimage.sh on Linux)
# rather than a separate download/version-pin copy here: it downloads and caches the same way
# Resolved via the shared WiiCompiled.Setup.Common.Cli helper (also used by build-appimage.sh on
# Linux) rather than a separate download/version-pin copy here: it downloads and caches the same way
# NodToolProvider.cs always does (Launcher/artifacts/nodtool.exe), replacing the old manual
# -DolphinToolPath handoff with an automated, pinned acquisition step.
$nodToolCliProject = Join-Path $PSScriptRoot 'WiiCompiled.NodTool.Cli'
$nodToolCliProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Common.Cli'
$nodTool = (& dotnet run --project $nodToolCliProject -c Release -- --workspace $repoRoot | Select-Object -Last 1)
if ($LASTEXITCODE -ne 0) { throw "nodtool resolution failed with exit code $LASTEXITCODE." }
Assert-File $nodTool 'Resolved nodtool'
+1 -1
View File
@@ -15,7 +15,7 @@ if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) {
}
$repoRoot = [IO.Path]::GetFullPath($RepositoryRoot)
$launcher = Join-Path $repoRoot 'Launcher'
$setup = Join-Path $launcher 'WiiCompiled.Setup'
$setup = Join-Path $launcher 'WiiCompiled.Setup.Windows'
$failures = [Collections.Generic.List[string]]::new()
function Add-Failure([string]$Message) { $failures.Add($Message) }
@@ -1,11 +1,11 @@
using WiiCompiled.NodTool;
using WiiCompiled.Setup.Common;
// A packaging-time-only helper - never shipped, never run by an end user. Both
// Launcher/build-appimage.sh and Launcher/Build-Installer.ps1 invoke this to obtain the nodtool
// binary they bundle, so there is exactly one place (NodToolProvider) that knows the pinned
// version/URL/platform-asset mapping, instead of a separate copy per packaging script.
//
// Usage: WiiCompiled.NodTool.Cli --workspace <repo-root>
// Usage: WiiCompiled.Setup.Common.Cli --workspace <repo-root>
// Prints the resolved nodtool path to stdout.
string? workspace = null;
@@ -19,7 +19,7 @@ for (var i = 0; i < args.Length; i++)
if (workspace is null)
{
Console.Error.WriteLine("Usage: WiiCompiled.NodTool.Cli --workspace <repo-root>");
Console.Error.WriteLine("Usage: WiiCompiled.Setup.Common.Cli --workspace <repo-root>");
return 1;
}
@@ -4,8 +4,8 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.NodTool.Cli</RootNamespace>
<AssemblyName>WiiCompiled.NodTool.Cli</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
<Version>0.2.22</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
@@ -14,6 +14,6 @@
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WiiCompiled.NodTool\WiiCompiled.NodTool.csproj" />
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
</ItemGroup>
</Project>
@@ -1,14 +1,14 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Common;
/// <summary>
/// One entry of an exact regular directory tree. Directory topology is part of the content
/// contract everywhere this walker is used: an empty directory can be a runtime-visible asset just
/// as a regular file can be, so it must not disappear from a content identity or a staged copy.
/// </summary>
internal sealed record RegularTreeEntry(string RelativePath, string FullPath, bool IsDirectory,
public sealed record RegularTreeEntry(string RelativePath, string FullPath, bool IsDirectory,
bool IsEmptyDirectory, long Length);
internal static class FileSystemUtilities
public static class FileSystemUtilities
{
public static void CopyDirectory(string source, string destination,
CancellationToken cancellationToken = default)
@@ -1,9 +1,9 @@
using System.Text.Json;
namespace WiiCompiled.Setup.Linux;
namespace WiiCompiled.Setup.Common;
/// <summary>Reads and atomically writes the small JSON state documents this tool keeps.</summary>
internal static class JsonState
/// <summary>Reads and atomically writes the small JSON state documents each installer keeps.</summary>
public static class JsonState
{
private static readonly JsonSerializerOptions ReadOptions = new() { PropertyNameCaseInsensitive = true };
private static readonly JsonSerializerOptions WriteOptions = new() { WriteIndented = true };
@@ -17,7 +17,7 @@ internal static class JsonState
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.
// caller already treats as "assume stale and rebuild", not into a crash.
return null;
}
}
@@ -1,6 +1,6 @@
using System.Text.RegularExpressions;
namespace WiiCompiled.NodTool;
namespace WiiCompiled.Setup.Common;
/// <summary>Disc metadata parsed from `nodtool info`'s stdout.</summary>
public sealed record NodToolDiscInfo(string GameId, string Title, int Revision);
@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
namespace WiiCompiled.NodTool;
namespace WiiCompiled.Setup.Common;
/// <summary>
/// Resolves the `nodtool` binary both installers use for Wii disc validation/extraction (see
@@ -9,7 +9,7 @@ namespace WiiCompiled.NodTool;
/// 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.NodTool.Cli (invoked once at packaging time by both
/// 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).
/// </summary>
public static class NodToolProvider
@@ -1,4 +1,4 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Common;
/// <summary>
/// A portable installation is a self-contained directory tree the user can move or carry on removable media:
@@ -9,8 +9,11 @@ namespace WiiCompiled.Setup;
/// </code>
/// The runtime finds the same root independently (<c>runtime/include/runtime_config.h</c>,
/// <c>PortableRootDirectory</c>); this class must keep the same marker name, layout, and depth bound.
/// Shared by both installers - Linux's CLI has no <c>--portable</c> flag, so it only ever calls
/// <see cref="TryFind"/>/<see cref="UserDataDirectory"/>/<see cref="Contains"/> (always missing,
/// since it never creates a marker file), not <see cref="Create"/>.
/// </summary>
internal static class PortableRoot
public static class PortableRoot
{
public const string MarkerFileName = "portable.txt";
public const string UserDataDirectoryName = "UserData";
@@ -72,7 +75,7 @@ internal static class PortableRoot
if (!File.Exists(marker))
{
File.WriteAllText(marker,
$"{ProductInfo.Name} portable installation." + Environment.NewLine +
"WiiCompiled portable installation." + Environment.NewLine +
"This marker makes the runtime keep Config.toml, NAND, Cache, and Logs in UserData\\ " +
"beside it instead of in %LOCALAPPDATA%." + Environment.NewLine +
"Delete it to make this installation use per-user application data again." +
@@ -90,54 +93,3 @@ internal static class PortableRoot
private static string Normalize(string path) => FileSystemUtilities.NormalizePath(path);
}
/// <summary>
/// A portable root can be moved or renamed between operations. Every installed-host operation that
/// reads <c>install-state.json</c> passes through here first so exactly one place decides what a
/// moved installation means, and so a non-portable installation is never touched.
/// </summary>
internal static class PortableInstallHealing
{
/// <summary>
/// Reconciles a moved portable installation with its recorded location: the state file adopts the
/// directory it was actually found in, and the native build tree is discarded because its
/// CMake cache holds absolute paths from the old location. Returns whether anything was healed.
/// </summary>
public static bool HealMovedInstall(Installation installation, IInstallReporter? reporter = null)
{
// Guard: an ordinary installation that disagrees with its state file is a real problem for
// the operation to report, not something to silently rewrite.
if (PortableRoot.TryFind(installation.Root) is null) return false;
var state = installation.ReadInstallState();
if (state is not { SchemaVersion: 1 } || string.IsNullOrWhiteSpace(state.InstallDir)) return false;
string recorded;
try
{
recorded = FileSystemUtilities.NormalizePath(state.InstallDir);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
{
recorded = state.InstallDir;
}
if (recorded.Equals(installation.Root, StringComparison.OrdinalIgnoreCase)) return false;
var previous = state.InstallDir;
state.InstallDir = installation.Root;
JsonState.Write(installation.InstallStatePath, state);
// The configured native build directory bakes absolute source, toolchain, and output paths
// into CMakeCache.txt. After a move it is unusable and would fail the next configure rather
// than being reused, so it is removed and reconfigured from scratch on the next build.
var nativeBuild = Path.Combine(installation.WorkspaceDirectory, "native-build");
var hadNativeBuild = Directory.Exists(nativeBuild);
if (hadNativeBuild) FileSystemUtilities.DeleteDirectoryIfExists(nativeBuild);
reporter?.Diagnostic(
$"This portable installation moved from {previous} to {installation.Root}. " +
"The recorded location was updated" +
(hadNativeBuild ? " and the location-bound native build cache was discarded." : "."));
return true;
}
}
@@ -1,16 +1,17 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Common;
/// <summary>
/// Resolves the one canonical Retro Rewind install. Wheel Wizard owns and passes it as
/// <c>--retro-dir</c>; the backend only resolves, reads, and records it, never packages or copies it.
/// <c>--retro-dir</c>; each installer only resolves, reads, and records it, never packages or
/// copies it.
/// </summary>
internal static class RetroRewindSource
public static class RetroRewindSource
{
/// <summary>
/// Resolves the <c>RetroRewind6</c> folder from a selection that may be the folder itself or a
/// parent containing exactly one <c>RetroRewind6/Binaries/Code.pul</c>.
/// </summary>
internal static string ResolveRetroRewind6(string selected)
public static string ResolveRetroRewind6(string selected)
{
if (string.IsNullOrWhiteSpace(selected))
throw new InvalidDataException("Choose the canonical Retro Rewind folder.");
@@ -1,21 +1,28 @@
using System.Diagnostics;
using System.Buffers.Binary;
using System.Net;
using System.Security.Cryptography;
using System.Text.Json;
using WiiCompiled.NodTool;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Common;
internal static class InputValidation
/// <summary>
/// One validated, content-identified download in operation-owned scratch space. Callers use this
/// exact directory for both the update decision and any resulting build.
/// </summary>
public sealed record RetroWfcPayloadSnapshot(string Directory, string Sha256, long ByteLength);
/// <summary>
/// Downloads and verifies the Retro-WFC payload (a small, RSA-signed blob served from a single
/// fixed endpoint). Shared by both installers - moved here from WiiCompiled.Setup.Windows's
/// InputValidation.cs, which keeps every one of these method names as thin forwarding wrappers so
/// its many existing call sites (ProductRepairService.cs, LocalBuildService.cs, Installation.cs,
/// SelfTests.cs) needed no changes.
/// </summary>
public static class RetroWfcPayload
{
private const long MaximumRetroWfcPayloadBytes = 16L * 1024 * 1024;
// The payload is tens of kilobytes from a single fixed endpoint 30s is good.
private static readonly TimeSpan RetroWfcDownloadTimeout = TimeSpan.FromSeconds(30);
private static readonly TimeSpan RetroWfcRetryDelay = TimeSpan.FromSeconds(1);
private static readonly HashSet<string> SupportedDiscImageExtensions = new(
[".iso", ".gcm", ".gcz", ".ciso", ".wbfs", ".wia", ".rvz"],
StringComparer.OrdinalIgnoreCase);
public const string CurrentRetroWfcPayloadUri = "http://nas.play.rwfc.net/payload?g=RMCPD00";
private static readonly string RetroWfcOfflinePayloadFile =
@@ -38,57 +45,6 @@ internal static class InputValidation
private const int RetroWfcPayloadSignatureOffset = 0x10;
private const int RetroWfcPayloadMinimumBytes = 0x130;
public static void ValidateExtension(string gamePath)
{
if (!File.Exists(gamePath))
throw new FileNotFoundException("The selected game image does not exist.", gamePath);
var extension = Path.GetExtension(gamePath);
if (!SupportedDiscImageExtensions.Contains(extension))
throw new InvalidDataException(
"Select a complete Wii disc image in ISO, GCM, GCZ, CISO, WBFS, WIA, or RVZ format.");
}
public static async Task<DiscHeader> ReadDiscHeaderAsync(string nodTool, string gamePath,
CancellationToken cancellationToken = default)
{
ValidateExtension(gamePath);
var result = await ProcessRunner.RunAsync(nodTool,
["info", Path.GetFullPath(gamePath)], null, cancellationToken);
if (result.ExitCode != 0)
throw new InvalidDataException("nodtool could not read this disc image. " + result.CombinedOutput.Trim());
var info = NodToolInfoParser.Parse(result.StandardOutput);
return new DiscHeader
{
GameId = info.GameId,
InternalName = info.Title,
Region = RegionFromGameId(info.GameId),
Revision = info.Revision,
};
}
private static string RegionFromGameId(string gameId) => gameId.Length >= 4
? gameId[3] switch
{
'P' => "PAL",
'E' => "NTSC-U",
'J' => "NTSC-J",
'K' => "Korea",
'W' => "Taiwan",
_ => gameId[3].ToString(),
}
: "Unknown";
public static void EnsureCompatibleDisc(DiscHeader header, PayloadManifest manifest)
{
if (!header.GameId.Equals(manifest.ExpectedGameId, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException(
$"This build supports Mario Kart Wii PAL ({manifest.ExpectedGameId}). " +
$"The selected image is {header.GameId} ({header.InternalName}, {header.Region}).");
}
}
public static string ValidateStagedRetroWfcPayloadDirectory(string stagedDirectory,
RSAParameters? signingKey = null)
{
@@ -201,7 +157,7 @@ internal static class InputValidation
}
}
internal static bool IsTransientRetroWfcDownloadFailure(Exception exception,
public static bool IsTransientRetroWfcDownloadFailure(Exception exception,
CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested) return false;
@@ -247,73 +203,9 @@ internal static class InputValidation
"The Retro-WFC payload is not signed by the pinned Retro-WFC signing key.");
}
public static string Sha256File(string path)
private static string Sha256File(string path)
{
using var stream = File.OpenRead(path);
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
}
}
internal sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError)
{
public string CombinedOutput => StandardOutput + Environment.NewLine + StandardError;
}
internal static class ProcessRunner
{
/// <summary>Runs a redirected child process to completion. <paramref name="configure"/> sets up a
/// working directory or scrubbed environment; <paramref name="capture"/> is off for callers that only
/// forward output live, so a build's output isn't buffered in memory for nobody to read.</summary>
public static async Task<ProcessResult> RunAsync(string executable, IReadOnlyList<string> arguments,
Action<string>? output, CancellationToken cancellationToken,
Action<ProcessStartInfo>? configure = null, bool capture = true,
Action<Exception>? onTerminationFailure = null)
{
var info = new ProcessStartInfo
{
FileName = executable,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
foreach (var argument in arguments) info.ArgumentList.Add(argument);
configure?.Invoke(info);
using var process = new Process { StartInfo = info, EnableRaisingEvents = true };
var stdout = new List<string>();
var stderr = new List<string>();
process.OutputDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stdout.Add(e.Data); output?.Invoke(e.Data); } };
process.ErrorDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stderr.Add(e.Data); output?.Invoke(e.Data); } };
if (!process.Start()) throw new InvalidOperationException($"Could not start {executable}.");
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await WaitForExitAsync(process, cancellationToken, onTerminationFailure);
return new ProcessResult(process.ExitCode, string.Join(Environment.NewLine, stdout),
string.Join(Environment.NewLine, stderr));
}
public static async Task WaitForExitAsync(Process process, CancellationToken cancellationToken,
Action<Exception>? onTerminationFailure = null)
{
try
{
await process.WaitForExitAsync(cancellationToken);
}
catch (OperationCanceledException)
{
try
{
if (!process.HasExited) process.Kill(entireProcessTree: true);
}
catch (Exception ex)
{
onTerminationFailure?.Invoke(ex);
}
await process.WaitForExitAsync(CancellationToken.None);
process.WaitForExit();
throw;
}
process.WaitForExit();
}
}
@@ -1,16 +1,19 @@
using System.Globalization;
using System.Text;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Common;
internal sealed record RuntimeConfigSnapshot(bool Existed, byte[] Contents);
public sealed record RuntimeConfigSnapshot(bool Existed, byte[] Contents);
/// <summary>
/// Reads and writes the runtime's <c>Config.toml</c>. Every entry point takes the file it operates on
/// since its location depends on the installation (portable <c>UserData</c> vs. per-user app data);
/// callers obtain it once via <see cref="ResolveConfigPath"/>.
/// callers obtain it once via <see cref="ResolveConfigPath"/>. Shared by both installers - Linux
/// never creates a <see cref="PortableRoot.MarkerFileName"/> marker file, so
/// <see cref="ResolveConfigPath"/>/<see cref="FormatPathValue"/>'s portable-root lookups always miss
/// there and this degrades to the same plain per-user-app-data, always-absolute-path behavior a
/// non-portable Windows install already gets.
/// </summary>
internal static class RuntimeConfiguration
public static class RuntimeConfiguration
{
public const string ConfigFileName = "Config.toml";
@@ -51,7 +54,7 @@ internal static class RuntimeConfiguration
File.Delete(configPath);
}
internal static void SetDvdRoot(string configPath, string dvdRoot) =>
public static void SetDvdRoot(string configPath, string dvdRoot) =>
SetPath(configPath, "dvd_root", dvdRoot);
/// <summary>
@@ -59,21 +62,21 @@ internal static class RuntimeConfiguration
/// asset overlay by scanning this directory live at launch, so an asset-only Retro Rewind update
/// needs no backend work: the next launch simply reads the new files.
/// </summary>
internal static void SetRetroRewindRoot(string configPath, string retroRewindRoot) =>
public static void SetRetroRewindRoot(string configPath, string retroRewindRoot) =>
SetPath(configPath, "retro_rewind_root", retroRewindRoot);
/// <summary>The canonical Retro Rewind root, or null when no installation has recorded one.</summary>
public static string? GetRetroRewindRoot(string configPath) =>
GetResolvedPath(configPath, "retro_rewind_root");
internal static void RemoveRetroRewindRootIfOwned(string configPath, string retroRewindRoot) =>
public static void RemoveRetroRewindRootIfOwned(string configPath, string retroRewindRoot) =>
RemovePathIfOwned(configPath, "retro_rewind_root", retroRewindRoot);
internal static void RemoveDvdRootIfOwned(string configPath, string dvdRoot) =>
public static void RemoveDvdRootIfOwned(string configPath, string dvdRoot) =>
RemovePathIfOwned(configPath, "dvd_root", dvdRoot);
/// <summary>The raw stored text of a <c>[paths]</c> key, exactly as the file holds it.</summary>
internal static string? GetPath(string configPath, string key) =>
public static string? GetPath(string configPath, string key) =>
TryUnquoteToml(GetRawValue(configPath, "paths", key) ?? "", out var value) ? value : null;
/// <summary>
@@ -81,17 +84,17 @@ internal static class RuntimeConfiguration
/// <c>[paths]</c> value against the directory holding <c>Config.toml</c> (never the working
/// directory), so the host must resolve it the same way before comparing or reading it.
/// </summary>
internal static string? GetResolvedPath(string configPath, string key)
public static string? GetResolvedPath(string configPath, string key)
{
var stored = GetPath(configPath, key);
return string.IsNullOrWhiteSpace(stored) ? null : ResolveAgainstConfig(configPath, stored);
}
internal static string ConfigDirectory(string configPath) =>
public static string ConfigDirectory(string configPath) =>
Path.GetDirectoryName(Path.GetFullPath(configPath))
?? throw new InvalidOperationException($"{configPath} has no containing directory.");
internal static string ResolveAgainstConfig(string configPath, string value) =>
public static string ResolveAgainstConfig(string configPath, string value) =>
Path.GetFullPath(value, ConfigDirectory(configPath));
private static void SetPath(string configPath, string key, string value)
@@ -150,7 +153,7 @@ internal static class RuntimeConfiguration
}
/// <summary>The raw TOML literal stored for a key, or null when the section or key is absent.</summary>
internal static string? GetRawValue(string configPath, string section, string key)
public static string? GetRawValue(string configPath, string section, string key)
{
if (!File.Exists(configPath)) return null;
var header = $"[{section}]";
@@ -265,7 +268,7 @@ internal static class RuntimeConfiguration
private static string QuoteToml(string value) =>
"\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
internal static bool TryUnquoteToml(string value, out string result)
public static bool TryUnquoteToml(string value, out string result)
{
result = "";
if (value.Length < 2) return false;
@@ -292,5 +295,4 @@ internal static class RuntimeConfiguration
result = builder.ToString();
return true;
}
}
@@ -3,12 +3,12 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.NodTool</RootNamespace>
<AssemblyName>WiiCompiled.NodTool</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
<Version>0.2.22</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Shared nodtool acquisition/parsing logic used by both the Windows and Linux installers</Description>
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
<DebugType>embedded</DebugType>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
@@ -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)
+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>
@@ -1,4 +1,4 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Bridges a frontend-owned, named Windows event into the cancellation token used by setup.
@@ -1,4 +1,4 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal enum AppMode
{
@@ -1,8 +1,9 @@
using System.Buffers.Binary;
using System.Security.Cryptography;
using System.Text;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal sealed record RetroRewindCompileInputs(
string RetroRewindRoot,
@@ -1,6 +1,7 @@
using System.Text.Json;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal static class ConsoleCommands
{
@@ -1,6 +1,6 @@
using System.Diagnostics;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal static class GameLaunchService
{
@@ -0,0 +1,165 @@
using System.Diagnostics;
using System.Buffers.Binary;
using System.Net;
using System.Security.Cryptography;
using System.Text.Json;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
internal static class InputValidation
{
private static readonly HashSet<string> SupportedDiscImageExtensions = new(
[".iso", ".gcm", ".gcz", ".ciso", ".wbfs", ".wia", ".rvz"],
StringComparer.OrdinalIgnoreCase);
public static void ValidateExtension(string gamePath)
{
if (!File.Exists(gamePath))
throw new FileNotFoundException("The selected game image does not exist.", gamePath);
var extension = Path.GetExtension(gamePath);
if (!SupportedDiscImageExtensions.Contains(extension))
throw new InvalidDataException(
"Select a complete Wii disc image in ISO, GCM, GCZ, CISO, WBFS, WIA, or RVZ format.");
}
public static async Task<DiscHeader> ReadDiscHeaderAsync(string nodTool, string gamePath,
CancellationToken cancellationToken = default)
{
ValidateExtension(gamePath);
var result = await ProcessRunner.RunAsync(nodTool,
["info", Path.GetFullPath(gamePath)], null, cancellationToken);
if (result.ExitCode != 0)
throw new InvalidDataException("nodtool could not read this disc image. " + result.CombinedOutput.Trim());
var info = NodToolInfoParser.Parse(result.StandardOutput);
return new DiscHeader
{
GameId = info.GameId,
InternalName = info.Title,
Region = RegionFromGameId(info.GameId),
Revision = info.Revision,
};
}
private static string RegionFromGameId(string gameId) => gameId.Length >= 4
? gameId[3] switch
{
'P' => "PAL",
'E' => "NTSC-U",
'J' => "NTSC-J",
'K' => "Korea",
'W' => "Taiwan",
_ => gameId[3].ToString(),
}
: "Unknown";
public static void EnsureCompatibleDisc(DiscHeader header, PayloadManifest manifest)
{
if (!header.GameId.Equals(manifest.ExpectedGameId, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException(
$"This build supports Mario Kart Wii PAL ({manifest.ExpectedGameId}). " +
$"The selected image is {header.GameId} ({header.InternalName}, {header.Region}).");
}
}
// Thin forwarding wrappers: the actual download/RSA-verification logic lives in
// WiiCompiled.Setup.Common.RetroWfcPayload (shared with WiiCompiled.Setup.Linux) so there's one
// copy of it, not two. Kept under these names so every existing call site here
// (ProductRepairService.cs, LocalBuildService.cs, Installation.cs, SelfTests.cs) is unchanged.
public const string CurrentRetroWfcPayloadUri = RetroWfcPayload.CurrentRetroWfcPayloadUri;
public static string ValidateStagedRetroWfcPayloadDirectory(string stagedDirectory,
RSAParameters? signingKey = null) =>
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(stagedDirectory, signingKey);
public static string ResolveRetroWfcPayloadFile(string stagedDirectory,
RSAParameters? signingKey = null) =>
RetroWfcPayload.ResolveRetroWfcPayloadFile(stagedDirectory, signingKey);
public static string ComputeRetroWfcPayloadSha256(string stagedDirectory,
RSAParameters? signingKey = null) =>
RetroWfcPayload.ComputeRetroWfcPayloadSha256(stagedDirectory, signingKey);
public static void ValidateRetroWfcPayloadUri(string uriText) =>
RetroWfcPayload.ValidateRetroWfcPayloadUri(uriText);
public static Task<RetroWfcPayloadSnapshot> DownloadRetroWfcPayloadAsync(string uriText,
string destinationDirectory, CancellationToken cancellationToken) =>
RetroWfcPayload.DownloadRetroWfcPayloadAsync(uriText, destinationDirectory, cancellationToken);
internal static bool IsTransientRetroWfcDownloadFailure(Exception exception,
CancellationToken cancellationToken) =>
RetroWfcPayload.IsTransientRetroWfcDownloadFailure(exception, cancellationToken);
public static string Sha256File(string path)
{
using var stream = File.OpenRead(path);
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
}
}
internal sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError)
{
public string CombinedOutput => StandardOutput + Environment.NewLine + StandardError;
}
internal static class ProcessRunner
{
/// <summary>Runs a redirected child process to completion. <paramref name="configure"/> sets up a
/// working directory or scrubbed environment; <paramref name="capture"/> is off for callers that only
/// forward output live, so a build's output isn't buffered in memory for nobody to read.</summary>
public static async Task<ProcessResult> RunAsync(string executable, IReadOnlyList<string> arguments,
Action<string>? output, CancellationToken cancellationToken,
Action<ProcessStartInfo>? configure = null, bool capture = true,
Action<Exception>? onTerminationFailure = null)
{
var info = new ProcessStartInfo
{
FileName = executable,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
foreach (var argument in arguments) info.ArgumentList.Add(argument);
configure?.Invoke(info);
using var process = new Process { StartInfo = info, EnableRaisingEvents = true };
var stdout = new List<string>();
var stderr = new List<string>();
process.OutputDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stdout.Add(e.Data); output?.Invoke(e.Data); } };
process.ErrorDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stderr.Add(e.Data); output?.Invoke(e.Data); } };
if (!process.Start()) throw new InvalidOperationException($"Could not start {executable}.");
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await WaitForExitAsync(process, cancellationToken, onTerminationFailure);
return new ProcessResult(process.ExitCode, string.Join(Environment.NewLine, stdout),
string.Join(Environment.NewLine, stderr));
}
public static async Task WaitForExitAsync(Process process, CancellationToken cancellationToken,
Action<Exception>? onTerminationFailure = null)
{
try
{
await process.WaitForExitAsync(cancellationToken);
}
catch (OperationCanceledException)
{
try
{
if (!process.HasExited) process.Kill(entireProcessTree: true);
}
catch (Exception ex)
{
onTerminationFailure?.Invoke(ex);
}
await process.WaitForExitAsync(CancellationToken.None);
process.WaitForExit();
throw;
}
process.WaitForExit();
}
}
@@ -1,7 +1,8 @@
using System.Security.Cryptography;
using System.Text;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// A fail-fast, cross-process lock covering install, repair and launch operations for one install
@@ -1,6 +1,6 @@
using System.Text.Json;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Stable stage identifiers reported by <c>--progress-json</c>. These are part of the public
@@ -1,4 +1,6 @@
namespace WiiCompiled.Setup;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Owns one temporary directory for an install operation. The name carries the installation's scope
@@ -1,4 +1,6 @@
namespace WiiCompiled.Setup;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
internal enum InstallTransactionEntryKind
{
@@ -1,4 +1,6 @@
namespace WiiCompiled.Setup;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
/// <summary>Provenance written by the bundled build script next to every product it produces.</summary>
internal sealed class LocalBuildProvenance
@@ -1,4 +1,4 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Names of the installed/staged layout. Not cosmetic: payload and toolkit identities hash relative paths
@@ -1,4 +1,6 @@
namespace WiiCompiled.Setup;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
internal sealed class InstallerEngine
{
@@ -1,6 +1,7 @@
using System.Diagnostics;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// <see cref="Both"/> runs one retro-aware translation and compiles the two products from a single
@@ -1,6 +1,7 @@
using System.Text.Json.Serialization;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal enum RetroWfcPayloadMode
{
@@ -54,12 +55,6 @@ internal sealed class PayloadManifest
public string NativeToolchainFingerprint { get; set; } = "";
}
/// <summary>
/// One validated, content-identified download in operation-owned scratch space. Callers use this
/// exact directory for both the update decision and any resulting build.
/// </summary>
internal sealed record RetroWfcPayloadSnapshot(string Directory, string Sha256, long ByteLength);
internal sealed class DiscHeader
{
[JsonPropertyName("game_id")]
@@ -2,7 +2,7 @@ using System.IO.Compression;
using System.Text;
using System.Text.Json;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal sealed class PayloadArchive : IDisposable
{
@@ -0,0 +1,54 @@
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// A portable root can be moved or renamed between operations. Every installed-host operation that
/// reads <c>install-state.json</c> passes through here first so exactly one place decides what a
/// moved installation means, and so a non-portable installation is never touched.
/// </summary>
internal static class PortableInstallHealing
{
/// <summary>
/// Reconciles a moved portable installation with its recorded location: the state file adopts the
/// directory it was actually found in, and the native build tree is discarded because its
/// CMake cache holds absolute paths from the old location. Returns whether anything was healed.
/// </summary>
public static bool HealMovedInstall(Installation installation, IInstallReporter? reporter = null)
{
// Guard: an ordinary installation that disagrees with its state file is a real problem for
// the operation to report, not something to silently rewrite.
if (PortableRoot.TryFind(installation.Root) is null) return false;
var state = installation.ReadInstallState();
if (state is not { SchemaVersion: 1 } || string.IsNullOrWhiteSpace(state.InstallDir)) return false;
string recorded;
try
{
recorded = FileSystemUtilities.NormalizePath(state.InstallDir);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
{
recorded = state.InstallDir;
}
if (recorded.Equals(installation.Root, StringComparison.OrdinalIgnoreCase)) return false;
var previous = state.InstallDir;
state.InstallDir = installation.Root;
JsonState.Write(installation.InstallStatePath, state);
// The configured native build directory bakes absolute source, toolchain, and output paths
// into CMakeCache.txt. After a move it is unusable and would fail the next configure rather
// than being reused, so it is removed and reconfigured from scratch on the next build.
var nativeBuild = Path.Combine(installation.WorkspaceDirectory, "native-build");
var hadNativeBuild = Directory.Exists(nativeBuild);
if (hadNativeBuild) FileSystemUtilities.DeleteDirectoryIfExists(nativeBuild);
reporter?.Diagnostic(
$"This portable installation moved from {previous} to {installation.Root}. " +
"The recorded location was updated" +
(hadNativeBuild ? " and the location-bound native build cache was discarded." : "."));
return true;
}
}
@@ -1,4 +1,6 @@
namespace WiiCompiled.Setup;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Reconciles installed products against the canonical Retro Rewind install Wheel Wizard owns: the
@@ -1,4 +1,4 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
using System.Runtime.InteropServices;
@@ -1,6 +1,7 @@
using System.Diagnostics;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Refuses to replace installed products while one of them is running: publishing renames the
@@ -1,4 +1,6 @@
namespace WiiCompiled.Setup;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// The one path by which a product receives its copied runtime assets, shared by install and repair.
@@ -1,7 +1,8 @@
using System.Buffers.Binary;
using System.Security.Cryptography;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal static class SelfTests
{
@@ -1,6 +1,6 @@
using Microsoft.Win32;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal static class ShellIntegration
{
@@ -1,7 +1,8 @@
using System.Security.Cryptography;
using System.Text;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Content identity of everything that decides what the locally produced executables contain.
@@ -1,7 +1,8 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal static class UninstallService
{
@@ -5,7 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup</RootNamespace>
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.24</Version>
<Authors>patchzy</Authors>
@@ -15,6 +15,6 @@
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WiiCompiled.NodTool\WiiCompiled.NodTool.csproj" />
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
</ItemGroup>
</Project>
@@ -1,4 +1,4 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal static class WorkspaceTimestamps
-27
View File
@@ -1,27 +0,0 @@
using System.Text.Json;
namespace WiiCompiled.Setup;
/// <summary>Reads and atomically writes the small JSON state documents kept inside an installation.</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 rebuild", not into a failed launch.
return null;
}
}
public static void Write<T>(string path, T value) =>
FileSystemUtilities.WriteAtomic(path, JsonSerializer.Serialize(value, WriteOptions));
}
+2 -2
View File
@@ -61,12 +61,12 @@ dotnet publish "$workspace/translator/src/Translator.Cli" -c Release -r linux-x6
cp "$translator_publish_tmp/Translator.Cli" "$appdir/usr/bin/translator-cli"
chmod +x "$appdir/usr/bin/translator-cli"
# Resolved via the shared WiiCompiled.NodTool.Cli helper (also used by Build-Installer.ps1 on
# Resolved via the shared WiiCompiled.Setup.Common.Cli helper (also used by Build-Installer.ps1 on
# Windows) rather than a second curl/version-pin copy here: it downloads and caches the same way
# NodToolProvider.cs always does (Launcher/artifacts/nodtool), so there is exactly one place that
# knows the nodtool version/URL/platform-asset mapping.
echo "Resolving nodtool..."
nodtool_path=$(dotnet run --project "$workspace/Launcher/WiiCompiled.NodTool.Cli" -c Release -- \
nodtool_path=$(dotnet run --project "$workspace/Launcher/WiiCompiled.Setup.Common.Cli" -c Release -- \
--workspace "$workspace" | tail -n1)
cp "$nodtool_path" "$appdir/usr/bin/nodtool"
chmod +x "$appdir/usr/bin/nodtool"
-2
View File
@@ -52,7 +52,6 @@ output_dir=""
base_output_dir=""
retro_rewind_package_dir=""
retro_wfc_offline_dir=""
retro_wfc_payload_origin=offline
skip_retro_wfc_payload=0
force_clean_build=0
parallel_override=0
@@ -93,7 +92,6 @@ while [[ $# -gt 0 ]]; do
--base-output-dir) base_output_dir=$2; shift 2 ;;
--retro-rewind-package-dir) retro_rewind_package_dir=$2; shift 2 ;;
--retro-wfc-offline-dir) retro_wfc_offline_dir=$2; shift 2 ;;
--retro-wfc-payload-origin) retro_wfc_payload_origin=$2; shift 2 ;;
--skip-retro-wfc-payload) skip_retro_wfc_payload=1; shift ;;
--force-clean-build) force_clean_build=1; shift ;;
--parallel) parallel_override=$2; shift 2 ;;