switch to standalone nod tool rather than dolphin-tool

This commit is contained in:
theofficialgman
2026-08-26 18:55:47 -04:00
parent edc5fa1dd3
commit f09590aa5d
18 changed files with 346 additions and 95 deletions
+37 -14
View File
@@ -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
-6
View File
@@ -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
@@ -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 <repo-root>
// 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 <repo-root>");
return 1;
}
var path = await NodToolProvider.ResolveAsync(workspace, CancellationToken.None);
Console.WriteLine(path);
return 0;
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.NodTool.Cli</RootNamespace>
<AssemblyName>WiiCompiled.NodTool.Cli</AssemblyName>
<Version>0.2.22</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
<DebugType>embedded</DebugType>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WiiCompiled.NodTool\WiiCompiled.NodTool.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,38 @@
using System.Text.RegularExpressions;
namespace WiiCompiled.NodTool;
/// <summary>Disc metadata parsed from `nodtool info`'s stdout.</summary>
public sealed record NodToolDiscInfo(string GameId, string Title, int Revision);
/// <summary>
/// Parses the plain-text stdout of `nodtool info &lt;iso&gt;`. 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.
/// </summary>
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();
}
@@ -0,0 +1,67 @@
using System.Runtime.InteropServices;
namespace WiiCompiled.NodTool;
/// <summary>
/// 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).
/// </summary>
public static class NodToolProvider
{
public const string Version = "v2.0.0-alpha.10";
public static async Task<string> 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}"),
};
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.NodTool</RootNamespace>
<AssemblyName>WiiCompiled.NodTool</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>
<DebugType>embedded</DebugType>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
</Project>
+54 -29
View File
@@ -1,28 +1,32 @@
using System.Security.Cryptography;
using System.Text.Json;
using WiiCompiled.NodTool;
namespace WiiCompiled.Setup.Linux;
/// <summary>
/// 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).
/// </summary>
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<string> RunInfoAsync(string nodTool, string isoPath, CancellationToken cancellationToken)
{
using var stream = File.OpenRead(path);
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
}
private static async Task<string> 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}");
}
}
}
+4 -2
View File
@@ -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
@@ -14,4 +14,7 @@
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WiiCompiled.NodTool\WiiCompiled.NodTool.csproj" />
</ItemGroup>
</Project>
@@ -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();
+25 -10
View File
@@ -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<DiscHeader> ReadDiscHeaderAsync(string dolphinTool, string gamePath,
public static async Task<DiscHeader> 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<DiscHeader>(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))
+16 -11
View File
@@ -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<InstallTransactionEntry> 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 <outdir> 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)
+7 -7
View File
@@ -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: {}");
@@ -78,12 +78,12 @@ internal static class ToolkitFingerprint
var workspace = InstalledLayout.Workspace(root);
var entries = new SortedDictionary<string, string>(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);
@@ -14,4 +14,7 @@
<DebugType>embedded</DebugType>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WiiCompiled.NodTool\WiiCompiled.NodTool.csproj" />
</ItemGroup>
</Project>
+20 -6
View File
@@ -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"
+6 -6
View File
@@ -140,6 +140,7 @@ included in the installer's `licenses/` folder.
| SQLite | 3.51.3 amalgamation | Public domain | <https://sqlite.org/> |
| Tracy Profiler | pinned commit | BSD-3-Clause | <https://github.com/wolfpld/tracy> |
| C++/WinRT | - | MIT (Microsoft) | <https://github.com/microsoft/cppwinrt> |
| nodtool (disc image extraction) | v2.0.0-alpha.10 | MIT OR Apache-2.0 | <https://github.com/encounter/nod> |
### 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 | <https://github.com/mstorsjo/llvm-mingw> |
| CMake | BSD-3-Clause | <https://cmake.org/> |
| Ninja | Apache-2.0 | <https://ninja-build.org/> |
| DolphinTool (disc image extraction) | GPL-2.0-or-later | <https://github.com/dolphin-emu/dolphin> |
| nodtool (disc image extraction) | MIT OR Apache-2.0 | <https://github.com/encounter/nod> |
| 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`.
---