Compare commits

..

1 Commits

Author SHA1 Message Date
patchzyy 28a24ea49d Always refresh Retro-WFC payload with fallback 2026-09-28 20:41:51 +02:00
6 changed files with 70 additions and 57 deletions
+2 -9
View File
@@ -103,7 +103,7 @@ function Write-MkwBuildStep([string]$StepId, [string]$Message) {
function Reset-LocalDirectory([string]$Path) { function Reset-LocalDirectory([string]$Path) {
$full = [IO.Path]::GetFullPath($Path) $full = [IO.Path]::GetFullPath($Path)
$root = [IO.Path]::GetFullPath($Workspace).TrimEnd('\') + '\' $root = [IO.Path]::GetFullPath($Workspace).TrimEnd('\') + '\'
$installRoot = [IO.Path]::GetFullPath((Split-Path -Parent $realWorkspace)).TrimEnd('\') + '\' $installRoot = [IO.Path]::GetFullPath((Split-Path -Parent $Workspace)).TrimEnd('\') + '\'
# The caller-supplied output destinations are legitimate reset targets by # The caller-supplied output destinations are legitimate reset targets by
# definition, wherever the caller placed them: a fresh install's operation # definition, wherever the caller placed them: a fresh install's operation
# scratch lives beside the installation directory rather than inside it. # scratch lives beside the installation directory rather than inside it.
@@ -150,15 +150,8 @@ if ($Profile -eq 'both' -and [string]::IsNullOrWhiteSpace($BaseOutputDirectory))
if ($Profile -ne 'both' -and -not [string]::IsNullOrWhiteSpace($BaseOutputDirectory)) { if ($Profile -ne 'both' -and -not [string]::IsNullOrWhiteSpace($BaseOutputDirectory)) {
throw '-BaseOutputDirectory is valid only with -Profile both.' throw '-BaseOutputDirectory is valid only with -Profile both.'
} }
$realWorkspace = $Workspace.TrimEnd('\')
$Workspace = Get-MkwBuildSafePath $realWorkspace 'workspace' 'runtime\CMakeLists.txt'
# One spelling of the workspace, so the staged Code.pul check below can't copy a file onto itself.
if (-not [string]::IsNullOrWhiteSpace($RetroRewindPackageDirectory) -and
$RetroRewindPackageDirectory.StartsWith($realWorkspace + '\', [StringComparison]::OrdinalIgnoreCase)) {
$RetroRewindPackageDirectory = $Workspace + $RetroRewindPackageDirectory.Substring($realWorkspace.Length)
}
$translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe' $translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe'
$toolchain = Get-MkwBuildSafePath $Toolkit 'toolchain' 'CMake\bin\cmake.exe' $toolchain = Get-MkwShellSafeToolchainRoot $Toolkit
$cmake = Join-Path $toolchain 'CMake\bin\cmake.exe' $cmake = Join-Path $toolchain 'CMake\bin\cmake.exe'
$ninja = Join-Path $toolchain 'Ninja\ninja.exe' $ninja = Join-Path $toolchain 'Ninja\ninja.exe'
$toolchainBin = Join-Path $toolchain 'llvm-mingw\bin' $toolchainBin = Join-Path $toolchain 'llvm-mingw\bin'
+12 -19
View File
@@ -40,48 +40,41 @@ function Get-MkwToolchainPath([string]$ToolchainRoot) {
) -join ';') ) -join ';')
} }
function Get-MkwBuildSafePath([string]$Path, [string]$Kind, [string]$MarkerFile) { function Get-MkwShellSafeToolchainRoot([string]$ToolchainRoot) {
<# if ([string]::IsNullOrWhiteSpace($ToolchainRoot)) { throw 'A toolchain root is required.' }
$Path, or a junction to it whose path is plain ASCII. cmd.exe, Ninja response files and the $full = [IO.Path]::GetFullPath($ToolchainRoot)
compiler each have their own quoting rules, so a path outside this allowlist ('&', '%', an
apostrophe, non-ASCII...) is never handed to the native build at all. $MarkerFile is a file
that must exist under a live junction.
#>
if ([string]::IsNullOrWhiteSpace($Path)) { throw "A $Kind path is required." }
$full = [IO.Path]::GetFullPath($Path)
# A drive root keeps its separator: "C:" is relative to the current directory on that drive. # A drive root keeps its separator: "C:" is relative to the current directory on that drive.
if ($full -ne [IO.Path]::GetPathRoot($full)) { $full = $full.TrimEnd('\') } if ($full -ne [IO.Path]::GetPathRoot($full)) { $full = $full.TrimEnd('\') }
if ($full -cmatch '^[A-Za-z0-9 ._\\:-]+$') { return $full } if ($full -notmatch '[()&^%!]') { return $full }
$sha = [Security.Cryptography.SHA256]::Create() $sha = [Security.Cryptography.SHA256]::Create()
try { try {
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($full.ToLowerInvariant())) $bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($full.ToLowerInvariant()))
} finally { $sha.Dispose() } } finally { $sha.Dispose() }
$linkName = "$Kind-" + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '') $linkName = 'toolchain-' + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '')
$failures = @() $failures = @()
foreach ($base in @($env:ProgramData, $env:PUBLIC)) { foreach ($base in @($env:ProgramData, $env:PUBLIC)) {
if ([string]::IsNullOrWhiteSpace($base) -or $base -cnotmatch '^[A-Za-z0-9._\\:-]+$') { continue } if ([string]::IsNullOrWhiteSpace($base) -or $base -match '[()&^%! ]') { continue }
$link = Join-Path (Join-Path $base 'WiiCompiled') $linkName $link = Join-Path (Join-Path $base 'WiiCompiled') $linkName
try { try {
[IO.Directory]::CreateDirectory((Split-Path -Parent $link)) | Out-Null [IO.Directory]::CreateDirectory((Split-Path -Parent $link)) | Out-Null
# The name already identifies the target, so an existing junction that still resolves is # The name already identifies the target, so an existing junction that still resolves is
# this one; only a broken leftover is replaced. Directory.Delete removes the reparse # this one; only a broken leftover is replaced. Directory.Delete removes the reparse
# point itself, where Remove-Item -Recurse would delete the tree it points at. # point itself, where Remove-Item -Recurse would delete the toolchain it points at.
if (-not (Test-Path -LiteralPath (Join-Path $link $MarkerFile) -PathType Leaf)) { if (-not (Test-Path -LiteralPath (Join-Path $link 'CMake\bin\cmake.exe') -PathType Leaf)) {
if (Test-Path -LiteralPath $link) { [IO.Directory]::Delete($link) } if (Test-Path -LiteralPath $link) { [IO.Directory]::Delete($link) }
New-Item -ItemType Junction -Path $link -Target $full -ErrorAction Stop | Out-Null New-Item -ItemType Junction -Path $link -Target $full -ErrorAction Stop | Out-Null
} }
Write-Host "MKWCBUILD: Building the $Kind through $link, because $full contains characters the native build cannot quote reliably" Write-Host "MKWCBUILD: Building through $link, because $full contains characters cmd.exe cannot parse"
return $link return $link
} catch { } catch {
$failures += "$link ($($_.Exception.Message))" $failures += "$link ($($_.Exception.Message))"
} }
} }
Write-Host ("MKWCBUILD: Warning: no junction to $full could be created (" + ($failures -join '; ') + throw ("The toolchain path $full contains a character (one of ( ) & ^ % !) that the compiler " +
'); building from the original path, which may fail. Installing to a path of plain ' + 'cannot be invoked through, and no junction to it could be created: ' + ($failures -join '; ') +
'letters, digits and spaces avoids this.') '. Install to a path without those characters.')
return $full
} }
function Get-MkwProjectPins([string]$ProjectFile) { function Get-MkwProjectPins([string]$ProjectFile) {
+27 -6
View File
@@ -127,18 +127,39 @@ internal static class Program
string? retroWfcOfflineDir = null; string? retroWfcOfflineDir = null;
if (downloadPayload) 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"); var cacheDir = Path.Combine(workspace, "generated", "retro-wfc-payload");
reporter.Progress(InstallStages.Validate, "Preparing the Retro-WFC payload", 1); reporter.Progress(InstallStages.Validate,
"Downloading the current Retro-WFC payload", 1);
try
{
// A valid signature authenticates a payload, but does not prove it is the latest
// signed revision. Always ask the fixed endpoint for the current snapshot; the
// downloader verifies it before atomically replacing the cache.
await RetroWfcPayload.DownloadRetroWfcPayloadAsync(
RetroWfcPayload.CurrentRetroWfcPayloadUri, cacheDir, token);
}
catch (Exception downloadFailure) when (!token.IsCancellationRequested &&
downloadFailure is HttpRequestException or TimeoutException
or IOException)
{
// Offline installs may continue with a previously authenticated snapshot. Do not
// use this path for a newly downloaded payload that failed signature validation:
// that must remain a hard failure instead of hiding possible endpoint tampering.
try try
{ {
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir); RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir);
} }
catch (InvalidDataException) catch (Exception cacheFailure) when (cacheFailure is IOException or
UnauthorizedAccessException or InvalidDataException)
{ {
await RetroWfcPayload.DownloadRetroWfcPayloadAsync( throw new InvalidOperationException(
RetroWfcPayload.CurrentRetroWfcPayloadUri, cacheDir, token); "The current Retro-WFC payload could not be downloaded and no valid cached " +
$"payload is available ({cacheFailure.Message.TrimEnd('.')}).", downloadFailure);
}
reporter.Diagnostic(
"The current Retro-WFC payload could not be downloaded; using the previously " +
$"verified cached payload instead ({downloadFailure.Message.TrimEnd('.')}).");
} }
retroWfcOfflineDir = cacheDir; retroWfcOfflineDir = cacheDir;
} }
@@ -110,8 +110,7 @@ internal sealed class ProductRepairService
InputValidation.CurrentRetroWfcPayloadUri, payloadScratch, cancellationToken); InputValidation.CurrentRetroWfcPayloadUri, payloadScratch, cancellationToken);
} }
catch (Exception ex) when (!cancellationToken.IsCancellationRequested && catch (Exception ex) when (!cancellationToken.IsCancellationRequested &&
ex is HttpRequestException or IOException or InvalidDataException ex is HttpRequestException or TimeoutException or IOException)
or InvalidOperationException or OperationCanceledException)
{ {
payloadSnapshot = RecoverInstalledRetroWfcPayload(toolkitFingerprint, payloadSnapshot = RecoverInstalledRetroWfcPayload(toolkitFingerprint,
Path.Combine(scratchRoot, "retro-wfc-payload-recovered"), ex, cancellationToken); Path.Combine(scratchRoot, "retro-wfc-payload-recovered"), ex, cancellationToken);
+19 -5
View File
@@ -77,26 +77,40 @@ if [[ -n "$retro_dir" ]]; then
# verify its pinned signature before publishing it into the local cache. # verify its pinned signature before publishing it into the local cache.
retro_wfc_dir="$support_root/RetroWfcPayload" retro_wfc_dir="$support_root/RetroWfcPayload"
retro_wfc_payload="$retro_wfc_dir/binary/payload.RMCPD00.bin" retro_wfc_payload="$retro_wfc_dir/binary/payload.RMCPD00.bin"
if [[ -f "$retro_wfc_payload" ]] && ! "$translator" validate-retro-wfc-payload --directory "$retro_wfc_dir"; then cached_payload_valid=0
if [[ -f "$retro_wfc_payload" ]]; then
if "$translator" validate-retro-wfc-payload --directory "$retro_wfc_dir"; then
cached_payload_valid=1
else
printf 'Discarding an invalid cached Retro-WFC payload...\n' >&2 printf 'Discarding an invalid cached Retro-WFC payload...\n' >&2
rm -f "$retro_wfc_payload" rm -f "$retro_wfc_payload"
fi fi
if [[ ! -f "$retro_wfc_payload" ]]; then fi
printf 'Downloading the Retro-WFC payload needed for online play...\n'
# A signed cache may still be an older vulnerable revision, so always attempt to replace it
# with the current signed snapshot. A transport failure may fall back to the verified cache;
# a downloaded snapshot with an invalid signature remains a hard failure.
printf 'Downloading the current Retro-WFC payload needed for online play...\n'
mkdir -p "$retro_wfc_dir" mkdir -p "$retro_wfc_dir"
payload_stage=$(mktemp -d "$retro_wfc_dir/.payload-download.XXXXXX") payload_stage=$(mktemp -d "$retro_wfc_dir/.payload-download.XXXXXX")
temporary_payload="$payload_stage/binary/payload.RMCPD00.bin" temporary_payload="$payload_stage/binary/payload.RMCPD00.bin"
mkdir -p "$(dirname "$temporary_payload")" mkdir -p "$(dirname "$temporary_payload")"
trap 'rm -rf "$payload_stage"' EXIT trap 'rm -rf "$payload_stage"' EXIT
/usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \ if /usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \
--retry 1 --output "$temporary_payload" \ --retry 1 --output "$temporary_payload" \
'https://rwfc.net/api/wfc/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play' 'https://rwfc.net/api/wfc/payload?g=RMCPD00'; then
"$translator" validate-retro-wfc-payload --directory "$payload_stage" || \ "$translator" validate-retro-wfc-payload --directory "$payload_stage" || \
fail 'downloaded Retro-WFC payload failed signature validation' fail 'downloaded Retro-WFC payload failed signature validation'
mkdir -p "$retro_wfc_dir/binary" mkdir -p "$retro_wfc_dir/binary"
mv "$temporary_payload" "$retro_wfc_payload" mv "$temporary_payload" "$retro_wfc_payload"
rmdir "$payload_stage/binary" "$payload_stage" rmdir "$payload_stage/binary" "$payload_stage"
trap - EXIT trap - EXIT
elif (( cached_payload_valid )); then
printf 'Could not download the current Retro-WFC payload; using the previously verified cached payload.\n' >&2
rm -rf "$payload_stage"
trap - EXIT
else
fail 'could not download the current Retro-WFC payload and no valid cached payload is available'
fi fi
build_args+=(--profile both --base-output-dir "$products" --retro-rewind-package-dir "$retro_dir" --retro-wfc-offline-dir "$retro_wfc_dir") build_args+=(--profile both --base-output-dir "$products" --retro-rewind-package-dir "$retro_dir" --retro-wfc-offline-dir "$retro_wfc_dir")
fi fi
-7
View File
@@ -21,13 +21,6 @@ if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
message(FATAL_ERROR "WiiCompiled only supports Release builds") message(FATAL_ERROR "WiiCompiled only supports Release builds")
endif() endif()
# Ninja writes Windows-quoted response files; the GNU clang driver otherwise reads them POSIX-style.
if(CMAKE_HOST_WIN32)
foreach(_mkw_lang C CXX)
set(CMAKE_${_mkw_lang}_RESPONSE_FILE_LINK_FLAG "--rsp-quoting=windows @")
endforeach()
endif()
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON) option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
# Preprocessor definitions that belong to this project's own code (the runtime, # Preprocessor definitions that belong to this project's own code (the runtime,