From f09590aa5d43985176fb4a4c9b384d4f65298068 Mon Sep 17 00:00:00 2001 From: theofficialgman <28281419+theofficialgman@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:55:47 -0400 Subject: [PATCH] switch to standalone nod tool rather than dolphin-tool --- Launcher/Build-Installer.ps1 | 51 ++++++++---- Launcher/Prepare-Release.ps1 | 6 -- Launcher/WiiCompiled.NodTool.Cli/Program.cs | 28 +++++++ .../WiiCompiled.NodTool.Cli.csproj | 19 +++++ .../WiiCompiled.NodTool/NodToolInfoParser.cs | 38 +++++++++ .../WiiCompiled.NodTool/NodToolProvider.cs | 67 +++++++++++++++ .../WiiCompiled.NodTool.csproj | 15 ++++ Launcher/WiiCompiled.Setup.Linux/DiscTool.cs | 83 ++++++++++++------- Launcher/WiiCompiled.Setup.Linux/Program.cs | 6 +- .../WiiCompiled.Setup.Linux.csproj | 3 + Launcher/WiiCompiled.Setup/ConsoleCommands.cs | 4 +- Launcher/WiiCompiled.Setup/InputValidation.cs | 35 +++++--- Launcher/WiiCompiled.Setup/InstallerEngine.cs | 27 +++--- Launcher/WiiCompiled.Setup/SelfTests.cs | 14 ++-- .../WiiCompiled.Setup/ToolkitFingerprint.cs | 4 +- .../WiiCompiled.Setup.csproj | 3 + Launcher/build-appimage.sh | 26 ++++-- THIRD-PARTY-NOTICES.md | 12 +-- 18 files changed, 346 insertions(+), 95 deletions(-) create mode 100644 Launcher/WiiCompiled.NodTool.Cli/Program.cs create mode 100644 Launcher/WiiCompiled.NodTool.Cli/WiiCompiled.NodTool.Cli.csproj create mode 100644 Launcher/WiiCompiled.NodTool/NodToolInfoParser.cs create mode 100644 Launcher/WiiCompiled.NodTool/NodToolProvider.cs create mode 100644 Launcher/WiiCompiled.NodTool/WiiCompiled.NodTool.csproj diff --git a/Launcher/Build-Installer.ps1 b/Launcher/Build-Installer.ps1 index c208ce7..d3d033c 100644 --- a/Launcher/Build-Installer.ps1 +++ b/Launcher/Build-Installer.ps1 @@ -1,7 +1,6 @@ [CmdletBinding(PositionalBinding = $false)] param( [string]$OutputDirectory = 'Launcher/dist', - [string]$DolphinToolPath, [string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools', [string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies', [string]$VcRuntimeDirectory, @@ -15,10 +14,6 @@ Set-StrictMode -Version 3.0 # helpers shared with LocalBuild.ps1 and Prepare-NativePrebuilt.ps1. . (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1') -if ([string]::IsNullOrWhiteSpace($DolphinToolPath)) { - throw 'Build-Installer.ps1 requires -DolphinToolPath pointing to DolphinTool.exe.' -} - $repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) $outputRoot = [IO.Path]::GetFullPath((Join-Path $repoRoot $OutputDirectory)) $portableTools = [IO.Path]::GetFullPath((Join-Path $repoRoot $PortableToolsDirectory)) @@ -91,7 +86,6 @@ function Compress-Zip([string]$Source, [string]$Destination, [string[]]$Entries) Assert-File $setupProject '.NET setup project' Assert-File $translatorProject 'Translator CLI project' -Assert-File $DolphinToolPath 'DolphinTool' if (-not (Test-Path -LiteralPath (Join-Path $portableTools 'llvm-mingw\bin\x86_64-w64-mingw32-clang++.exe'))) { & (Join-Path $PSScriptRoot 'Prepare-PortableTools.ps1') -Destination $portableTools @@ -169,6 +163,15 @@ $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 +# 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' +$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' + Write-Host '[2/6] Staging the explicit, game-code-free payload allowlist...' # The staged layout mirrors the installed layout exactly (Toolkit, BuildWorkspace): payload # identities hash relative paths, so the names here are part of the fingerprint contract. @@ -191,7 +194,7 @@ Get-ChildItem -LiteralPath (Join-Path $toolkit 'llvm-mingw\bin') -File | Remove-Item -Force [IO.Directory]::CreateDirectory((Join-Path $toolkit 'Translator')) | Out-Null Copy-Item -LiteralPath $translator -Destination (Join-Path $toolkit 'Translator\Translator.Cli.exe') -Copy-Item -LiteralPath $DolphinToolPath -Destination (Join-Path $toolkit 'DolphinTool.exe') +Copy-Item -LiteralPath $nodTool -Destination (Join-Path $toolkit 'nodtool.exe') [IO.Directory]::CreateDirectory((Join-Path $toolkit 'Redist')) | Out-Null Copy-Item -Path (Join-Path $vcRuntime '*.dll') -Destination (Join-Path $toolkit 'Redist') Copy-Item -Path (Join-Path $vcRuntime '*.dll') -Destination (Join-Path $toolkit 'CMake\bin') @@ -225,18 +228,38 @@ Copy-Item (Join-Path $dependencySources 'cppwinrt\LICENSE.txt') (Join-Path $payl # The precompiled aurora/third-party archives are built from the very sources # already shipped under build-workspace\Dependencies and aurora-main, so they add # no third-party component and therefore no new license obligation. -$dolphinLicense = Join-Path (Split-Path -Parent $DolphinToolPath) 'COPYING' -if (Test-Path $dolphinLicense) { Copy-Item $dolphinLicense (Join-Path $payloadRoot 'licenses\Dolphin-COPYING.txt') } @" -DolphinTool source offer +nodtool (disc image extraction) -Project and complete corresponding source: https://github.com/dolphin-emu/dolphin -Dolphin is licensed under GPLv2+ with additional per-file SPDX licenses. -"@ | Set-Content (Join-Path $payloadRoot 'licenses\Dolphin-SOURCE.txt') -Encoding UTF8 +Project: https://github.com/encounter/nod +Dual-licensed under MIT OR Apache-2.0. + +MIT License + +Copyright 2021 Luke Street. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +"@ | Set-Content (Join-Path $payloadRoot 'licenses\nodtool-LICENSE-MIT.txt') -Encoding UTF8 @" Microsoft Visual C++ Runtime -Redistributable x64 runtime DLLs are included app-locally for DolphinTool and third-party renderer DLLs. +Redistributable x64 runtime DLLs are included app-locally for nodtool and third-party renderer DLLs. Microsoft license terms: https://visualstudio.microsoft.com/license-terms/ "@ | Set-Content (Join-Path $payloadRoot 'licenses\Microsoft-VC-Runtime.txt') -Encoding UTF8 # Compute the payload's content identities once, here, with the same code every installed host diff --git a/Launcher/Prepare-Release.ps1 b/Launcher/Prepare-Release.ps1 index 8e51414..53a8df0 100644 --- a/Launcher/Prepare-Release.ps1 +++ b/Launcher/Prepare-Release.ps1 @@ -1,6 +1,5 @@ [CmdletBinding(PositionalBinding = $false)] param( - [string]$DolphinToolPath, [string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools', [string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies', [string]$VcRuntimeDirectory, @@ -10,12 +9,7 @@ param( $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3.0 -if ([string]::IsNullOrWhiteSpace($DolphinToolPath)) { - throw 'Prepare-Release.ps1 requires -DolphinToolPath pointing to DolphinTool.exe.' -} - $arguments = @{ - DolphinToolPath = $DolphinToolPath PortableToolsDirectory = $PortableToolsDirectory DependencySourceDirectory = $DependencySourceDirectory ToolkitReleaseTag = $ToolkitReleaseTag diff --git a/Launcher/WiiCompiled.NodTool.Cli/Program.cs b/Launcher/WiiCompiled.NodTool.Cli/Program.cs new file mode 100644 index 0000000..e5315d1 --- /dev/null +++ b/Launcher/WiiCompiled.NodTool.Cli/Program.cs @@ -0,0 +1,28 @@ +using WiiCompiled.NodTool; + +// 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 +// Prints the resolved nodtool path to stdout. + +string? workspace = null; +for (var i = 0; i < args.Length; i++) +{ + if (args[i] == "--workspace" && i + 1 < args.Length) + { + workspace = args[++i]; + } +} + +if (workspace is null) +{ + Console.Error.WriteLine("Usage: WiiCompiled.NodTool.Cli --workspace "); + return 1; +} + +var path = await NodToolProvider.ResolveAsync(workspace, CancellationToken.None); +Console.WriteLine(path); +return 0; diff --git a/Launcher/WiiCompiled.NodTool.Cli/WiiCompiled.NodTool.Cli.csproj b/Launcher/WiiCompiled.NodTool.Cli/WiiCompiled.NodTool.Cli.csproj new file mode 100644 index 0000000..0f7e534 --- /dev/null +++ b/Launcher/WiiCompiled.NodTool.Cli/WiiCompiled.NodTool.Cli.csproj @@ -0,0 +1,19 @@ + + + Exe + net8.0 + enable + enable + WiiCompiled.NodTool.Cli + WiiCompiled.NodTool.Cli + 0.2.22 + patchzy + WiiCompiled + Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1 + embedded + en + + + + + diff --git a/Launcher/WiiCompiled.NodTool/NodToolInfoParser.cs b/Launcher/WiiCompiled.NodTool/NodToolInfoParser.cs new file mode 100644 index 0000000..0c8a8cd --- /dev/null +++ b/Launcher/WiiCompiled.NodTool/NodToolInfoParser.cs @@ -0,0 +1,38 @@ +using System.Text.RegularExpressions; + +namespace WiiCompiled.NodTool; + +/// Disc metadata parsed from `nodtool info`'s stdout. +public sealed record NodToolDiscInfo(string GameId, string Title, int Revision); + +/// +/// Parses the plain-text stdout of `nodtool info <iso>`. nodtool has no JSON output mode, but +/// prints one unconditional disc-level Title/Game ID/Disc-Revision block (via its own +/// `print_header`) before any per-partition breakdown - Wii discs also have differently-scoped +/// "Title"/"Game ID" lines per update/channel partition further down, so the first match of each +/// pattern is always the disc-level one both installers want. +/// +public static partial class NodToolInfoParser +{ + public static NodToolDiscInfo Parse(string infoStdout) + { + var gameIdMatch = GameIdLine().Match(infoStdout); + if (!gameIdMatch.Success) + throw new InvalidOperationException("nodtool did not return disc metadata."); + var titleMatch = TitleLine().Match(infoStdout); + var revisionMatch = RevisionLine().Match(infoStdout); + return new NodToolDiscInfo( + GameId: gameIdMatch.Groups[1].Value, + Title: titleMatch.Success ? titleMatch.Groups[1].Value : "", + Revision: revisionMatch.Success ? int.Parse(revisionMatch.Groups[1].Value) : 0); + } + + [GeneratedRegex(@"^Game ID: (\S+)", RegexOptions.Multiline)] + private static partial Regex GameIdLine(); + + [GeneratedRegex(@"^Title: (.+)$", RegexOptions.Multiline)] + private static partial Regex TitleLine(); + + [GeneratedRegex(@"^Disc \d+, Revision (\d+)", RegexOptions.Multiline)] + private static partial Regex RevisionLine(); +} diff --git a/Launcher/WiiCompiled.NodTool/NodToolProvider.cs b/Launcher/WiiCompiled.NodTool/NodToolProvider.cs new file mode 100644 index 0000000..2ab6f93 --- /dev/null +++ b/Launcher/WiiCompiled.NodTool/NodToolProvider.cs @@ -0,0 +1,67 @@ +using System.Runtime.InteropServices; + +namespace WiiCompiled.NodTool; + +/// +/// Resolves the `nodtool` binary both installers use for Wii disc validation/extraction (see +/// NodToolInfoParser.cs), replacing the earlier dependency on `dolphin-tool`/`DolphinTool.exe`. A +/// 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.NodTool.Cli (invoked once at packaging time by both +/// build-appimage.sh and Build-Installer.ps1 to acquire the copy each bundles). +/// +public static class NodToolProvider +{ + public const string Version = "v2.0.0-alpha.10"; + + public static async Task ResolveAsync(string workspace, CancellationToken cancellationToken) + { + var cacheName = OperatingSystem.IsWindows() ? "nodtool.exe" : "nodtool"; + var cachePath = Path.Combine(workspace, "Launcher", "artifacts", cacheName); + if (File.Exists(cachePath)) return cachePath; + + var url = $"https://github.com/encounter/nod/releases/download/{Version}/{AssetName()}"; + + Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!); + var tempPath = cachePath + ".tmp"; + using (var http = new HttpClient()) + using (var response = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken)) + { + response.EnsureSuccessStatusCode(); + await using var fileStream = File.Create(tempPath); + await response.Content.CopyToAsync(fileStream, cancellationToken); + } + File.Move(tempPath, cachePath, overwrite: true); + if (!OperatingSystem.IsWindows()) + { + File.SetUnixFileMode(cachePath, + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.GroupRead | UnixFileMode.GroupExecute | + UnixFileMode.OtherRead | UnixFileMode.OtherExecute); + } + return cachePath; + } + + private static string AssetName() + { + if (OperatingSystem.IsWindows()) + { + return RuntimeInformation.OSArchitecture switch + { + Architecture.X64 => "nodtool-windows-x86_64.exe", + Architecture.Arm64 => "nodtool-windows-arm64.exe", + Architecture.X86 => "nodtool-windows-x86.exe", + var other => throw new PlatformNotSupportedException($"No prebuilt nodtool release for Windows {other}"), + }; + } + return RuntimeInformation.OSArchitecture switch + { + Architecture.X64 => "nodtool-linux-x86_64", + Architecture.Arm64 => "nodtool-linux-aarch64", + Architecture.X86 => "nodtool-linux-i686", + var other => throw new PlatformNotSupportedException($"No prebuilt nodtool release for Linux {other}"), + }; + } +} diff --git a/Launcher/WiiCompiled.NodTool/WiiCompiled.NodTool.csproj b/Launcher/WiiCompiled.NodTool/WiiCompiled.NodTool.csproj new file mode 100644 index 0000000..967ee2c --- /dev/null +++ b/Launcher/WiiCompiled.NodTool/WiiCompiled.NodTool.csproj @@ -0,0 +1,15 @@ + + + net8.0 + enable + enable + WiiCompiled.NodTool + WiiCompiled.NodTool + 0.2.22 + patchzy + WiiCompiled + Shared nodtool acquisition/parsing logic used by both the Windows and Linux installers + embedded + en + + diff --git a/Launcher/WiiCompiled.Setup.Linux/DiscTool.cs b/Launcher/WiiCompiled.Setup.Linux/DiscTool.cs index d130fda..8c88296 100644 --- a/Launcher/WiiCompiled.Setup.Linux/DiscTool.cs +++ b/Launcher/WiiCompiled.Setup.Linux/DiscTool.cs @@ -1,28 +1,32 @@ using System.Security.Cryptography; -using System.Text.Json; +using WiiCompiled.NodTool; namespace WiiCompiled.Setup.Linux; /// -/// Validates and extracts the user's own Mario Kart Wii disc via the system-installed -/// `dolphin-tool` (required prerequisite, per the project's scoping decision to depend on system -/// tools rather than bundling one) - the Linux analogue of the Windows installer shelling out to a -/// bundled DolphinTool.exe. +/// 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 +/// 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). /// internal static class DiscTool { public static async Task ValidateAndExtractAsync( - string isoPath, ProjectManifest manifest, string assetsDirectory, - IInstallReporter reporter, CancellationToken cancellationToken) + string isoPath, ProjectManifest manifest, string assetsDirectory, string workspace, + string? nodToolBin, IInstallReporter reporter, CancellationToken cancellationToken) { + var nodTool = nodToolBin ?? await NodToolProvider.ResolveAsync(workspace, cancellationToken); + + // `nodtool info` only decodes the disc/partition headers (milliseconds); `nodtool extract` + // copies the whole data partition to disk (tens of seconds for a custom-track-heavy MKWii + // ISO). Checking the game ID first, before extracting, means a wrong disc fails fast - + // matching the original dolphin-tool `header` step this replaces. reporter.Progress(InstallStages.ExtractDisc, "Reading the disc header", 2); - var headerJson = await RunAsync(["header", "-i", isoPath, "-j"], cancellationToken); - using var header = JsonDocument.Parse(headerJson); - var gameId = header.RootElement.GetProperty("game_id").GetString(); - if (!string.Equals(gameId, manifest.GameId, StringComparison.Ordinal)) + var info = NodToolInfoParser.Parse(await RunInfoAsync(nodTool, isoPath, cancellationToken)); + if (!string.Equals(info.GameId, manifest.GameId, StringComparison.Ordinal)) { throw new InvalidOperationException( - $"This disc is '{gameId}', not the expected '{manifest.GameId}' (Mario Kart Wii, region {manifest.Region}). " + + $"This disc is '{info.GameId}', not the expected '{manifest.GameId}' (Mario Kart Wii, region {manifest.Region}). " + "Only your own legally-owned copy of that exact game/region can be used."); } @@ -31,12 +35,12 @@ internal static class DiscTool Directory.CreateDirectory(scratch); try { - await RunAsync(["extract", "-i", isoPath, "-g", "-o", scratch, "-q"], cancellationToken); + await RunExtractAsync(nodTool, isoPath, scratch, cancellationToken); - var dolPath = Path.Combine(scratch, "DATA", "sys", "main.dol"); - var relPath = Path.Combine(scratch, "DATA", "files", "rel", "StaticR.rel"); - if (!File.Exists(dolPath)) throw new FileNotFoundException("dolphin-tool did not produce main.dol", dolPath); - if (!File.Exists(relPath)) throw new FileNotFoundException("dolphin-tool did not produce StaticR.rel", relPath); + 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); @@ -64,32 +68,53 @@ internal static class DiscTool } } - private static string Sha256Of(string path) + private static async Task RunInfoAsync(string nodTool, string isoPath, CancellationToken cancellationToken) { - using var stream = File.OpenRead(path); - return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant(); - } - - private static async Task RunAsync(string[] arguments, CancellationToken cancellationToken) - { - var startInfo = new System.Diagnostics.ProcessStartInfo("dolphin-tool") + var startInfo = new System.Diagnostics.ProcessStartInfo(nodTool) { + ArgumentList = { "info", isoPath }, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, }; - foreach (var argument in arguments) startInfo.ArgumentList.Add(argument); - using var process = System.Diagnostics.Process.Start(startInfo) - ?? throw new InvalidOperationException("Failed to start dolphin-tool. Is it installed and on PATH?"); + ?? throw new InvalidOperationException($"Failed to start {nodTool}."); var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken); var stderr = await process.StandardError.ReadToEndAsync(cancellationToken); await process.WaitForExitAsync(cancellationToken); if (process.ExitCode != 0) { throw new InvalidOperationException( - $"dolphin-tool {string.Join(' ', arguments)} failed (exit {process.ExitCode}): {stderr}"); + $"nodtool could not read this disc image (exit {process.ExitCode}): {stderr}{stdout}".Trim()); } return stdout; } + + private static string Sha256Of(string path) + { + using var stream = File.OpenRead(path); + return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant(); + } + + private static async Task RunExtractAsync(string nodTool, string isoPath, string outDir, CancellationToken cancellationToken) + { + var startInfo = new System.Diagnostics.ProcessStartInfo(nodTool) + { + ArgumentList = { "extract", isoPath, outDir, "-q" }, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + + using var process = System.Diagnostics.Process.Start(startInfo) + ?? throw new InvalidOperationException($"Failed to start {nodTool}."); + var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken); + var stderr = await process.StandardError.ReadToEndAsync(cancellationToken); + await process.WaitForExitAsync(cancellationToken); + if (process.ExitCode != 0) + { + throw new InvalidOperationException( + $"nodtool extract {isoPath} failed (exit {process.ExitCode}): {stderr}{stdout}"); + } + } } diff --git a/Launcher/WiiCompiled.Setup.Linux/Program.cs b/Launcher/WiiCompiled.Setup.Linux/Program.cs index a626a97..9c8e9ed 100644 --- a/Launcher/WiiCompiled.Setup.Linux/Program.cs +++ b/Launcher/WiiCompiled.Setup.Linux/Program.cs @@ -84,7 +84,8 @@ internal static class Program reporter.Progress(InstallStages.Validate, "Checking prerequisites", 1); if (flags.TryGetValue("game", out var isoPath) && !string.IsNullOrEmpty(isoPath)) { - await DiscTool.ValidateAndExtractAsync(isoPath, manifest, assetsDir, reporter, token); + await DiscTool.ValidateAndExtractAsync(isoPath, manifest, assetsDir, workspace, + flags.GetValueOrDefault("disc-tool-bin"), reporter, token); } else { @@ -261,7 +262,8 @@ internal static class Program 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] - [--force-clean-build] [--translator-bin PATH] [--progress-json] [--workspace DIR] + [--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH] + [--progress-json] [--workspace DIR] uninstall [--profile {base|retro-rewind|both|all}] launch-base launch-retro diff --git a/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj b/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj index 0b19ca5..59a7df6 100644 --- a/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj +++ b/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj @@ -14,4 +14,7 @@ en true + + + diff --git a/Launcher/WiiCompiled.Setup/ConsoleCommands.cs b/Launcher/WiiCompiled.Setup/ConsoleCommands.cs index 499b591..a269a1c 100644 --- a/Launcher/WiiCompiled.Setup/ConsoleCommands.cs +++ b/Launcher/WiiCompiled.Setup/ConsoleCommands.cs @@ -63,8 +63,8 @@ internal static class ConsoleCommands { using var payload = PayloadArchive.OpenCurrent(); var manifest = payload.ReadManifest(); - var tool = Path.Combine(temp, "DolphinTool.exe"); - payload.ExtractEntry(InstalledLayout.ToolkitEntryPrefix + "DolphinTool.exe", tool); + var tool = Path.Combine(temp, "nodtool.exe"); + payload.ExtractEntry(InstalledLayout.ToolkitEntryPrefix + "nodtool.exe", tool); payload.ExtractDirectory(InstalledLayout.ToolkitEntryPrefix + "Redist", temp); reporter?.Progress(InstallStages.Validate, "Checking the Wii disc image...", 10); var header = InputValidation.ReadDiscHeaderAsync(tool, command.GamePath!).GetAwaiter().GetResult(); diff --git a/Launcher/WiiCompiled.Setup/InputValidation.cs b/Launcher/WiiCompiled.Setup/InputValidation.cs index 854bd37..68ae52f 100644 --- a/Launcher/WiiCompiled.Setup/InputValidation.cs +++ b/Launcher/WiiCompiled.Setup/InputValidation.cs @@ -3,6 +3,7 @@ using System.Buffers.Binary; using System.Net; using System.Security.Cryptography; using System.Text.Json; +using WiiCompiled.NodTool; namespace WiiCompiled.Setup; @@ -47,23 +48,37 @@ internal static class InputValidation "Select a complete Wii disc image in ISO, GCM, GCZ, CISO, WBFS, WIA, or RVZ format."); } - public static async Task ReadDiscHeaderAsync(string dolphinTool, string gamePath, + public static async Task ReadDiscHeaderAsync(string nodTool, string gamePath, CancellationToken cancellationToken = default) { ValidateExtension(gamePath); - var result = await ProcessRunner.RunAsync(dolphinTool, - ["header", "-i", Path.GetFullPath(gamePath), "-j"], null, cancellationToken); + var result = await ProcessRunner.RunAsync(nodTool, + ["info", Path.GetFullPath(gamePath)], null, cancellationToken); if (result.ExitCode != 0) - throw new InvalidDataException("DolphinTool could not read this disc image. " + result.CombinedOutput.Trim()); + throw new InvalidDataException("nodtool could not read this disc image. " + result.CombinedOutput.Trim()); - var json = result.StandardOutput.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) - .FirstOrDefault(line => line.TrimStart().StartsWith('{')); - if (json is null) - throw new InvalidDataException("DolphinTool did not return disc metadata."); - return JsonSerializer.Deserialize(json) - ?? throw new InvalidDataException("DolphinTool returned invalid disc metadata."); + 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)) diff --git a/Launcher/WiiCompiled.Setup/InstallerEngine.cs b/Launcher/WiiCompiled.Setup/InstallerEngine.cs index 3381eea..f5cb13c 100644 --- a/Launcher/WiiCompiled.Setup/InstallerEngine.cs +++ b/Launcher/WiiCompiled.Setup/InstallerEngine.cs @@ -57,9 +57,9 @@ internal sealed class InstallerEngine var runtimeAssetsCurrent = sameToolkit && RuntimeAssetsAreCurrent(existing, candidateRuntimeAssetsFingerprint, cancellationToken); - var installedDolphinTool = Path.Combine(existing.ToolkitDirectory, "DolphinTool.exe"); + var installedNodTool = Path.Combine(existing.ToolkitDirectory, "nodtool.exe"); var extractToolkit = MustRefreshToolkit(sameToolkit, samePackageContent, - File.Exists(installedDolphinTool)); + File.Exists(installedNodTool)); var extractWorkspace = !sameToolkit || !runtimeAssetsCurrent; _reporter.Progress(InstallStages.ExtractToolkit, @@ -77,9 +77,9 @@ internal sealed class InstallerEngine payload.ExtractEntry(InstalledLayout.PayloadManifestFileName, Path.Combine(staging, InstalledLayout.PayloadManifestFileName)); - var dolphinTool = extractToolkit ? Path.Combine(toolkit, "DolphinTool.exe") : installedDolphinTool; + var nodTool = extractToolkit ? Path.Combine(toolkit, "nodtool.exe") : installedNodTool; _reporter.Progress(InstallStages.Validate, "Checking the Wii disc image...", 2); - var header = await InputValidation.ReadDiscHeaderAsync(dolphinTool, options.GamePath, + var header = await InputValidation.ReadDiscHeaderAsync(nodTool, options.GamePath, cancellationToken); InputValidation.EnsureCompatibleDisc(header, manifest); var canonicalRetroRoot = options.RetroDirectoryPath is null @@ -153,7 +153,7 @@ internal sealed class InstallerEngine if (reusableGameAssets is null) { - await ExtractGameAssetsAsync(dolphinTool, options.GamePath, + await ExtractGameAssetsAsync(nodTool, options.GamePath, Path.Combine(staging, "GameAssets"), manifest, cancellationToken); } @@ -164,8 +164,8 @@ internal sealed class InstallerEngine internal static bool MustRefreshToolkit(bool sameToolkit, bool samePackageContent, - bool dolphinToolPresent) => - !sameToolkit || !samePackageContent || !dolphinToolPresent; + bool nodToolPresent) => + !sameToolkit || !samePackageContent || !nodToolPresent; private static void AddComponent(List entries, string staging, string installDirectory, string name) => @@ -469,18 +469,23 @@ internal sealed class InstallerEngine } } - private async Task ExtractGameAssetsAsync(string dolphinTool, string gamePath, string destination, + private async Task ExtractGameAssetsAsync(string nodTool, string gamePath, string destination, PayloadManifest manifest, CancellationToken cancellationToken) { _reporter.Progress(InstallStages.ExtractDisc, "Extracting the game disc. This is the longest preparation step...", 6); - var extraction = await ProcessRunner.RunAsync(dolphinTool, - ["extract", "-i", Path.GetFullPath(gamePath), "-o", destination, "-g", "-q"], + // Extracted straight into a "DATA" subfolder so the on-disk layout matches what + // Installation.GameDataDirectory and every other reader of it already expect - nodtool + // itself has no such wrapper (it extracts sys/+files/ directly to whatever is + // given), so this is purely destination-side, not a nodtool convention. + var dataRoot = Path.Combine(destination, "DATA"); + var extraction = await ProcessRunner.RunAsync(nodTool, + ["extract", Path.GetFullPath(gamePath), dataRoot, "-q"], line => { if (!string.IsNullOrWhiteSpace(line)) _reporter.Diagnostic(line); }, cancellationToken); if (extraction.ExitCode != 0) throw new InvalidDataException("Game extraction failed. " + extraction.CombinedOutput.Trim()); - ValidateExtractedGame(Path.Combine(destination, "DATA"), manifest); + ValidateExtractedGame(dataRoot, manifest); } private static void ValidateExtractedGame(string dataRoot, PayloadManifest manifest) diff --git a/Launcher/WiiCompiled.Setup/SelfTests.cs b/Launcher/WiiCompiled.Setup/SelfTests.cs index 3a807f3..fc97c0b 100644 --- a/Launcher/WiiCompiled.Setup/SelfTests.cs +++ b/Launcher/WiiCompiled.Setup/SelfTests.cs @@ -143,17 +143,17 @@ internal static class SelfTests private static void TestToolkitRefreshDecision() { if (!InstallerEngine.MustRefreshToolkit(sameToolkit: false, samePackageContent: true, - dolphinToolPresent: true)) + nodToolPresent: true)) throw new Exception("A republished workspace kept the installed toolkit; the shipped " + "translator and project file could come from different releases."); if (!InstallerEngine.MustRefreshToolkit(sameToolkit: true, samePackageContent: false, - dolphinToolPresent: true)) + nodToolPresent: true)) throw new Exception("Changed toolkit package content was not extracted."); if (!InstallerEngine.MustRefreshToolkit(sameToolkit: true, samePackageContent: true, - dolphinToolPresent: false)) - throw new Exception("A missing DolphinTool.exe did not force toolkit extraction."); + nodToolPresent: false)) + throw new Exception("A missing nodtool.exe did not force toolkit extraction."); if (InstallerEngine.MustRefreshToolkit(sameToolkit: true, samePackageContent: true, - dolphinToolPresent: true)) + nodToolPresent: true)) throw new Exception("An unchanged toolkit was needlessly re-extracted."); } @@ -1010,7 +1010,7 @@ internal static class SelfTests throw new Exception("The toolkit fingerprint is not stable."); // A file that has nothing to do with generated code must not invalidate every install. - File.WriteAllText(Path.Combine(root, "Toolkit", "DolphinTool.exe"), "irrelevant"); + File.WriteAllText(Path.Combine(root, "Toolkit", "nodtool.exe"), "irrelevant"); if (ToolkitFingerprint.Compute(root) != first) throw new Exception("An unrelated toolkit file changed the fingerprint."); @@ -1177,7 +1177,7 @@ internal static class SelfTests "x86_64-w64-mingw32-clang++.exe", "x86_64-w64-mingw32-windres.exe" }) File.WriteAllText(Path.Combine(root, "Toolkit", "llvm-mingw", "bin", executable), executable); - File.WriteAllText(Path.Combine(root, "Toolkit", "DolphinTool.exe"), "tool"); + File.WriteAllText(Path.Combine(root, "Toolkit", "nodtool.exe"), "tool"); File.WriteAllText(Path.Combine(root, "BuildWorkspace", "LocalBuild.ps1"), "# build"); File.WriteAllText(Path.Combine(root, "BuildWorkspace", "NativeBuildFlags.ps1"), "# flags"); File.WriteAllText(Path.Combine(root, "BuildWorkspace", "projects", "mkwii", "recomp.yml"), "profiles: {}"); diff --git a/Launcher/WiiCompiled.Setup/ToolkitFingerprint.cs b/Launcher/WiiCompiled.Setup/ToolkitFingerprint.cs index ded4feb..8976210 100644 --- a/Launcher/WiiCompiled.Setup/ToolkitFingerprint.cs +++ b/Launcher/WiiCompiled.Setup/ToolkitFingerprint.cs @@ -78,12 +78,12 @@ internal static class ToolkitFingerprint var workspace = InstalledLayout.Workspace(root); var entries = new SortedDictionary(StringComparer.Ordinal); - // DolphinTool validates/extracts the user disc but does not influence generated products. + // nodtool validates/extracts the user disc but does not influence generated products. // Everything else in Toolkit can affect translation, compilation, linking, or copied // runtime support and therefore belongs to the compile identity. AddDirectory(entries, root, toolkit, null, cancellationToken, - file => !Path.GetFileName(file).Equals("DolphinTool.exe", StringComparison.OrdinalIgnoreCase)); + file => !Path.GetFileName(file).Equals("nodtool.exe", StringComparison.OrdinalIgnoreCase)); AddFile(entries, root, Path.Combine(workspace, "LocalBuild.ps1"), cancellationToken); AddFile(entries, root, Path.Combine(workspace, "NativeBuildFlags.ps1"), cancellationToken); AddDirectory(entries, root, Path.Combine(workspace, "projects"), null, cancellationToken); diff --git a/Launcher/WiiCompiled.Setup/WiiCompiled.Setup.csproj b/Launcher/WiiCompiled.Setup/WiiCompiled.Setup.csproj index 729883b..d598994 100644 --- a/Launcher/WiiCompiled.Setup/WiiCompiled.Setup.csproj +++ b/Launcher/WiiCompiled.Setup/WiiCompiled.Setup.csproj @@ -14,4 +14,7 @@ embedded en + + + diff --git a/Launcher/build-appimage.sh b/Launcher/build-appimage.sh index cf02a1c..6db0e79 100644 --- a/Launcher/build-appimage.sh +++ b/Launcher/build-appimage.sh @@ -1,10 +1,12 @@ #!/usr/bin/env bash # Packages Launcher/WiiCompiled.Setup.Linux as a self-contained AppImage: a single file Wheel -# Wizard (or anyone else) can fetch and execute with no git clone and no `dotnet` install at all - -# both the installer and the translator are published as self-contained binaries and bundled, so -# AppRun passes --translator-bin to skip local-build.sh's own dotnet-build-from-source step. It -# still shells out to system clang/cmake/ninja/dolphin-tool - no C/C++ toolchain is bundled, -# matching Launcher/local-build.sh's own remaining prerequisites. +# Wizard (or anyone else) can fetch and execute with no git clone, no `dotnet` install, and no +# `dolphin-tool` package required at all. The installer and translator are published as +# self-contained binaries, and `nodtool` (a prebuilt MIT/Apache-2.0 CLI from encounter/nod, see +# NodToolProvider.cs) is downloaded and bundled too - AppRun passes --translator-bin and +# --disc-tool-bin so local-build.sh/DiscTool.cs skip their from-source/download fallbacks entirely. +# It still shells out to system clang/cmake/ninja - no C/C++ toolchain is bundled, matching +# Launcher/local-build.sh's own remaining prerequisites. # # An AppImage mounts read-only, but local-build.sh writes generated/, native-build/, Assets/, etc. # into the workspace it's given. So AppRun (written below) copies the bundled workspace snapshot @@ -59,6 +61,16 @@ 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 +# 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 -- \ + --workspace "$workspace" | tail -n1) +cp "$nodtool_path" "$appdir/usr/bin/nodtool" +chmod +x "$appdir/usr/bin/nodtool" + echo "Staging the bundled workspace snapshot..." for dir in runtime aurora-main projects; do cp -r "$workspace/$dir" "$appdir/workspace/$dir" @@ -94,7 +106,9 @@ if [ ! -f "$CACHE/.bundle-version" ] || \ cp "$HERE/workspace/Launcher/local-build.sh" "$CACHE/Launcher/local-build.sh" cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version" fi -exec "$HERE/usr/bin/wiicompiled-setup" --workspace "$CACHE" --translator-bin "$HERE/usr/bin/translator-cli" "$@" +exec "$HERE/usr/bin/wiicompiled-setup" --workspace "$CACHE" \ + --translator-bin "$HERE/usr/bin/translator-cli" \ + --disc-tool-bin "$HERE/usr/bin/nodtool" "$@" APPRUN chmod +x "$appdir/AppRun" diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 2fc265a..ee637b5 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -140,6 +140,7 @@ included in the installer's `licenses/` folder. | SQLite | 3.51.3 amalgamation | Public domain | | | Tracy Profiler | pinned commit | BSD-3-Clause | | | C++/WinRT | - | MIT (Microsoft) | | +| nodtool (disc image extraction) | v2.0.0-alpha.10 | MIT OR Apache-2.0 | | ### Dual-licensed components - elections made by this project @@ -164,16 +165,15 @@ unmodified, with their license texts, in the installer's `licenses/` folder. | llvm-mingw (Clang, LLD, libc++, libunwind, MinGW-w64 runtime) | Apache-2.0 with LLVM Exception; MinGW-w64 runtime under its own permissive terms; bundled GNU utilities under GPL-2.0-or-later or GPL-3.0-or-later | | | CMake | BSD-3-Clause | | | Ninja | Apache-2.0 | | -| DolphinTool (disc image extraction) | GPL-2.0-or-later | | +| nodtool (disc image extraction) | MIT OR Apache-2.0 | | | Microsoft Visual C++ Runtime (`vcruntime140.dll`, `vcruntime140_1.dll`, `msvcp140.dll`) | Microsoft redistributable terms | Microsoft Visual Studio | | `dxil.dll` | Microsoft redistributable (proprietary signing library) | Microsoft | > [!IMPORTANT] -> Several toolkit components are GPL-licensed (DolphinTool, and the GNU utilities inside -> llvm-mingw). Their complete corresponding source is available from the upstream projects linked -> above at their pinned versions, and this project will supply it on request for the exact versions -> shipped in any given release. Pins live in `Launcher/Prepare-PortableTools.ps1` and -> `Launcher/NativeBuildFlags.ps1`. +> The GNU utilities bundled inside llvm-mingw are GPL-licensed. Their complete corresponding source +> is available from the upstream project linked above at its pinned version, and this project will +> supply it on request for the exact version shipped in any given release. Pins live in +> `Launcher/Prepare-PortableTools.ps1` and `Launcher/NativeBuildFlags.ps1`. ---