mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-10 09:11:52 -04:00
302 lines
15 KiB
PowerShell
302 lines
15 KiB
PowerShell
# Canonical native (CMake) configure flags, plus the shared helpers (file assertions, checked
|
|
# process spawn, file hashing) both LocalBuild.ps1 and Prepare-NativePrebuilt.ps1 dot-source.
|
|
# They MUST stay byte-identical: the prebuilt package ships static archives linked into objects
|
|
# the user's machine compiles, so any flag divergence is an ABI/ODR hazard, not just a rebuild.
|
|
# The flag fingerprint below is recorded in the package provenance and re-checked before use.
|
|
|
|
Set-StrictMode -Version 3.0
|
|
|
|
function Assert-File([string]$Path, [string]$Description) {
|
|
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "$Description is missing: $Path" }
|
|
}
|
|
|
|
function Assert-Directory([string]$Path, [string]$Description) {
|
|
if (-not (Test-Path -LiteralPath $Path -PathType Container)) { throw "$Description is missing: $Path" }
|
|
}
|
|
|
|
function Get-MkwFileSha256([string]$Path) {
|
|
<#
|
|
Lower-case SHA-256 of one file. -LiteralPath is required: Get-FileHash treats a positional
|
|
path as a wildcard, so an install directory containing [, ] or * would hash the wrong file.
|
|
#>
|
|
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
}
|
|
|
|
function Get-MkwToolchainPath([string]$ToolchainRoot) {
|
|
<#
|
|
The scrubbed PATH a bundled-toolchain build runs with. The bundled toolchain must be the only
|
|
one visible, or an ambient Visual Studio/MSYS install on PATH could pull in its own headers,
|
|
libraries, or linker. Shared by LocalBuild.ps1 and Prepare-NativePrebuilt.ps1 since the
|
|
installed Toolkit and the maintainer portable-tools tree share the same subdirectory layout.
|
|
#>
|
|
if ([string]::IsNullOrWhiteSpace($ToolchainRoot)) { throw 'A toolchain root is required.' }
|
|
$root = [IO.Path]::GetFullPath($ToolchainRoot)
|
|
return (@(
|
|
(Join-Path $root 'llvm-mingw\bin'),
|
|
(Join-Path $root 'CMake\bin'),
|
|
(Join-Path $root 'Ninja'),
|
|
(Join-Path $env:SystemRoot 'System32'),
|
|
$env:SystemRoot
|
|
) -join ';')
|
|
}
|
|
|
|
function Get-MkwShellSafeToolchainRoot([string]$ToolchainRoot) {
|
|
if ([string]::IsNullOrWhiteSpace($ToolchainRoot)) { throw 'A toolchain root is required.' }
|
|
$full = [IO.Path]::GetFullPath($ToolchainRoot)
|
|
# 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 -notmatch '[()&^%!]') { return $full }
|
|
|
|
$sha = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($full.ToLowerInvariant()))
|
|
} finally { $sha.Dispose() }
|
|
$linkName = 'toolchain-' + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '')
|
|
|
|
$failures = @()
|
|
foreach ($base in @($env:ProgramData, $env:PUBLIC)) {
|
|
if ([string]::IsNullOrWhiteSpace($base) -or $base -match '[()&^%! ]') { continue }
|
|
$link = Join-Path (Join-Path $base 'WiiCompiled') $linkName
|
|
try {
|
|
[IO.Directory]::CreateDirectory((Split-Path -Parent $link)) | Out-Null
|
|
# 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
|
|
# point itself, where Remove-Item -Recurse would delete the toolchain it points at.
|
|
if (-not (Test-Path -LiteralPath (Join-Path $link 'CMake\bin\cmake.exe') -PathType Leaf)) {
|
|
if (Test-Path -LiteralPath $link) { [IO.Directory]::Delete($link) }
|
|
New-Item -ItemType Junction -Path $link -Target $full -ErrorAction Stop | Out-Null
|
|
}
|
|
Write-Host "MKWCBUILD: Building through $link, because $full contains characters cmd.exe cannot parse"
|
|
return $link
|
|
} catch {
|
|
$failures += "$link ($($_.Exception.Message))"
|
|
}
|
|
}
|
|
throw ("The toolchain path $full contains a character (one of ( ) & ^ % !) that the compiler " +
|
|
'cannot be invoked through, and no junction to it could be created: ' + ($failures -join '; ') +
|
|
'. Install to a path without those characters.')
|
|
}
|
|
|
|
function Get-MkwProjectPins([string]$ProjectFile) {
|
|
<#
|
|
The Mario Kart Wii facts pinned by projects/mkwii/recomp.yml (game identity, clean input
|
|
digests, translation entry point, Retro-WFC endpoint), read from one place instead of being
|
|
restated; Test-PinnedFacts.ps1 checks the copies that can't read YAML (C++ header, C#
|
|
constants) against these values. Parsing is literal, not a YAML dependency, since the file is
|
|
machine-written with a fixed shape.
|
|
#>
|
|
Assert-File $ProjectFile 'Translation project file'
|
|
$pins = [ordered]@{
|
|
GameId = ''; DolSha256 = ''; RelSha256 = ''; EntryPoint = ''; RetroWfcPayloadUri = ''
|
|
}
|
|
$section = ''
|
|
$inputKey = ''
|
|
foreach ($raw in [IO.File]::ReadAllLines($ProjectFile)) {
|
|
$line = ($raw -replace '#.*$', '')
|
|
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
|
if ($line -match '^([A-Za-z0-9_]+):') { $section = $Matches[1]; $inputKey = ''; continue }
|
|
if ($section -eq 'inputs' -and $line -match '^\s{2}([A-Za-z0-9_]+):\s*$') { $inputKey = $Matches[1]; continue }
|
|
if ($section -eq 'project' -and $line -match '^\s*game_id:\s*(\S+)\s*$') {
|
|
$pins.GameId = $Matches[1]
|
|
} elseif ($section -eq 'inputs' -and $line -match '^\s*sha256:\s*([0-9a-fA-F]{64})\s*$') {
|
|
if ($inputKey -eq 'dol') { $pins.DolSha256 = $Matches[1].ToLowerInvariant() }
|
|
elseif ($inputKey -eq 'rel') { $pins.RelSha256 = $Matches[1].ToLowerInvariant() }
|
|
} elseif ($section -eq 'translation' -and $pins.EntryPoint -eq '' -and
|
|
$line -match '^\s*-\s*(0[xX][0-9a-fA-F]+)\s*$') {
|
|
$pins.EntryPoint = $Matches[1].ToLowerInvariant()
|
|
} elseif ($line -match '^\s*retro_wfc_payload:\s*(\S+)\s*$') {
|
|
$pins.RetroWfcPayloadUri = $Matches[1]
|
|
}
|
|
}
|
|
foreach ($name in @($pins.Keys)) {
|
|
if ([string]::IsNullOrWhiteSpace($pins[$name])) {
|
|
throw "$ProjectFile does not pin $name; the project file is not the shape this build expects."
|
|
}
|
|
}
|
|
return $pins
|
|
}
|
|
|
|
function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Description,
|
|
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '') {
|
|
<#
|
|
Runs a build tool and turns a non-zero exit code into a described failure. Start-Process -Wait
|
|
is deliberate: it waits for the whole process tree, since a .NET single-file bundle host may
|
|
hand off to an extracted child that PowerShell's call operator would not wait for. Start-Process
|
|
doesn't publish $LASTEXITCODE, so this sets it manually for callers that check it.
|
|
-StepId emits the machine-readable form the installer's progress bar consumes (BuildStepIds in
|
|
WiiCompiled.Setup/InstallProgress.cs); the human sentence stays on the same log line.
|
|
#>
|
|
if ($StepId) { Write-Host "${LogPrefix}:STEP:${StepId} $Description" }
|
|
else { Write-Host "${LogPrefix}: $Description" }
|
|
$quotedArguments = @($Arguments | ForEach-Object {
|
|
if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" }
|
|
'"' + $_ + '"'
|
|
})
|
|
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
|
|
-NoNewWindow -Wait -PassThru
|
|
$exitCode = $process.ExitCode
|
|
$global:LASTEXITCODE = $exitCode
|
|
if ($exitCode -ne 0) { throw "$Description failed with exit code $exitCode." }
|
|
}
|
|
|
|
function Get-MkwNativeFixedConfigureFlags {
|
|
<#
|
|
The path-independent half of the configure command line, identical on every machine, which is
|
|
what makes one precompiled aurora/third-party package valid for all users. Path-bearing
|
|
arguments are added separately by Get-MkwNativeConfigureArguments and excluded from the fingerprint.
|
|
#>
|
|
return @(
|
|
'-DCMAKE_BUILD_TYPE=Release',
|
|
'-DCMAKE_SYSTEM_PROCESSOR=x86_64',
|
|
'-DAURORA_DAWN_PROVIDER=package', '-DAURORA_SDL3_PROVIDER=vendor',
|
|
'-DCMAKE_POLICY_DEFAULT_CMP0168=NEW', '-DFETCHCONTENT_FULLY_DISCONNECTED=ON',
|
|
'-DAWK:FILEPATH='
|
|
)
|
|
}
|
|
|
|
function Get-MkwNativeFlagFingerprint {
|
|
<#
|
|
.SYNOPSIS
|
|
SHA-256 over the canonical flag list, used to reject a stale package.
|
|
#>
|
|
$canonical = (Get-MkwNativeFixedConfigureFlags) -join "`n"
|
|
$sha = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonical))
|
|
} finally { $sha.Dispose() }
|
|
return (($bytes | ForEach-Object { $_.ToString('x2') }) -join '')
|
|
}
|
|
|
|
function Get-MkwDawnRuntimeSha256([string]$DependenciesDirectory) {
|
|
<#
|
|
SHA-256 of the pinned Dawn runtime the prebuilt aurora must match. The precompiled aurora
|
|
archives are compiled and linked against the pinned dawn_prebuilt tree's headers/import
|
|
library, but the upstream release asset is mutable (the same version tag has already served
|
|
two different windows-amd64 archives), so the version string alone can't prove agreement.
|
|
A mismatch surfaces as an access violation at surface creation, not a link error, so the
|
|
digest is recorded at harvest and re-checked before use. Returns '' when the tree is absent.
|
|
#>
|
|
if ([string]::IsNullOrWhiteSpace($DependenciesDirectory)) { return '' }
|
|
$runtime = Join-Path $DependenciesDirectory 'dawn_prebuilt\bin\webgpu_dawn.dll'
|
|
if (-not (Test-Path -LiteralPath $runtime -PathType Leaf)) { return '' }
|
|
return Get-MkwFileSha256 $runtime
|
|
}
|
|
|
|
function Get-MkwThirdPartySourceFingerprint([string]$ThirdPartyDirectory) {
|
|
<#
|
|
SHA-256 over runtime/third_party, the vendored sources behind the prebuilt package. The
|
|
precompiled Crypto++ archive is linked while first-party TUs compile against the same headers,
|
|
so a workspace whose vendored tree moved on must not consume archives harvested from an older
|
|
one (same reasoning as the aurora fingerprint below). Unlike that scan this hashes hidden files
|
|
too, since Build-Installer stages this tree with Copy-Item -Force. Returns '' when absent.
|
|
#>
|
|
if ([string]::IsNullOrWhiteSpace($ThirdPartyDirectory)) { return '' }
|
|
if (-not (Test-Path -LiteralPath $ThirdPartyDirectory -PathType Container)) { return '' }
|
|
$root = [IO.Path]::GetFullPath($ThirdPartyDirectory).TrimEnd('\')
|
|
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
|
|
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
|
|
$sha = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$builder = [Text.StringBuilder]::new()
|
|
foreach ($file in $files) {
|
|
$relative = $file.FullName.Substring($root.Length + 1).Replace('\', '/')
|
|
$content = Get-MkwFileSha256 $file.FullName
|
|
[void]$builder.Append($relative).Append(' ').Append($content).Append("`n")
|
|
}
|
|
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($builder.ToString()))
|
|
} finally { $sha.Dispose() }
|
|
return (($bytes | ForEach-Object { $_.ToString('x2') }) -join '')
|
|
}
|
|
|
|
function Get-MkwAuroraSourceFingerprint([string]$AuroraDirectory) {
|
|
<#
|
|
SHA-256 over the aurora sources the precompiled archives were harvested from. The compiler,
|
|
flag, and Dawn digests only prove the package agrees with its target environment; none notice
|
|
aurora itself moving on, and since the runtime calls aurora headers directly, a package
|
|
harvested before an aurora change links old code against new call sites (undefined symbol at
|
|
best, silently stale renderer at worst). extern/ is excluded because the payload ships those
|
|
trees separately. Returns '' when the tree is absent.
|
|
#>
|
|
if ([string]::IsNullOrWhiteSpace($AuroraDirectory)) { return '' }
|
|
if (-not (Test-Path -LiteralPath $AuroraDirectory -PathType Container)) { return '' }
|
|
$root = [IO.Path]::GetFullPath($AuroraDirectory).TrimEnd('\')
|
|
$excludedPrefixes = @(
|
|
(Join-Path $root 'extern') + '\',
|
|
(Join-Path $root 'build') + '\'
|
|
)
|
|
# Hidden files are skipped deliberately: Build-Installer.ps1 stages this tree
|
|
# with Copy-Item -Recurse (no -Force), so they never reach the payload and
|
|
# hashing them would make the two sides disagree by construction.
|
|
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
|
|
Where-Object {
|
|
$full = $_.FullName
|
|
-not ($excludedPrefixes | Where-Object { $full.StartsWith($_, [StringComparison]::OrdinalIgnoreCase) })
|
|
} |
|
|
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
|
|
$sha = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$builder = [Text.StringBuilder]::new()
|
|
foreach ($file in $files) {
|
|
$relative = $file.FullName.Substring($root.Length + 1).Replace('\', '/')
|
|
$content = Get-MkwFileSha256 $file.FullName
|
|
[void]$builder.Append($relative).Append(' ').Append($content).Append("`n")
|
|
}
|
|
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($builder.ToString()))
|
|
} finally { $sha.Dispose() }
|
|
return (($bytes | ForEach-Object { $_.ToString('x2') }) -join '')
|
|
}
|
|
|
|
function ConvertTo-MkwCMakePath([string]$Path) {
|
|
return [IO.Path]::GetFullPath($Path).Replace('\', '/')
|
|
}
|
|
|
|
function Get-MkwNativeConfigureArguments {
|
|
<# The complete `cmake` configure argument list for a local runtime build. #>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)] [string]$SourceDirectory,
|
|
[Parameter(Mandatory)] [string]$BuildDirectory,
|
|
[Parameter(Mandatory)] [string]$Ninja,
|
|
[Parameter(Mandatory)] [string]$CCompiler,
|
|
[Parameter(Mandatory)] [string]$CxxCompiler,
|
|
[Parameter(Mandatory)] [string]$ResourceCompiler,
|
|
[string]$DependenciesDirectory,
|
|
[string]$NativePrebuiltDirectory,
|
|
[string[]]$AdditionalArguments = @()
|
|
)
|
|
|
|
$arguments = @(
|
|
'-S', (ConvertTo-MkwCMakePath $SourceDirectory),
|
|
'-B', (ConvertTo-MkwCMakePath $BuildDirectory),
|
|
'-G', 'Ninja',
|
|
"-DCMAKE_MAKE_PROGRAM=$(ConvertTo-MkwCMakePath $Ninja)",
|
|
"-DCMAKE_C_COMPILER=$(ConvertTo-MkwCMakePath $CCompiler)",
|
|
"-DCMAKE_CXX_COMPILER=$(ConvertTo-MkwCMakePath $CxxCompiler)",
|
|
"-DCMAKE_RC_COMPILER=$(ConvertTo-MkwCMakePath $ResourceCompiler)"
|
|
)
|
|
$arguments += Get-MkwNativeFixedConfigureFlags
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($DependenciesDirectory) -and
|
|
(Test-Path -LiteralPath $DependenciesDirectory -PathType Container)) {
|
|
$cppWinRt = Join-Path $DependenciesDirectory 'cppwinrt'
|
|
if (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf) {
|
|
$arguments += "-DMKW_CPPWINRT_INCLUDE_DIR=$(ConvertTo-MkwCMakePath $cppWinRt)"
|
|
}
|
|
foreach ($directory in Get-ChildItem -LiteralPath $DependenciesDirectory -Directory) {
|
|
# native_prebuilt holds compiled output, not a FetchContent source tree.
|
|
if ($directory.Name -eq 'native_prebuilt') { continue }
|
|
# FetchContent uppercases the declared name but preserves punctuation
|
|
# (for example abseil-cpp -> FETCHCONTENT_SOURCE_DIR_ABSEIL-CPP).
|
|
$name = $directory.Name.ToUpperInvariant()
|
|
$arguments += "-DFETCHCONTENT_SOURCE_DIR_$name=$(ConvertTo-MkwCMakePath $directory.FullName)"
|
|
}
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($NativePrebuiltDirectory)) {
|
|
$arguments += "-DMKW_NATIVE_PREBUILT_DIR=$(ConvertTo-MkwCMakePath $NativePrebuiltDirectory)"
|
|
}
|
|
|
|
$arguments += $AdditionalArguments
|
|
return $arguments
|
|
}
|