mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-12 01:38:50 -04:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4c53dad4b | |||
| e05e15bce5 | |||
| 9066cb3560 | |||
| 05d6a0b1ab | |||
| afb8f49866 | |||
| 58bfd32022 | |||
| a135beb201 | |||
| 88b990b060 | |||
| e0e362bd99 | |||
| 5654d8f21b | |||
| 730e3122d5 | |||
| f424536d3b | |||
| 2d9dc4e0f2 | |||
| 8e57cc162f | |||
| 1c0a3edee9 | |||
| d1d80613cc | |||
| 5d67b229f6 | |||
| 56db6ba641 | |||
| a67069afd3 | |||
| 009697fb97 | |||
| 3f7fed48c9 | |||
| 6eba523d70 | |||
| 8c6c177857 | |||
| be153e0fa0 | |||
| e34f055b3a | |||
| 602348f905 | |||
| e6f9b2197e | |||
| 65047bc7b7 | |||
| 989d5e00da | |||
| efc44b0482 | |||
| d3d0de62a6 | |||
| c6ef17378e | |||
| 38a11b7b2c | |||
| 7a13696a8b | |||
| 2bcfdca199 | |||
| f2165a5aa6 | |||
| ffc7244277 | |||
| dec9ed8df1 | |||
| 8dc9bb67b3 | |||
| ca7d126a13 | |||
| 5c76e2b0df | |||
| ae3096c89b | |||
| 6f4dd2f470 | |||
| 2b5b889249 | |||
| fbfb741b49 | |||
| e630132dd3 | |||
| a6f3720598 | |||
| abe7cd2b61 | |||
| 7aa04bfe80 | |||
| 1af7cd6d9b | |||
| d31942c356 | |||
| 13b6d0b447 | |||
| db576cd5cb | |||
| edacfcb8bc | |||
| 36faa76dc0 | |||
| 3fc1555871 | |||
| be691bd08d | |||
| 243eb86021 | |||
| 7b750e96ac | |||
| e4e388295b | |||
| 2c38db4174 | |||
| 1c7e4e79c5 | |||
| 9a22de1079 | |||
| c0359b05a7 | |||
| 6c1d4faaf7 | |||
| 3fb7373c4f | |||
| 8dd689c6d2 | |||
| 69be584cc5 | |||
| 8d569ef77b | |||
| 36cc6abe24 | |||
| 6ddc153e44 | |||
| 806f4127bb | |||
| fb9b101de7 | |||
| 64ea7b7401 | |||
| 4f4716be5a | |||
| cb812c1fd0 | |||
| c0ed2bfbeb | |||
| f09590aa5d | |||
| edc5fa1dd3 | |||
| c5db4d0e8e | |||
| e3c4028b50 | |||
| 48c4df342f | |||
| c93f82e938 | |||
| 9f799fe12b | |||
| 02df7ef9ee | |||
| 88ab029c74 | |||
| a0b54b874b | |||
| f63d0c84b5 | |||
| 6d836dab3e | |||
| 6bffedf028 | |||
| 09ca4e98f4 | |||
| 6dc4e59052 | |||
| 82b295b20c | |||
| eb1721fd72 | |||
| 129c714f02 | |||
| 2794024d3a | |||
| b5e5858e1d | |||
| 4897e7e27d | |||
| 7ebda6bf7f | |||
| 0403f176bd | |||
| 1912292c80 | |||
| e498e62622 | |||
| f73739f248 | |||
| 3389fe35fc | |||
| e11062a1ba | |||
| 1d1d064d37 | |||
| 9d1f3db1d5 | |||
| 251529d66e | |||
| 987b666296 | |||
| 260022b4ba | |||
| f1c2b60df1 | |||
| 5193dd9749 | |||
| ec9bd92415 | |||
| dbf6d05625 | |||
| 8ba44162fa | |||
| 9ee14e582d | |||
| 0c349a9296 | |||
| 21b57f08a4 | |||
| e146b10af8 | |||
| edd03f8991 | |||
| 9c4f4b738a |
@@ -14,18 +14,30 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
recompilation:
|
||||
name: Recompilation test
|
||||
uses: ./.github/workflows/recomp-test.yml
|
||||
|
||||
translator:
|
||||
name: Translator (build + test)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('translator/Translator.sln', '**/*.csproj', '**/*.props', '**/*.targets', '**/packages.lock.json', 'global.json', 'NuGet.config', 'nuget.config') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore translator/Translator.sln
|
||||
|
||||
@@ -34,3 +46,22 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
|
||||
|
||||
cpp:
|
||||
name: C++ runtime tests (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-14]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Configure
|
||||
run: cmake -S runtime/tests -B build/tests -DCMAKE_BUILD_TYPE=Release
|
||||
- name: Build
|
||||
run: cmake --build build/tests --config Release --parallel
|
||||
- name: Test
|
||||
run: ctest --test-dir build/tests -C Release --output-on-failure --no-tests=error
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
name: Package installers
|
||||
|
||||
# Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe /
|
||||
# WiiCompiled-Setup-x86_64.AppImage) via Launcher/Build-Installer.ps1 and
|
||||
# Launcher/build-appimage.sh respectively - the same scripts a maintainer runs by hand today to
|
||||
# produce a GitHub Release asset. This does NOT build the actual translated game executable:
|
||||
# that step requires the end user's own Mario Kart Wii dump (Assets/main.dol, Assets/StaticR.rel),
|
||||
# which is proprietary and not present in this repository or in CI.
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: package-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
recompilation:
|
||||
name: Recompilation test
|
||||
uses: ./.github/workflows/recomp-test.yml
|
||||
|
||||
linux-appimage:
|
||||
name: Linux (AppImage, ${{ matrix.arch }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-22.04
|
||||
arch: x86_64
|
||||
- runner: ubuntu-22.04-arm
|
||||
arch: aarch64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
# appimagetool ships as an AppImage itself and needs to self-mount via FUSE to run directly;
|
||||
# GitHub's ubuntu-latest runners don't have libfuse2 preinstalled.
|
||||
- name: Install dependencies (required by appimagetool and SDL3 build)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libfuse2 build-essential git make \
|
||||
pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \
|
||||
libaudio-dev libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
|
||||
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
|
||||
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
|
||||
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev libthai-dev libusb-1.0-0-dev \
|
||||
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
|
||||
|
||||
- name: Build AppImage
|
||||
run: bash Launcher/build-appimage.sh
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: WiiCompiled-Setup-linux-${{ matrix.arch }}
|
||||
path: Launcher/dist/WiiCompiled-Setup-${{ matrix.arch }}.AppImage
|
||||
if-no-files-found: error
|
||||
archive: false
|
||||
|
||||
windows-installer:
|
||||
name: Windows (installer exe)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Build installer
|
||||
shell: pwsh
|
||||
run: ./Launcher/Build-Installer.ps1
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: WiiCompiled-Setup-windows-x64
|
||||
path: Launcher/dist/WiiCompiled-Setup.exe
|
||||
if-no-files-found: error
|
||||
archive: false
|
||||
|
||||
# Publishes the packaged installers as a GitHub Release whenever a v* tag is pushed. Wheel Wizard
|
||||
# discovers updates from these releases, so the contract it relies on is enforced here: a full
|
||||
# (non-prerelease) release whose tag is v<semver>, carrying an asset named exactly
|
||||
# WiiCompiled-Setup.exe, produced by a setup host that reports that same version.
|
||||
release:
|
||||
name: Publish GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [linux-appimage, windows-installer, recompilation]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Wheel Wizard runs the downloaded setup with --version and refuses it when the reported
|
||||
# version differs from the release tag, so a tag that was pushed without bumping every pinned
|
||||
# version would ship an update nobody can install. Catch that before anything is published.
|
||||
- name: Verify the tag matches the pinned setup version
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="${TAG#v}"
|
||||
status=0
|
||||
check() {
|
||||
if ! grep -Fq "$2" "$1"; then
|
||||
echo "::error file=$1::expected '$2' for tag $TAG"
|
||||
status=1
|
||||
fi
|
||||
}
|
||||
check Launcher/WiiCompiled.Setup.Windows/Program.cs "public const string Version = \"$version\";"
|
||||
check Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj "<Version>$version</Version>"
|
||||
check Launcher/Build-Installer.ps1 "ProductVersion = '$version'"
|
||||
exit $status
|
||||
|
||||
# The build jobs upload with `archive: false`, which stores each installer as a raw file
|
||||
# rather than a zip (and names the artifact after the file). Only download-artifact v8 and
|
||||
# later understand such direct uploads; v7 tries to unzip everything and fails on them.
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Publish release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ls -lR artifacts
|
||||
assets=()
|
||||
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage; do
|
||||
found="$(find artifacts -type f -name "$name" | head -n 1)"
|
||||
[ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; }
|
||||
assets+=("$found")
|
||||
done
|
||||
# A re-run of a tag whose release already exists only refreshes the assets.
|
||||
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
gh release upload "$TAG" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber
|
||||
else
|
||||
gh release create "$TAG" "${assets[@]}" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--title "$TAG" \
|
||||
--verify-tag \
|
||||
--generate-notes
|
||||
fi
|
||||
@@ -0,0 +1,61 @@
|
||||
name: Synthetic recompilation
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
name: Windows runtime (synthetic DOL)
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
# Cache downloads only. Preparation still validates pins, and every run
|
||||
# compiles current Aurora, runtime, and generated sources from scratch.
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: Launcher/artifacts/downloads
|
||||
key: windows-recomp-downloads-${{ hashFiles('Launcher/Prepare-PortableTools.ps1', 'Launcher/Prepare-Dependencies.ps1') }}
|
||||
|
||||
# Install sccache.
|
||||
- name: Run sccache-action
|
||||
uses: mozilla/sccache-action@v0.0.11
|
||||
|
||||
# Tell CMake to use sccache and use GitHub's API.
|
||||
- name: Configure sccache environment
|
||||
shell: pwsh
|
||||
run: |
|
||||
"SCCACHE_GHA_ENABLED=true" | Add-Content -Path $env:GITHUB_ENV
|
||||
"ACTIONS_CACHE_SERVICE_V2=on" | Add-Content -Path $env:GITHUB_ENV
|
||||
"CMAKE_C_COMPILER_LAUNCHER=$env:SCCACHE_PATH" | Add-Content -Path $env:GITHUB_ENV
|
||||
"CMAKE_CXX_COMPILER_LAUNCHER=$env:SCCACHE_PATH" | Add-Content -Path $env:GITHUB_ENV
|
||||
|
||||
- name: Prepare the shipped Windows toolchain
|
||||
shell: pwsh
|
||||
run: ./Launcher/Prepare-PortableTools.ps1
|
||||
|
||||
- name: Prepare pinned native dependencies
|
||||
shell: pwsh
|
||||
run: ./Launcher/Prepare-Dependencies.ps1
|
||||
|
||||
# Expose cache token context to the build script.
|
||||
- name: Translate, compile the full runtime, and link
|
||||
shell: pwsh
|
||||
run: ./Launcher/Test-Recompilation.ps1 -Parallel 4
|
||||
|
||||
# Print cache results (even if the build fails)
|
||||
- name: Show sccache stats
|
||||
if: always()
|
||||
shell: pwsh
|
||||
run: sccache --show-stats
|
||||
@@ -25,6 +25,9 @@ Code.pul
|
||||
# Build output
|
||||
/build/
|
||||
/build-*/
|
||||
/native-build/
|
||||
/native-build-macos/
|
||||
/local-products/
|
||||
/dist/
|
||||
/out/
|
||||
[Bb]in/
|
||||
@@ -69,3 +72,5 @@ project.lock.json
|
||||
*.log
|
||||
output.txt
|
||||
|
||||
# Operating System
|
||||
.DS_Store
|
||||
|
||||
@@ -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))
|
||||
@@ -26,7 +21,7 @@ $dependencySources = [IO.Path]::GetFullPath((Join-Path $repoRoot $DependencySour
|
||||
$workRoot = Join-Path $PSScriptRoot 'artifacts\installer-build'
|
||||
$publish = Join-Path $workRoot 'publish'
|
||||
$payloadRoot = Join-Path $workRoot 'payload'
|
||||
$setupProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup\WiiCompiled.Setup.csproj'
|
||||
$setupProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Windows\WiiCompiled.Setup.Windows.csproj'
|
||||
$translatorProject = Join-Path $repoRoot 'translator\src\Translator.Cli\Translator.Cli.csproj'
|
||||
$projectFile = Join-Path $repoRoot 'projects\mkwii\recomp.yml'
|
||||
|
||||
@@ -91,20 +86,25 @@ 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'))) {
|
||||
# A PowerShell script, not a native executable - it never touches $LASTEXITCODE, and its own
|
||||
# $ErrorActionPreference = 'Stop' + throw already aborts this run on failure.
|
||||
& (Join-Path $PSScriptRoot 'Prepare-PortableTools.ps1') -Destination $portableTools
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Portable tool preparation failed.' }
|
||||
}
|
||||
Assert-File (Join-Path $portableTools 'CMake\bin\cmake.exe') 'Portable CMake'
|
||||
Assert-File (Join-Path $portableTools 'Ninja\ninja.exe') 'Portable Ninja'
|
||||
Assert-Directory $dependencySources 'Pinned offline dependency sources'
|
||||
# native_prebuilt carries the aurora/third-party archives the user no longer has
|
||||
# to compile (launcher/Prepare-NativePrebuilt.ps1).
|
||||
# Kept in step with InstalledLayout.DependencyNames by Test-PinnedFacts.ps1: the installed host
|
||||
# refuses to call a toolkit complete unless every one of these directories is present.
|
||||
$requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','native_prebuilt','png','SDL','sqlite3','tracy','xxhash','zlib','zstd')
|
||||
$requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','libusb','native_prebuilt','png','SDL','sqlite3','tracy','xxhash','zlib','zstd')
|
||||
$missingSources = @($requiredDependencies | Where-Object { $_ -ne 'native_prebuilt' } |
|
||||
Where-Object { -not (Test-Path -LiteralPath (Join-Path $dependencySources $_) -PathType Container) })
|
||||
if ($missingSources.Count -gt 0) {
|
||||
& (Join-Path $PSScriptRoot 'Prepare-Dependencies.ps1') -Destination $dependencySources
|
||||
}
|
||||
Assert-Directory $dependencySources 'Pinned offline dependency sources'
|
||||
|
||||
# The precompiled archives are only interchangeable with what the user's machine
|
||||
# compiles if both came from this toolchain and this flag set, so a stale package
|
||||
@@ -169,6 +169,14 @@ $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.Setup.Common.Cli helper (also used by build-appimage.sh on
|
||||
# Linux) rather than a separate download/version-pin copy here: it downloads and caches the same way
|
||||
# NodToolProvider.cs always does (Launcher/artifacts/nodtool.exe)
|
||||
$nodToolCliProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Common.Cli'
|
||||
$nodTool = (& dotnet run --project $nodToolCliProject -c Release -- --workspace $repoRoot | Select-Object -Last 1)
|
||||
if ($LASTEXITCODE -ne 0) { throw "nodtool resolution failed with exit code $LASTEXITCODE." }
|
||||
Assert-File $nodTool 'Resolved nodtool'
|
||||
|
||||
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 +199,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 +233,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
|
||||
@@ -253,7 +281,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
|
||||
|
||||
$manifest = [ordered]@{
|
||||
SchemaVersion = 2
|
||||
ProductVersion = '0.2.22'
|
||||
ProductVersion = '0.2.31'
|
||||
ExpectedGameId = $pins.GameId
|
||||
ExpectedDolSha256 = $pins.DolSha256
|
||||
ExpectedRelSha256 = $pins.RelSha256
|
||||
@@ -269,11 +297,12 @@ $manifest = [ordered]@{
|
||||
$manifest | ConvertTo-Json | Set-Content (Join-Path $payloadRoot 'payload-manifest.json') -Encoding UTF8
|
||||
|
||||
Write-Host '[4/6] Enforcing the copyright and generated-code boundary...'
|
||||
# Both audits are PowerShell scripts that throw directly on failure (Test-PayloadBoundary.ps1
|
||||
# never invokes a native command at all, so it never touches $LASTEXITCODE); there is no exit
|
||||
# code to check here, and doing so risks reading a stale value from an unrelated earlier command.
|
||||
& (Join-Path $PSScriptRoot 'Test-PayloadBoundary.ps1') -PayloadRoot $payloadRoot
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Payload boundary audit failed.' }
|
||||
& (Join-Path $PSScriptRoot 'Test-NativeDependencies.ps1') -PayloadRoot $payloadRoot -SetupHost $setupHost `
|
||||
-LlvmReadobjPath (Join-Path $portableTools 'llvm-mingw\bin\llvm-readobj.exe')
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Native dependency audit failed.' }
|
||||
|
||||
Write-Host '[5/6] Creating the canonical installer payload...'
|
||||
$payloadZip = Join-Path $workRoot 'payload.zip'
|
||||
|
||||
@@ -151,9 +151,10 @@ if ($Profile -ne 'both' -and -not [string]::IsNullOrWhiteSpace($BaseOutputDirect
|
||||
throw '-BaseOutputDirectory is valid only with -Profile both.'
|
||||
}
|
||||
$translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe'
|
||||
$cmake = Join-Path $Toolkit 'CMake\bin\cmake.exe'
|
||||
$ninja = Join-Path $Toolkit 'Ninja\ninja.exe'
|
||||
$toolchainBin = Join-Path $Toolkit 'llvm-mingw\bin'
|
||||
$toolchain = Get-MkwShellSafeToolchainRoot $Toolkit
|
||||
$cmake = Join-Path $toolchain 'CMake\bin\cmake.exe'
|
||||
$ninja = Join-Path $toolchain 'Ninja\ninja.exe'
|
||||
$toolchainBin = Join-Path $toolchain 'llvm-mingw\bin'
|
||||
$cc = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang.exe'
|
||||
$cxx = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang++.exe'
|
||||
$windres = Join-Path $toolchainBin 'x86_64-w64-mingw32-windres.exe'
|
||||
@@ -203,7 +204,7 @@ if ($Parallel -gt 0) {
|
||||
$oldPath = $env:PATH
|
||||
$oldDotnet = $env:DOTNET_ROOT
|
||||
try {
|
||||
$env:PATH = Get-MkwToolchainPath $Toolkit
|
||||
$env:PATH = Get-MkwToolchainPath $toolchain
|
||||
Remove-Item Env:DOTNET_ROOT -ErrorAction SilentlyContinue
|
||||
Push-Location $Workspace
|
||||
try {
|
||||
|
||||
@@ -40,6 +40,43 @@ function Get-MkwToolchainPath([string]$ToolchainRoot) {
|
||||
) -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
|
||||
@@ -80,12 +117,13 @@ function Get-MkwProjectPins([string]$ProjectFile) {
|
||||
}
|
||||
|
||||
function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Description,
|
||||
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '') {
|
||||
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '', [bool]$WaitForProcessTree = $true) {
|
||||
<#
|
||||
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.
|
||||
Runs a build tool and turns a non-zero exit code into a described failure. By default,
|
||||
Start-Process -Wait 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. Callers that
|
||||
need to avoid waiting on unrelated descendants can opt into the call-operator path.
|
||||
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.
|
||||
#>
|
||||
@@ -95,9 +133,14 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
|
||||
if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" }
|
||||
'"' + $_ + '"'
|
||||
})
|
||||
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
|
||||
-NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
if ($WaitForProcessTree) {
|
||||
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
|
||||
-NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
} else {
|
||||
& $FilePath @Arguments
|
||||
$exitCode = $LASTEXITCODE
|
||||
}
|
||||
$global:LASTEXITCODE = $exitCode
|
||||
if ($exitCode -ne 0) { throw "$Description failed with exit code $exitCode." }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
# Populates Launcher/artifacts/dependencies: the pinned, unpatched upstream source trees (plus the
|
||||
# prebuilt Dawn package and generated C++/WinRT headers) that Build-Installer.ps1 ships inside the
|
||||
# installer so the user's build runs with FETCHCONTENT_FULLY_DISCONNECTED=ON. Every pin is the one
|
||||
# aurora-main's own CMake declares; the Pin fields below are asserted against those files so the
|
||||
# two cannot drift apart silently. native_prebuilt is not fetched here - Prepare-NativePrebuilt.ps1
|
||||
# compiles it from these trees.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Destination,
|
||||
# Windows metadata source for cppwinrt.exe: 'local' (this machine's WinMetadata), 'sdk', or an
|
||||
# installed SDK version such as 10.0.26100.0.
|
||||
[string]$CppWinRtInput = 'local'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 3.0
|
||||
|
||||
$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
$auroraCMake = Join-Path $repoRoot 'aurora-main\CMakeLists.txt'
|
||||
$auroraExtern = Join-Path $repoRoot 'aurora-main\extern\CMakeLists.txt'
|
||||
$auroraDawn = Join-Path $repoRoot 'aurora-main\cmake\AuroraDawnProvider.cmake'
|
||||
$auroraLibUsb = Join-Path $repoRoot 'aurora-main\cmake\AuroraLibUSB.cmake'
|
||||
$auroraSdl = Join-Path $repoRoot 'aurora-main\cmake\AuroraSDL3Provider.cmake'
|
||||
|
||||
# Name = directory under the destination; FetchContent maps it back through
|
||||
# FETCHCONTENT_SOURCE_DIR_<UPPERCASE NAME> (NativeBuildFlags.ps1), so the names are the declared
|
||||
# FetchContent names, not the upstream project names.
|
||||
$packages = @(
|
||||
[pscustomobject]@{
|
||||
Name = 'SDL'; File = 'SDL3-3.4.4.tar.gz'
|
||||
Uris = @('https://github.com/libsdl-org/SDL/releases/download/release-3.4.4/SDL3-3.4.4.tar.gz')
|
||||
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_SDL3_VERSION "3.4.4"' },
|
||||
@{ File = $auroraSdl; Text = 'releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'abseil-cpp'; File = 'abseil-cpp-20240722.0.tar.gz'
|
||||
Uris = @('https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'dawn_prebuilt'; File = 'dawn-v20260603.191052-windows-amd64.tar.gz'
|
||||
Uris = @('https://github.com/encounter/dawn-build/releases/download/v20260603.191052/dawn-windows-amd64.tar.gz')
|
||||
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_DAWN_VERSION "v20260603.191052"' },
|
||||
@{ File = $auroraDawn; Text = 'SHA256=7785373d569b3b0237918ec9c523239f7d0667857c5ea8242e3cdfde95e6aeab' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'fmt'; File = 'fmt-11.1.4.tar.gz'
|
||||
Uris = @('https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'freetype'; File = 'freetype-2.14.3.tar.gz'
|
||||
Uris = @('https://files.twilitrealm.dev/freetype-2.14.3.tar.gz',
|
||||
'https://download.savannah.gnu.org/releases/freetype/freetype-2.14.3.tar.gz',
|
||||
'https://downloads.sourceforge.net/project/freetype/freetype2/2.14.3/freetype-2.14.3.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://files.twilitrealm.dev/freetype-2.14.3.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'imgui'; File = 'imgui-1.91.9b-docking.tar.gz'
|
||||
Uris = @('https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'libusb'; File = 'libusb-1.0.30.tar.bz2'
|
||||
Uris = @('https://github.com/libusb/libusb/releases/download/v1.0.30/libusb-1.0.30.tar.bz2')
|
||||
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_LIBUSB_VERSION "1.0.30"' },
|
||||
@{ File = $auroraLibUsb; Text = 'releases/download/v${AURORA_LIBUSB_VERSION}/libusb-${AURORA_LIBUSB_VERSION}.tar.bz2' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'png'; File = 'libpng-1.6.58.tar.gz'
|
||||
Uris = @('https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'sqlite3'; File = 'sqlite-amalgamation-3510300.zip'
|
||||
Uris = @('https://sqlite.org/2026/sqlite-amalgamation-3510300.zip')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://sqlite.org/2026/sqlite-amalgamation-3510300.zip' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'tracy'; File = 'tracy-a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz'
|
||||
Uris = @('https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'xxhash'; File = 'xxHash-0.8.3.tar.gz'
|
||||
Uris = @('https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.3.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.3.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'zlib'; File = 'zlib-1.3.2.tar.gz'
|
||||
Uris = @('https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'zstd'; File = 'zstd-1.5.7.tar.gz'
|
||||
Uris = @('https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz' })
|
||||
}
|
||||
)
|
||||
|
||||
# The C++/WinRT compiler (NuGet package = zip) that generates the projection headers.
|
||||
$cppWinRtTool = [pscustomobject]@{
|
||||
Name = 'cppwinrt-tool'; File = 'Microsoft.Windows.CppWinRT.3.0.260818.1.nupkg'
|
||||
Uris = @('https://www.nuget.org/api/v2/package/Microsoft.Windows.CppWinRT/3.0.260818.1')
|
||||
Pins = @()
|
||||
}
|
||||
|
||||
if (-not $Destination) { $Destination = Join-Path $PSScriptRoot 'artifacts\dependencies' }
|
||||
$Destination = [IO.Path]::GetFullPath($Destination)
|
||||
$downloads = Join-Path $PSScriptRoot 'artifacts\downloads'
|
||||
$tar = Join-Path $env:SystemRoot 'System32\tar.exe'
|
||||
if (-not (Test-Path -LiteralPath $tar -PathType Leaf)) { throw "Windows archive tool is missing: $tar" }
|
||||
[IO.Directory]::CreateDirectory($Destination) | Out-Null
|
||||
[IO.Directory]::CreateDirectory($downloads) | Out-Null
|
||||
|
||||
function Assert-Pinned($Package) {
|
||||
foreach ($pin in $Package.Pins) {
|
||||
if (-not (Test-Path -LiteralPath $pin.File -PathType Leaf)) { throw "Pin source is missing: $($pin.File)" }
|
||||
$content = [IO.File]::ReadAllText($pin.File)
|
||||
if (-not $content.Contains($pin.Text)) {
|
||||
throw "$($Package.Name) is pinned to '$($pin.Text)' here but $($pin.File) no longer declares it; update both."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-Archive($Package) {
|
||||
$archive = Join-Path $downloads $Package.File
|
||||
if (Test-Path -LiteralPath $archive -PathType Leaf) { return $archive }
|
||||
$temporary = $archive + '.partial'
|
||||
$failures = @()
|
||||
foreach ($uri in $Package.Uris) {
|
||||
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
|
||||
Write-Host "Downloading $($Package.Name) from $uri..."
|
||||
try {
|
||||
Invoke-WebRequest -UseBasicParsing -Uri $uri -OutFile $temporary
|
||||
} catch {
|
||||
$failures += "$uri ($($_.Exception.Message))"
|
||||
continue
|
||||
}
|
||||
Move-Item -LiteralPath $temporary -Destination $archive -Force
|
||||
return $archive
|
||||
}
|
||||
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
|
||||
throw "$($Package.Name) could not be fetched: $($failures -join '; ')"
|
||||
}
|
||||
|
||||
function Expand-Package([string]$Archive, [string]$Target) {
|
||||
# Release archives wrap everything in one versioned directory (SDL3-3.4.4/, tracy-<sha>/) while
|
||||
# dawn-build's package is flat; either way the tree lands directly under the target.
|
||||
$extract = Join-Path (Split-Path -Parent $Target) ('.extract-' + (Split-Path -Leaf $Target))
|
||||
if (Test-Path -LiteralPath $extract) { Remove-Item -LiteralPath $extract -Recurse -Force }
|
||||
[IO.Directory]::CreateDirectory($extract) | Out-Null
|
||||
# Symbolic links need a privilege Windows tar usually lacks; the only ones in these archives
|
||||
# are zstd test aliases, and none are build inputs.
|
||||
$listing = & $tar -tvf $Archive
|
||||
if ($LASTEXITCODE -ne 0) { throw "Listing $Archive failed with exit code $LASTEXITCODE." }
|
||||
$excludes = @($listing | ForEach-Object { if ($_ -match '^l.*\s(\S+)\s->\s') { '--exclude=' + $Matches[1] } })
|
||||
& $tar -xf $Archive -C $extract @excludes
|
||||
if ($LASTEXITCODE -ne 0) { throw "Extracting $Archive failed with exit code $LASTEXITCODE." }
|
||||
$entries = @(Get-ChildItem -LiteralPath $extract -Force)
|
||||
$source = $extract
|
||||
if ($entries.Count -eq 1 -and $entries[0].PSIsContainer) { $source = $entries[0].FullName }
|
||||
if (Test-Path -LiteralPath $Target) { Remove-Item -LiteralPath $Target -Recurse -Force }
|
||||
Move-Item -LiteralPath $source -Destination $Target
|
||||
if (Test-Path -LiteralPath $extract) { Remove-Item -LiteralPath $extract -Recurse -Force }
|
||||
}
|
||||
|
||||
foreach ($package in $packages) {
|
||||
Assert-Pinned $package
|
||||
$target = Join-Path $Destination $package.Name
|
||||
if (Test-Path -LiteralPath $target -PathType Container) { continue }
|
||||
Expand-Package (Get-Archive $package) $target
|
||||
Write-Host "Prepared $($package.Name)"
|
||||
}
|
||||
|
||||
$cppWinRt = Join-Path $Destination 'cppwinrt'
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf)) {
|
||||
$toolRoot = Join-Path $PSScriptRoot 'artifacts\cppwinrt-tool'
|
||||
$compiler = Join-Path $toolRoot 'bin\cppwinrt.exe'
|
||||
if (-not (Test-Path -LiteralPath $compiler -PathType Leaf)) {
|
||||
Expand-Package (Get-Archive $cppWinRtTool) $toolRoot
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $compiler -PathType Leaf)) { throw "cppwinrt.exe is missing: $compiler" }
|
||||
if (Test-Path -LiteralPath $cppWinRt) { Remove-Item -LiteralPath $cppWinRt -Recurse -Force }
|
||||
[IO.Directory]::CreateDirectory($cppWinRt) | Out-Null
|
||||
Write-Host "Generating C++/WinRT headers (-input $CppWinRtInput)..."
|
||||
& $compiler -input $CppWinRtInput -output $cppWinRt
|
||||
if ($LASTEXITCODE -ne 0) { throw "cppwinrt.exe failed with exit code $LASTEXITCODE." }
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf)) {
|
||||
throw "cppwinrt.exe produced no winrt\base.h under $cppWinRt"
|
||||
}
|
||||
Copy-Item -LiteralPath (Join-Path $toolRoot 'LICENSE') -Destination (Join-Path $cppWinRt 'LICENSE.txt')
|
||||
Write-Host 'Prepared cppwinrt'
|
||||
}
|
||||
|
||||
Write-Host "Pinned dependency sources ready: $Destination"
|
||||
@@ -61,8 +61,6 @@ Assert-File $windres 'Portable resource compiler'
|
||||
Assert-File $clangBinary 'Portable clang driver binary'
|
||||
Assert-Directory $dependencies 'Pinned offline dependency sources'
|
||||
Assert-Directory $auroraSource 'aurora-main source tree'
|
||||
Assert-File (Join-Path $repoRoot 'generated\build_shards\shards.cmake') `
|
||||
'Translator shard manifest (run the developer build once so runtime/ can configure)'
|
||||
|
||||
if ($Parallel -le 0) { $Parallel = [Environment]::ProcessorCount }
|
||||
|
||||
|
||||
Executable
+590
@@ -0,0 +1,590 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the redistributable precompiled aurora + third-party package for native Linux: aurora
|
||||
# (~43% of local build CPU time per Prepare-NativePrebuilt.ps1) and vendored Crypto++ are identical
|
||||
# for every user under the pinned toolchain prepare-portable-tools.sh bundles, so this configures
|
||||
# runtime/ against that toolchain, builds just that closure, and harvests the archives plus a
|
||||
# generated CMake description into an output package - the Linux counterpart to
|
||||
# Launcher/Prepare-NativePrebuilt.ps1, consumed by the same platform-agnostic
|
||||
# runtime/cmake/NativePrebuilt.cmake either script's package works with unmodified.
|
||||
#
|
||||
# Not a byte-for-byte port of the Windows script: Linux needs no offline pinned dependency cache.
|
||||
# Every FetchContent dependency in aurora-main/extern/CMakeLists.txt is a fixed-version URL already
|
||||
# (verified directly), and aurora-main/CMakeLists.txt's own _default_linkage is "static" on any
|
||||
# non-Windows platform. Windows pins Launcher/artifacts/dependencies because its installer ships
|
||||
# pre-fetched sources to end users for a fully offline build; this harvest only ever runs on a
|
||||
# maintainer's own machine (network access needed once, here, not shipped to anyone), so a plain
|
||||
# networked configure is exactly as reproducible - what actually makes the harvested archives safe
|
||||
# to link into any consumer's own build is the pinned compiler below, not an offline source cache.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/.." && pwd)
|
||||
|
||||
arch=""
|
||||
output_dir=""
|
||||
stage_dir="$workspace/build/native-prebuilt-stage"
|
||||
keep_stage=0
|
||||
reuse_stage=0
|
||||
parallel=0
|
||||
print_fingerprint_only=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Prepare-NativePrebuilt.sh --arch {x86_64|aarch64} [options]
|
||||
|
||||
--arch ARCH Target architecture; selects the matching bundled toolchain (required)
|
||||
--output-dir DIR Where the package is written (default: Launcher/artifacts/native-prebuilt-ARCH)
|
||||
--stage-dir DIR Staging build directory (default: build/native-prebuilt-stage)
|
||||
--keep-stage Do not delete the staging build directory afterward
|
||||
--reuse-stage Reuse an existing staging build directory (maintainer iteration aid: a
|
||||
re-harvest does not recompile aurora from scratch)
|
||||
--parallel N Ninja build parallelism (default: nproc)
|
||||
--print-fingerprint-only Print the four provenance inputs (compiler_sha256, flag_fingerprint,
|
||||
aurora_fingerprint, third_party_fingerprint) as "key=value" lines and
|
||||
exit, without configuring/building/harvesting anything - lets a caller
|
||||
(build-appimage.sh) decide whether an existing package is still current
|
||||
without paying for a full aurora rebuild just to find out.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--arch) arch=$2; shift 2 ;;
|
||||
--output-dir) output_dir=$2; shift 2 ;;
|
||||
--stage-dir) stage_dir=$2; shift 2 ;;
|
||||
--keep-stage) keep_stage=1; shift ;;
|
||||
--reuse-stage) reuse_stage=1; shift ;;
|
||||
--parallel) parallel=$2; shift 2 ;;
|
||||
--print-fingerprint-only) print_fingerprint_only=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Prepare-NativePrebuilt.sh: unknown argument: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$arch" in
|
||||
x86_64|aarch64) ;;
|
||||
*) echo "Prepare-NativePrebuilt.sh: --arch must be x86_64 or aarch64" >&2; usage; exit 1 ;;
|
||||
esac
|
||||
|
||||
fail() { echo "Prepare-NativePrebuilt.sh: error: $*" >&2; exit 1; }
|
||||
assert_file() { [[ -f "$1" ]] || fail "$2 is missing: $1"; }
|
||||
assert_dir() { [[ -d "$1" ]] || fail "$2 is missing: $1"; }
|
||||
sha256_of() { sha256sum "$1" | awk '{print $1}'; }
|
||||
normalize() { readlink -f "$1"; }
|
||||
|
||||
[[ -n "$output_dir" ]] || output_dir="$script_dir/artifacts/native-prebuilt-$arch"
|
||||
[[ "$stage_dir" = /* ]] || stage_dir="$workspace/$stage_dir"
|
||||
|
||||
toolchain_dir="$script_dir/artifacts/portable-tools/toolchain-$arch"
|
||||
cc="$toolchain_dir/bin/clang"
|
||||
cxx="$toolchain_dir/bin/clang++"
|
||||
cmake_bin="$toolchain_dir/bin/cmake"
|
||||
ninja_bin="$toolchain_dir/bin/ninja"
|
||||
runtime_source="$workspace/runtime"
|
||||
aurora_source="$workspace/aurora-main"
|
||||
|
||||
assert_file "$cmake_bin" "Portable CMake (run prepare-portable-tools.sh --arch $arch first)"
|
||||
assert_file "$ninja_bin" "Portable Ninja"
|
||||
assert_file "$cc" "Portable C compiler"
|
||||
assert_file "$cxx" "Portable C++ compiler"
|
||||
assert_dir "$aurora_source" "aurora-main source tree"
|
||||
clang_binary=$(normalize "$toolchain_dir/bin/clang-22")
|
||||
assert_file "$clang_binary" "Portable clang driver binary"
|
||||
|
||||
(( parallel > 0 )) || parallel=$(nproc)
|
||||
|
||||
fingerprint_tree() {
|
||||
# $1 = root dir, remaining args = top-level subdirectory names to exclude
|
||||
local root=$1; shift
|
||||
root=$(normalize "$root")
|
||||
[[ -d "$root" ]] || return 0
|
||||
local find_args=("$root")
|
||||
local ex
|
||||
for ex in "$@"; do find_args+=(-path "$root/$ex" -prune -o); done
|
||||
find_args+=(-type f -print)
|
||||
find "${find_args[@]}" | LC_ALL=C sort | while IFS= read -r abs; do
|
||||
printf '%s %s\n' "${abs#$root/}" "$(sha256_of "$abs")"
|
||||
done | sha256sum | awk '{print $1}'
|
||||
}
|
||||
|
||||
# The fixed (path-independent) half of the configure command line - identical for every
|
||||
# harvest, which is what lets one package be valid regardless of where it was built.
|
||||
# -DBUILD_SHARED_LIBS=OFF: aurora-main/extern/CMakeLists.txt derives its own _USE_SHARED from
|
||||
# whether BUILD_SHARED_LIBS is *defined at all* (not its value), so leaving it unset would default
|
||||
# zlib/libpng to shared. -DAURORA_SDL3_PROVIDER=vendor: without it, "auto" resolves to "system" on
|
||||
# any machine that happens to have an SDL3 dev package installed. -DCMAKE_DISABLE_FIND_PACKAGE_*:
|
||||
# absl/PNG/Freetype each call find_package() unconditionally, with no provider flag to gate them
|
||||
# (unlike SDL3/Dawn) - verified directly that this machine's system libpng-dev/freetype-dev get
|
||||
# linked in shared instead of the pinned vendored source otherwise. Disabling find_package for
|
||||
# these three forces the same FetchContent-vendored, statically-built result regardless of what a
|
||||
# given maintainer's machine happens to have installed. ZLIB is deliberately NOT disabled the same
|
||||
# way: libpng's own vendored CMakeLists.txt calls find_package(ZLIB REQUIRED) internally even when
|
||||
# zlib came from aurora's own FetchContent (extern/CMakeLists.txt writes a redirect config for
|
||||
# exactly this), and CMAKE_DISABLE_FIND_PACKAGE_ZLIB errors out on any REQUIRED call site outright
|
||||
# (verified directly) - it cannot be scoped to only aurora's own initial, non-required check.
|
||||
# Dawn stays a prebuilt package regardless (Linux x86_64/aarch64 always auto-resolve to "package" -
|
||||
# see AuroraDawnProvider.cmake).
|
||||
fixed_configure_flags=(
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DBUILD_SHARED_LIBS=OFF
|
||||
-DAURORA_SDL3_PROVIDER=vendor
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_absl=ON
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_PNG=ON
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_Freetype=ON
|
||||
# Freetype's own vendored CMakeLists.txt separately probes for system BZip2 (optional
|
||||
# bzip2-compressed-font support aurora-main never asked for) regardless of the Freetype
|
||||
# find_package disable above, since that only stops aurora's own outer find_package(Freetype)
|
||||
# from picking up a system Freetype - it does not reach into the FetchContent-built copy's own
|
||||
# internal find_package(BZip2) call. FT_DISABLE_BZIP2 is Freetype's own documented flag for
|
||||
# exactly this (verified in its CMakeLists.txt), unlike the ZLIB/libpng situation where no such
|
||||
# source-level flag exists.
|
||||
-DFT_DISABLE_BZIP2=ON
|
||||
-DCMAKE_POLICY_DEFAULT_CMP0168=NEW
|
||||
)
|
||||
flag_fingerprint=$(printf '%s\n' "${fixed_configure_flags[@]}" | sha256sum | awk '{print $1}')
|
||||
|
||||
# extern/ is excluded because the payload ships that tree separately (aurora-main/extern is bundled
|
||||
# whole by build-appimage.sh); build/ is a plain developer build directory.
|
||||
aurora_fingerprint=$(fingerprint_tree "$aurora_source" extern build)
|
||||
[[ -n "$aurora_fingerprint" ]] || fail "The aurora source tree could not be fingerprinted: $aurora_source"
|
||||
|
||||
# The harvested Crypto++ archive is consumed against this tree's headers, so it is fingerprinted
|
||||
# for the same reason as aurora above. No exclusions: unlike aurora's extern/, nothing under
|
||||
# runtime/third_party is shipped separately.
|
||||
third_party_fingerprint=$(fingerprint_tree "$runtime_source/third_party")
|
||||
[[ -n "$third_party_fingerprint" ]] || fail "The vendored third-party tree could not be fingerprinted: $runtime_source/third_party"
|
||||
|
||||
compiler_sha256=$(sha256_of "$clang_binary")
|
||||
|
||||
if [[ "$print_fingerprint_only" -eq 1 ]]; then
|
||||
printf 'compiler_sha256=%s\n' "$compiler_sha256"
|
||||
printf 'flag_fingerprint=%s\n' "$flag_fingerprint"
|
||||
printf 'aurora_fingerprint=%s\n' "$aurora_fingerprint"
|
||||
printf 'third_party_fingerprint=%s\n' "$third_party_fingerprint"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The package must never contain a stale mixture of two builds.
|
||||
rm -rf "$output_dir"
|
||||
mkdir -p "$output_dir/lib" "$output_dir/bin" "$output_dir/include"
|
||||
|
||||
# The staging build has to be configured without the package present, otherwise the runtime would
|
||||
# consume the very package this script is producing.
|
||||
if [[ "$reuse_stage" -eq 0 ]]; then rm -rf "$stage_dir"; fi
|
||||
export_dir="$stage_dir/export"
|
||||
mkdir -p "$export_dir"
|
||||
|
||||
# fixed_configure_flags/flag_fingerprint were already computed above (needed before the
|
||||
# --print-fingerprint-only early exit).
|
||||
echo "Prepare-NativePrebuilt.sh: configuring the aurora/third-party staging build..."
|
||||
"$cmake_bin" -S "$runtime_source" -B "$stage_dir" -G Ninja \
|
||||
"${fixed_configure_flags[@]}" \
|
||||
-DCMAKE_C_COMPILER="$cc" -DCMAKE_CXX_COMPILER="$cxx" \
|
||||
-DCMAKE_MAKE_PROGRAM="$ninja_bin" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" \
|
||||
-DMKW_NATIVE_PREBUILT_EXPORT_DIR="$export_dir"
|
||||
|
||||
# mkw_np_probe links exactly what mkw_runtime_common and the public products link, so building it
|
||||
# compiles the whole redistributable closure and nothing else - and a successful link proves the
|
||||
# harvested archives are complete.
|
||||
echo "Prepare-NativePrebuilt.sh: building the aurora/third-party closure..."
|
||||
"$cmake_bin" --build "$stage_dir" --target mkw_np_probe --parallel "$parallel"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Read the description CMake wrote, plus the resolved command lines Ninja holds.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
meta_path="$export_dir/meta.txt"
|
||||
assert_file "$meta_path" "Native prebuilt export metadata"
|
||||
get_meta() { awk -F= -v k="$1" '$0 ~ "^" k "=" { sub(/^[^=]*=/, ""); print; exit }' "$meta_path"; }
|
||||
|
||||
build_ninja="$stage_dir/build.ninja"
|
||||
assert_file "$build_ninja" "Generated build.ninja"
|
||||
|
||||
extract_ninja_vars() {
|
||||
# $1 = ninja file, $2 = exact rule name. Prints "KEY\tVALUE" for every " KEY = VALUE" line
|
||||
# following the first "build ...: RULE ..." statement that uses that rule.
|
||||
awk -v rule="$2" '
|
||||
BEGIN { in_stmt = 0 }
|
||||
/^build / {
|
||||
if (in_stmt) exit
|
||||
colon = index($0, ": ")
|
||||
if (colon == 0) next
|
||||
rest = substr($0, colon + 2)
|
||||
sp = index(rest, " ")
|
||||
stmt_rule = (sp > 0) ? substr(rest, 1, sp - 1) : rest
|
||||
if (stmt_rule == rule) in_stmt = 1
|
||||
next
|
||||
}
|
||||
in_stmt {
|
||||
if (substr($0, 1, 2) != " ") exit
|
||||
line = substr($0, 3)
|
||||
eq = index(line, " = ")
|
||||
if (eq == 0) exit
|
||||
print substr(line, 1, eq - 1) "\t" substr(line, eq + 3)
|
||||
}
|
||||
' "$1"
|
||||
}
|
||||
|
||||
declare -A probe_compile=() control_compile=() probe_link=() control_link=()
|
||||
load_vars() {
|
||||
local -n dest=$1
|
||||
local k v
|
||||
while IFS=$'\t' read -r k v; do dest["$k"]=$v; done < <(extract_ninja_vars "$build_ninja" "$2")
|
||||
}
|
||||
load_vars probe_compile "CXX_COMPILER__mkw_np_probe_unscanned_Release"
|
||||
load_vars control_compile "CXX_COMPILER__mkw_np_probe_control_unscanned_Release"
|
||||
load_vars probe_link "CXX_EXECUTABLE_LINKER__mkw_np_probe_Release"
|
||||
load_vars control_link "CXX_EXECUTABLE_LINKER__mkw_np_probe_control_Release"
|
||||
[[ -n "${probe_compile[INCLUDES]+x}" ]] || fail "No Ninja statement used rule CXX_COMPILER__mkw_np_probe_unscanned_Release."
|
||||
[[ -n "${probe_link[LINK_LIBRARIES]+x}" ]] || fail "No Ninja statement used rule CXX_EXECUTABLE_LINKER__mkw_np_probe_Release."
|
||||
|
||||
split_list() {
|
||||
# $1 = string value, $2 = name of the array to fill (nameref)
|
||||
local -n out=$2
|
||||
out=()
|
||||
[[ -n "$1" ]] || return 0
|
||||
read -ra out <<< "$1"
|
||||
}
|
||||
|
||||
subtract_multiset() {
|
||||
# $1 = "all" array name, $2 = "remove" array name, $3 = result array name (all namerefs)
|
||||
local -n all_ref=$1 remove_ref=$2 result_ref=$3
|
||||
local -A pending=()
|
||||
local item
|
||||
for item in "${remove_ref[@]}"; do
|
||||
pending["$item"]=$(( ${pending["$item"]:-0} + 1 ))
|
||||
done
|
||||
result_ref=()
|
||||
for item in "${all_ref[@]}"; do
|
||||
if [[ "${pending["$item"]:-0}" -gt 0 ]]; then
|
||||
pending["$item"]=$(( pending["$item"] - 1 ))
|
||||
continue
|
||||
fi
|
||||
result_ref+=("$item")
|
||||
done
|
||||
}
|
||||
|
||||
split_list "${probe_compile[INCLUDES]:-}" _probe_includes
|
||||
split_list "${control_compile[INCLUDES]:-}" _control_includes
|
||||
subtract_multiset _probe_includes _control_includes include_arguments
|
||||
|
||||
split_list "${probe_compile[DEFINES]:-}" _probe_defines
|
||||
split_list "${control_compile[DEFINES]:-}" _control_defines
|
||||
subtract_multiset _probe_defines _control_defines define_arguments
|
||||
|
||||
split_list "${probe_compile[FLAGS]:-}" _probe_flags
|
||||
split_list "${control_compile[FLAGS]:-}" _control_flags
|
||||
subtract_multiset _probe_flags _control_flags compile_options_raw
|
||||
|
||||
split_list "${probe_link[LINK_LIBRARIES]:-}" _probe_link
|
||||
split_list "${control_link[LINK_LIBRARIES]:-}" _control_link
|
||||
subtract_multiset _probe_link _control_link link_items
|
||||
|
||||
[[ ${#include_arguments[@]} -gt 0 ]] || fail "The aurora compile interface is empty; the probe did not link aurora."
|
||||
[[ ${#link_items[@]} -gt 0 ]] || fail "The aurora link interface is empty; the probe did not link aurora."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Harvest the built libraries.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
binary_root=$(normalize "$stage_dir")
|
||||
aurora_root=$(normalize "$(get_meta aurora_dir)")
|
||||
runtime_root=$(normalize "$(get_meta runtime_dir)")
|
||||
|
||||
generated_include_index=0
|
||||
copy_harvested() {
|
||||
# $1 = source path, $2 = subdirectory under the package. Echoes "subdir/name".
|
||||
local source=$1 subdir=$2
|
||||
local name destination
|
||||
name=$(basename "$source")
|
||||
destination="$output_dir/$subdir/$name"
|
||||
[[ ! -e "$destination" ]] || fail "Two harvested files collide on the name $name: $source"
|
||||
cp "$source" "$destination"
|
||||
printf '%s/%s' "$subdir" "$name"
|
||||
}
|
||||
declare -A generated_include_tokens=()
|
||||
convert_to_token() {
|
||||
# $1 = absolute path, $2 = 'include' or another kind. Sets $TOKEN_RESULT - NOT echoed/command-
|
||||
# substituted: this needs to mutate generated_include_tokens/generated_include_index, and a
|
||||
# $(...) call runs in a subshell, silently discarding that mutation once it returns (verified
|
||||
# directly: every build-tree include directory collided into the same "generated01" until this
|
||||
# was written this way instead).
|
||||
local path kind=$2
|
||||
path=$(normalize "$1")
|
||||
case "$path" in
|
||||
"$aurora_root"/*) TOKEN_RESULT="@AURORA@/${path#$aurora_root/}"; return ;;
|
||||
"$runtime_root"/*) TOKEN_RESULT="@RUNTIME@/${path#$runtime_root/}"; return ;;
|
||||
"$binary_root"/*)
|
||||
[[ "$kind" == include ]] || fail "Unhandled build-tree artifact: $path"
|
||||
if [[ -n "${generated_include_tokens[$path]:-}" ]]; then
|
||||
TOKEN_RESULT=${generated_include_tokens[$path]}
|
||||
return
|
||||
fi
|
||||
generated_include_index=$((generated_include_index + 1))
|
||||
local name destination
|
||||
name=$(printf 'generated%02d' "$generated_include_index")
|
||||
destination="$output_dir/include/$name"
|
||||
mkdir -p "$destination"
|
||||
cp -a "$path/." "$destination/"
|
||||
TOKEN_RESULT="@PKG@/include/$name"
|
||||
generated_include_tokens[$path]=$TOKEN_RESULT
|
||||
return ;;
|
||||
*) fail "Path escapes every shippable root ($kind): $path" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
declare -A linker_file_to_reference=()
|
||||
shared_import_targets=() shared_import_runtime=() shared_import_implib=()
|
||||
targets_txt="$export_dir/targets.txt"
|
||||
assert_file "$targets_txt" "Native prebuilt export targets list"
|
||||
while IFS='|' read -r name type file linkerfile; do
|
||||
[[ -n "$name" ]] || continue
|
||||
[[ -f "$linkerfile" ]] || continue # not part of the closure mkw_np_probe pulled in
|
||||
linkerfile=$(normalize "$linkerfile")
|
||||
relative_linker=$(copy_harvested "$linkerfile" lib)
|
||||
if [[ "$type" == "SHARED_LIBRARY" ]]; then
|
||||
file_abs=$(normalize "$file")
|
||||
# Unlike Windows (a .dll + a separate .dll.a import library), a Linux shared object's
|
||||
# TARGET_FILE and TARGET_LINKER_FILE are the same path - one file, harvested once.
|
||||
if [[ "$file_abs" == "$linkerfile" ]]; then
|
||||
relative_runtime=$relative_linker
|
||||
else
|
||||
[[ -f "$file_abs" ]] || fail "Shared library $name has no runtime file: $file_abs"
|
||||
relative_runtime=$(copy_harvested "$file_abs" bin)
|
||||
fi
|
||||
shared_import_targets+=("mkw_np::$name")
|
||||
shared_import_runtime+=("$relative_runtime")
|
||||
shared_import_implib+=("$relative_linker")
|
||||
linker_file_to_reference["$linkerfile"]="mkw_np::$name"
|
||||
else
|
||||
linker_file_to_reference["$linkerfile"]="@PKG@/$relative_linker"
|
||||
fi
|
||||
done < "$targets_txt"
|
||||
# Unlike Windows (SDL/zlib/libpng ship as DLLs by default), everything here was forced static above
|
||||
# and Dawn's own Linux package (verified directly) ships libwebgpu_dawn.a, also static - so zero
|
||||
# shared imports is the expected, normal outcome, not a failure.
|
||||
echo "Prepare-NativePrebuilt.sh: harvested ${#linker_file_to_reference[@]} archive(s) (${#shared_import_targets[@]} shared)"
|
||||
|
||||
dawn_linker_file="" dawn_runtime_file=""
|
||||
dawn_txt="$export_dir/dawn.txt"
|
||||
if [[ -s "$dawn_txt" ]]; then
|
||||
IFS='|' read -r _ dawn_runtime_file dawn_linker_file < "$dawn_txt"
|
||||
dawn_runtime_file=$(normalize "$dawn_runtime_file")
|
||||
dawn_linker_file=$(normalize "$dawn_linker_file")
|
||||
fi
|
||||
[[ -n "$dawn_linker_file" ]] && linker_file_to_reference["$dawn_linker_file"]="dawn::webgpu_dawn"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rewrite the compile/link interface into workspace-relative tokens.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
package_include_dirs=()
|
||||
include_count=${#include_arguments[@]}
|
||||
i=0
|
||||
while (( i < include_count )); do
|
||||
entry=${include_arguments[i]}
|
||||
case "$entry" in
|
||||
-I*) convert_to_token "${entry#-I}" include; package_include_dirs+=("$TOKEN_RESULT") ;;
|
||||
-isystem)
|
||||
i=$((i + 1))
|
||||
(( i < include_count )) || fail "Trailing -isystem with no argument"
|
||||
convert_to_token "${include_arguments[i]}" include; package_include_dirs+=("$TOKEN_RESULT") ;;
|
||||
-isystem*) convert_to_token "${entry#-isystem}" include; package_include_dirs+=("$TOKEN_RESULT") ;;
|
||||
*) fail "Unexpected include argument: $entry" ;;
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
package_definitions=()
|
||||
for entry in "${define_arguments[@]}"; do
|
||||
case "$entry" in
|
||||
# Ninja records a quoted define's value with its quotes backslash-escaped (verified
|
||||
# directly: IMGUI_USER_CONFIG=\"aurora/imgui_config.h\" in build.ninja) regardless of
|
||||
# platform - format_cmake_block below re-escapes for CMake's own string syntax, so the
|
||||
# literal backslash has to come out here first or the result is double-escaped and the
|
||||
# define expands to literal backslash-quote characters instead of a quoted string.
|
||||
-D*) package_definitions+=("${entry#-D}") ;;
|
||||
*) fail "Unexpected define argument: $entry" ;;
|
||||
esac
|
||||
done
|
||||
package_definitions=("${package_definitions[@]//\\\"/\"}")
|
||||
|
||||
# fmt's `-include cstdlib` (and anything like it) is recorded from a C++ compile line, so re-scope
|
||||
# it to C++. The runtime also assembles generated .S blobs through the same targets, and a forced
|
||||
# C++ header include would break them.
|
||||
package_compile_options=()
|
||||
for entry in "${compile_options_raw[@]}"; do
|
||||
package_compile_options+=("\$<\$<COMPILE_LANGUAGE:CXX>:$entry>")
|
||||
done
|
||||
|
||||
package_link_items=()
|
||||
for item in "${link_items[@]}"; do
|
||||
case "$item" in
|
||||
-*) package_link_items+=("$item"); continue ;;
|
||||
esac
|
||||
if [[ "$item" = /* ]]; then item_abs=$(normalize "$item"); else item_abs=$(normalize "$stage_dir/$item"); fi
|
||||
ref=${linker_file_to_reference["$item_abs"]:-}
|
||||
if [[ -n "$ref" ]]; then
|
||||
package_link_items+=("$ref")
|
||||
continue
|
||||
fi
|
||||
case "$item_abs" in
|
||||
*/libz.so*)
|
||||
# The one unavoidable system reference: libpng's own vendored CMakeLists.txt calls
|
||||
# find_package(ZLIB REQUIRED) internally even when zlib came from aurora's own
|
||||
# FetchContent (extern/CMakeLists.txt writes a redirect config for exactly this case),
|
||||
# and CMAKE_DISABLE_FIND_PACKAGE_ZLIB errors out on any REQUIRED call site outright
|
||||
# (verified directly), so it cannot be forced to vendor/static the way absl/PNG/Freetype
|
||||
# are above. zlib is as close to a universal baseline as a Linux shared library gets
|
||||
# (glibc-adjacent - practically every distro has it already), so this is recorded as a
|
||||
# portable `-lz` link flag instead of the harvesting machine's absolute path.
|
||||
package_link_items+=("-lz")
|
||||
continue ;;
|
||||
esac
|
||||
fail "Link item is neither a system library nor a harvested archive: $item"
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Emit the consumer-facing CMake description.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
format_cmake_block() {
|
||||
# $1 = CMake variable name, remaining args = list items
|
||||
local name=$1; shift
|
||||
if [[ $# -eq 0 ]]; then
|
||||
printf 'set(%s "")\n' "$name"
|
||||
return
|
||||
fi
|
||||
printf 'set(%s\n' "$name"
|
||||
local item escaped
|
||||
for item in "$@"; do
|
||||
escaped=${item//\\/\\\\}
|
||||
escaped=${escaped//\"/\\\"}
|
||||
printf ' "%s"\n' "$escaped"
|
||||
done
|
||||
printf ')\n'
|
||||
}
|
||||
|
||||
dawn_config_dir_meta=$(get_meta dawn_config_dir)
|
||||
dawn_config_token=""
|
||||
if [[ -n "$dawn_config_dir_meta" ]]; then
|
||||
dawn_config_dir_abs=$(normalize "$dawn_config_dir_meta")
|
||||
case "$dawn_config_dir_abs" in
|
||||
"$binary_root"/*)
|
||||
# Dawn's find_package() config directory (lib/cmake/Dawn under its own fetched package
|
||||
# root) cannot go through the generic single-directory copy other build-tree artifacts
|
||||
# use: DawnConfig.cmake/DawnTargets.cmake derive _IMPORT_PREFIX/PACKAGE_PREFIX_DIR from
|
||||
# their OWN file location by walking up three parent directories (verified directly), so
|
||||
# the whole package root - include/, lib/, lib/cmake/Dawn/ together - must be copied as
|
||||
# one self-contained unit for that relative navigation to still resolve once moved.
|
||||
dawn_package_root=$(normalize "$dawn_config_dir_abs/../../..")
|
||||
dawn_config_relative=${dawn_config_dir_abs#$dawn_package_root/}
|
||||
mkdir -p "$output_dir/include/dawn_package"
|
||||
cp -a "$dawn_package_root/." "$output_dir/include/dawn_package/"
|
||||
dawn_config_token="@PKG@/include/dawn_package/$dawn_config_relative"
|
||||
;;
|
||||
*) convert_to_token "$dawn_config_dir_abs" include; dawn_config_token=$TOKEN_RESULT ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
generated_cmake="$output_dir/native_prebuilt.cmake"
|
||||
{
|
||||
echo "# Generated by Launcher/Prepare-NativePrebuilt.sh - do not edit."
|
||||
echo "#"
|
||||
echo "# Describes the precompiled aurora + third-party archives that replace a"
|
||||
echo "# from-source aurora-main build. Every path is a token resolved against the"
|
||||
echo "# consuming workspace, because the source trees still ship next to this"
|
||||
echo "# package and the runtime compiles against their headers."
|
||||
echo ""
|
||||
echo 'if(NOT MKW_NP_PACKAGE_DIR OR NOT MKW_NP_AURORA_DIR OR NOT MKW_NP_RUNTIME_DIR OR NOT MKW_NP_DEPS_DIR)'
|
||||
echo ' message(FATAL_ERROR "native_prebuilt.cmake must be included by runtime/cmake/NativePrebuilt.cmake")'
|
||||
echo 'endif()'
|
||||
echo ""
|
||||
format_cmake_block MKW_NP_INCLUDE_DIRECTORIES "${package_include_dirs[@]}"
|
||||
format_cmake_block MKW_NP_COMPILE_DEFINITIONS "${package_definitions[@]}"
|
||||
format_cmake_block MKW_NP_COMPILE_OPTIONS "${package_compile_options[@]}"
|
||||
format_cmake_block MKW_NP_LINK_LIBRARIES "${package_link_items[@]}"
|
||||
format_cmake_block MKW_NP_AURORA_TARGETS "aurora::gx" "aurora::pad" "aurora::si" "aurora::vi" "aurora::mtx"
|
||||
echo ""
|
||||
printf 'set(MKW_NP_DAWN_CONFIG_DIR "%s")\n' "$dawn_config_token"
|
||||
printf 'set(MKW_NP_DAWN_VERSION "%s")\n' "$(get_meta aurora_dawn_version)"
|
||||
echo ""
|
||||
echo "# Shared third-party libraries keep imported SHARED targets so that"
|
||||
echo '# $<TARGET_RUNTIME_DLLS> still copies them next to the game executable.'
|
||||
for idx in "${!shared_import_targets[@]}"; do
|
||||
printf 'add_library(%s SHARED IMPORTED GLOBAL)\n' "${shared_import_targets[$idx]}"
|
||||
printf 'set_target_properties(%s PROPERTIES\n' "${shared_import_targets[$idx]}"
|
||||
printf ' IMPORTED_LOCATION "${MKW_NP_PACKAGE_DIR}/%s"\n' "${shared_import_runtime[$idx]}"
|
||||
printf ' IMPORTED_IMPLIB "${MKW_NP_PACKAGE_DIR}/%s")\n' "${shared_import_implib[$idx]}"
|
||||
done
|
||||
} > "$generated_cmake"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Provenance: the pinned toolchain is what makes a precompiled archive interchangeable with
|
||||
# locally compiled objects (bit-for-bit the compiler and flag set the user's machine will use);
|
||||
# LocalBuild.ps1's Windows equivalent re-checks both before consuming a package, and a Linux
|
||||
# consumer-side check (once local-build.sh gains --native-prebuilt-dir) should do the same.
|
||||
# compiler_sha256/flag_fingerprint/aurora_fingerprint/third_party_fingerprint were already computed
|
||||
# above (needed before the --print-fingerprint-only early exit).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
dawn_runtime_sha256=""
|
||||
[[ -n "$dawn_runtime_file" && -f "$dawn_runtime_file" ]] && dawn_runtime_sha256=$(sha256_of "$dawn_runtime_file")
|
||||
[[ -n "$dawn_runtime_sha256" ]] || fail "The Dawn runtime could not be hashed: $dawn_runtime_file"
|
||||
|
||||
compiler_version=$(get_meta cxx_compiler_version)
|
||||
sdl3_target=$(get_meta aurora_sdl3_target)
|
||||
dawn_version=$(get_meta aurora_dawn_version)
|
||||
harvested_count=${#linker_file_to_reference[@]}
|
||||
|
||||
python3 - "$output_dir" "$compiler_sha256" "$compiler_version" "$flag_fingerprint" \
|
||||
"$dawn_version" "$dawn_runtime_sha256" "$aurora_fingerprint" "$third_party_fingerprint" \
|
||||
"$sdl3_target" "$harvested_count" <<'PY'
|
||||
import hashlib, json, os, sys, datetime
|
||||
|
||||
(output_dir, compiler_sha256, compiler_version, flag_fingerprint, dawn_version,
|
||||
dawn_runtime_sha256, aurora_fingerprint, third_party_fingerprint, sdl3_target,
|
||||
harvested_count) = sys.argv[1:]
|
||||
|
||||
contents = []
|
||||
for root, dirs, files in os.walk(output_dir):
|
||||
dirs.sort()
|
||||
for name in sorted(files):
|
||||
if name == "provenance.json":
|
||||
continue
|
||||
path = os.path.join(root, name)
|
||||
rel = os.path.relpath(path, output_dir).replace(os.sep, "/")
|
||||
with open(path, "rb") as fh:
|
||||
digest = hashlib.sha256(fh.read()).hexdigest()
|
||||
contents.append({"Path": rel, "Bytes": os.path.getsize(path), "Sha256": digest})
|
||||
contents.sort(key=lambda c: c["Path"])
|
||||
|
||||
provenance = {
|
||||
"SchemaVersion": 1,
|
||||
"BuiltUtc": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
||||
"CompilerSha256": compiler_sha256,
|
||||
"CompilerVersion": compiler_version,
|
||||
"FlagFingerprint": flag_fingerprint,
|
||||
"DawnVersion": dawn_version,
|
||||
"DawnRuntimeSha256": dawn_runtime_sha256,
|
||||
"AuroraSourceFingerprint": aurora_fingerprint,
|
||||
"ThirdPartySourceFingerprint": third_party_fingerprint,
|
||||
"Sdl3Target": sdl3_target,
|
||||
"HarvestedLibraryCount": int(harvested_count),
|
||||
"Contents": contents,
|
||||
}
|
||||
with open(os.path.join(output_dir, "provenance.json"), "w", encoding="utf-8") as fh:
|
||||
json.dump(provenance, fh, indent=2)
|
||||
fh.write("\n")
|
||||
|
||||
total_bytes = sum(c["Bytes"] for c in contents)
|
||||
print(f" files: {len(contents)}; size: {total_bytes / (1024 * 1024):.1f} MiB")
|
||||
PY
|
||||
|
||||
if [[ "$keep_stage" -eq 0 ]]; then rm -rf "$stage_dir"; fi
|
||||
|
||||
echo ""
|
||||
echo "Native prebuilt package: $output_dir"
|
||||
echo " archives: $(find "$output_dir/lib" -maxdepth 1 -type f | wc -l); shared imports: ${#shared_import_targets[@]}"
|
||||
echo " flag fingerprint: $flag_fingerprint"
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,7 @@ $llvmReadobj = [System.IO.Path]::GetFullPath($LlvmReadobjPath)
|
||||
|
||||
$systemDlls = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
@(
|
||||
'advapi32.dll', 'authz.dll', 'bcrypt.dll', 'combase.dll', 'comdlg32.dll', 'crypt32.dll',
|
||||
'advapi32.dll', 'authz.dll', 'bcrypt.dll', 'bcryptprimitives.dll', 'combase.dll', 'comdlg32.dll', 'crypt32.dll',
|
||||
'd3d11.dll', 'd3d12.dll', 'dbghelp.dll', 'dcomp.dll', 'dwrite.dll', 'dwmapi.dll',
|
||||
'dxgi.dll', 'gdi32.dll', 'imm32.dll',
|
||||
'iphlpapi.dll', 'kernel32.dll', 'mf.dll', 'mfplat.dll', 'mfreadwrite.dll', 'mfuuid.dll',
|
||||
|
||||
@@ -15,7 +15,8 @@ if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) {
|
||||
}
|
||||
$repoRoot = [IO.Path]::GetFullPath($RepositoryRoot)
|
||||
$launcher = Join-Path $repoRoot 'Launcher'
|
||||
$setup = Join-Path $launcher 'WiiCompiled.Setup'
|
||||
$setup = Join-Path $launcher 'WiiCompiled.Setup.Windows'
|
||||
$common = Join-Path $launcher 'WiiCompiled.Setup.Common'
|
||||
|
||||
$failures = [Collections.Generic.List[string]]::new()
|
||||
function Add-Failure([string]$Message) { $failures.Add($Message) }
|
||||
@@ -48,9 +49,11 @@ function Compare-Set([string[]]$Expected, [string[]]$Actual, [string]$ExpectedNa
|
||||
$pins = Get-MkwProjectPins (Join-Path $repoRoot 'projects\mkwii\recomp.yml')
|
||||
|
||||
# --- The Retro-WFC endpoint: recomp.yml owns it; the installer host pins the same string so a
|
||||
# --- redirected or rewritten endpoint cannot be fetched from.
|
||||
$inputValidation = Read-SourceFile (Join-Path $setup 'InputValidation.cs') 'InputValidation.cs'
|
||||
$hostUri = Get-CapturedValue $inputValidation 'CurrentRetroWfcPayloadUri\s*=\s*"([^"]+)"' `
|
||||
# --- redirected or rewritten endpoint cannot be fetched from. The literal lives in
|
||||
# --- WiiCompiled.Setup.Common (shared with WiiCompiled.Setup.Linux) - InputValidation.cs only
|
||||
# --- re-exports it as `= RetroWfcPayload.CurrentRetroWfcPayloadUri;`, no literal to capture there.
|
||||
$retroWfcPayload = Read-SourceFile (Join-Path $common 'RetroWfcPayload.cs') 'RetroWfcPayload.cs'
|
||||
$hostUri = Get-CapturedValue $retroWfcPayload 'CurrentRetroWfcPayloadUri\s*=\s*"([^"]+)"' `
|
||||
'The host Retro-WFC endpoint constant'
|
||||
if ($hostUri -cne $pins.RetroWfcPayloadUri) {
|
||||
Add-Failure "InputValidation.CurrentRetroWfcPayloadUri is '$hostUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'."
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# Build the real Windows runtime with translated, entirely synthetic PowerPC code.
|
||||
# No game dump, game symbol map, REL, mod download, or existing generated/ output is used.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools',
|
||||
[string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies',
|
||||
[string]$StageDirectory = 'build/recomp-test',
|
||||
[ValidateRange(1, 64)] [int]$Parallel = 3
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 3.0
|
||||
. (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1')
|
||||
|
||||
$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
function Full([string]$Path) {
|
||||
if ([IO.Path]::IsPathRooted($Path)) { return [IO.Path]::GetFullPath($Path) }
|
||||
return [IO.Path]::GetFullPath((Join-Path $repoRoot $Path))
|
||||
}
|
||||
$portableTools = Full $PortableToolsDirectory
|
||||
$dependencies = Full $DependencySourceDirectory
|
||||
$stage = Full $StageDirectory
|
||||
$dotnet = (Get-Command dotnet -CommandType Application).Source
|
||||
$cmake = Join-Path $portableTools 'CMake/bin/cmake.exe'
|
||||
$ninja = Join-Path $portableTools 'Ninja/ninja.exe'
|
||||
$compilerBin = Join-Path $portableTools 'llvm-mingw/bin'
|
||||
Assert-File $cmake 'Pinned CMake (run Prepare-PortableTools.ps1 first)'
|
||||
Assert-File $ninja 'Pinned Ninja'
|
||||
Assert-File (Join-Path $dependencies 'cppwinrt/winrt/base.h') 'Pinned dependencies (run Prepare-Dependencies.ps1 first)'
|
||||
|
||||
# Refuse reuse so a developer's game translation or an earlier build cannot make
|
||||
# the test pass. Keep the staging tree after the run for diagnostics.
|
||||
if (Test-Path -LiteralPath $stage) { throw "Test stage already exists; choose a fresh -StageDirectory: $stage" }
|
||||
[IO.Directory]::CreateDirectory($stage) | Out-Null
|
||||
Write-Host "Synthetic recompilation workspace: $stage"
|
||||
|
||||
# Copy current sources, including uncommitted edits, but no ignored build output.
|
||||
# An isolated workspace preserves the developer's real generated/ directory.
|
||||
$sourceFiles = & git -C $repoRoot -c core.quotepath=false ls-files --cached --others --exclude-standard -- runtime aurora-main
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not enumerate runtime and Aurora sources.' }
|
||||
foreach ($relative in $sourceFiles | Sort-Object -Unique) {
|
||||
$destination = Join-Path $stage $relative
|
||||
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination)) | Out-Null
|
||||
Copy-Item -LiteralPath (Join-Path $repoRoot $relative) -Destination $destination
|
||||
}
|
||||
|
||||
# One synthetic text section: li r3,40; addi r3,r3,2; nop; blr.
|
||||
# Native HLE wrappers also call these eight guest symbols directly. Give each
|
||||
# its own generated blr function so the real product can link without game code.
|
||||
# Keep this list explicit: a new unresolved guest dependency must fail the test.
|
||||
[uint32]$entry = 0x80001000L
|
||||
[uint32[]]$guestCallbacks = @(
|
||||
0x8012B830L, 0x801A0620L, 0x801A1ED8L, 0x801A961CL,
|
||||
0x801AADE0L, 0x801D8D30L, 0x801D9E94L, 0x8055531CL
|
||||
)
|
||||
$textSize = [int]($guestCallbacks[-1] - $entry + 4)
|
||||
$dataOffset = 0x100 + $textSize
|
||||
$dol = [byte[]]::new($dataOffset + 4)
|
||||
function Write-BigEndian32([int]$Offset, [uint32]$Value) {
|
||||
$dol[$Offset] = [byte](($Value -shr 24) -band 255)
|
||||
$dol[$Offset + 1] = [byte](($Value -shr 16) -band 255)
|
||||
$dol[$Offset + 2] = [byte](($Value -shr 8) -band 255)
|
||||
$dol[$Offset + 3] = [byte]($Value -band 255)
|
||||
}
|
||||
Write-BigEndian32 0x00 0x100 # text[0] file offset
|
||||
Write-BigEndian32 0x48 $entry # text[0] guest address
|
||||
Write-BigEndian32 0x90 $textSize # text[0] length (unreachable gaps are zero)
|
||||
Write-BigEndian32 0x1C $dataOffset # data[0] file offset
|
||||
Write-BigEndian32 0x64 0x80600000L # data[0] guest address
|
||||
Write-BigEndian32 0xAC 4 # data[0] length
|
||||
Write-BigEndian32 0xD8 0x80601000L # BSS address
|
||||
Write-BigEndian32 0xDC 32 # BSS length
|
||||
Write-BigEndian32 0xE0 $entry # entry point
|
||||
Write-BigEndian32 0x100 0x38600028 # li r3,40
|
||||
Write-BigEndian32 0x104 0x38630002 # addi r3,r3,2
|
||||
Write-BigEndian32 0x108 0x60000000 # nop
|
||||
Write-BigEndian32 0x10C 0x4E800020 # blr
|
||||
foreach ($address in $guestCallbacks) {
|
||||
Write-BigEndian32 ([int](0x100 + $address - $entry)) 0x4E800020
|
||||
}
|
||||
Write-BigEndian32 $dataOffset 0x12345678
|
||||
[IO.File]::WriteAllBytes((Join-Path $stage 'synthetic.dol'), $dol)
|
||||
$entryPoints = (@($entry) + $guestCallbacks | ForEach-Object { '0x{0:X8}' -f $_ }) -join ', '
|
||||
$functionMap = (@($entry) + $guestCallbacks | ForEach-Object { '{0:X8} func_{0:X8}' -f $_ }) -join "`n"
|
||||
[IO.File]::WriteAllText((Join-Path $stage 'synthetic-functions.txt'), $functionMap)
|
||||
$manifest = Join-Path $stage 'recomp.yml'
|
||||
[IO.File]::WriteAllText($manifest, @"
|
||||
schema_version: 1
|
||||
workspace_root: .
|
||||
project:
|
||||
id: ci-synthetic-dol
|
||||
display_name: CI Synthetic DOL
|
||||
memory:
|
||||
base: 0x80000000
|
||||
size: 0x01800000
|
||||
sda_base: 0x80600000
|
||||
sda2_base: 0x80600000
|
||||
inputs:
|
||||
dol:
|
||||
path: synthetic.dol
|
||||
translation:
|
||||
entry_points: [$entryPoints]
|
||||
function_map:
|
||||
path: synthetic-functions.txt
|
||||
allow_unsupported_instructions: false
|
||||
runtime:
|
||||
native_abi_directories: []
|
||||
native_registration_root: runtime/src
|
||||
output:
|
||||
root: generated
|
||||
"@)
|
||||
|
||||
$translatorProject = Join-Path $repoRoot 'translator/src/Translator.Cli/Translator.Cli.csproj'
|
||||
Invoke-Checked $dotnet @('build', $translatorProject, '-c', 'Release', '--disable-build-servers') 'Building the translator'
|
||||
$translator = Join-Path $repoRoot 'translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll'
|
||||
$metadata = Join-Path $stage 'generated/base_translation_output.json'
|
||||
Invoke-Checked $dotnet @($translator, 'translate-recursive', '0x80001000', '--project', $manifest,
|
||||
'--output-metadata', $metadata, '--threads', "$Parallel") `
|
||||
'Translating the synthetic DOL'
|
||||
# Function-map seeds can be skipped by discovery; do not accept a partial fixture.
|
||||
$translated = Get-Content -LiteralPath $metadata -Raw | ConvertFrom-Json
|
||||
foreach ($address in @($entry) + $guestCallbacks) {
|
||||
if ($address -notin $translated.functions.entryPoint) {
|
||||
throw ('Synthetic function 0x{0:X8} was not translated.' -f $address)
|
||||
}
|
||||
}
|
||||
Invoke-Checked $dotnet @($translator, 'generate-data-init', '--project', $manifest) 'Generating synthetic data and runtime configuration'
|
||||
Invoke-Checked $dotnet @($translator, 'emit-build-shards', '--project', $manifest) 'Emitting the production build graph'
|
||||
|
||||
$nativeBuild = Join-Path $stage 'native-build'
|
||||
$oldPath = $env:PATH
|
||||
try {
|
||||
$env:PATH = Get-MkwToolchainPath $portableTools
|
||||
$configure = Get-MkwNativeConfigureArguments -SourceDirectory (Join-Path $stage 'runtime') -BuildDirectory $nativeBuild `
|
||||
-Ninja $ninja -CCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang.exe') `
|
||||
-CxxCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang++.exe') `
|
||||
-ResourceCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-windres.exe') `
|
||||
-DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON')
|
||||
Invoke-Checked $cmake $configure 'Configuring the production Windows runtime' `
|
||||
-WaitForProcessTree $false
|
||||
Invoke-Checked $cmake @('--build', $nativeBuild, '--target', 'WiiCompiled', '--parallel', "$Parallel") `
|
||||
'Compiling and linking the synthetic product with the full runtime' `
|
||||
-WaitForProcessTree $false
|
||||
Assert-File (Join-Path $nativeBuild 'WiiCompiled.exe') 'Linked synthetic product'
|
||||
} finally {
|
||||
$env:PATH = $oldPath
|
||||
}
|
||||
Write-Host 'Synthetic recompilation passed (translation, data generation, runtime compilation, and product link).'
|
||||
@@ -0,0 +1,28 @@
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
// A packaging-time-only helper - never shipped, never run by an end user. Both
|
||||
// Launcher/build-appimage.sh and Launcher/Build-Installer.ps1 invoke this to obtain the nodtool
|
||||
// binary they bundle, so there is exactly one place (NodToolProvider) that knows the pinned
|
||||
// version/URL/platform-asset mapping, instead of a separate copy per packaging script.
|
||||
//
|
||||
// Usage: WiiCompiled.Setup.Common.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.Setup.Common.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.Setup.Common.Cli</RootNamespace>
|
||||
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
|
||||
<Version>0.2.31</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.Setup.Common\WiiCompiled.Setup.Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
/// <summary>
|
||||
/// One entry of an exact regular directory tree. Directory topology is part of the content
|
||||
/// contract everywhere this walker is used: an empty directory can be a runtime-visible asset just
|
||||
/// as a regular file can be, so it must not disappear from a content identity or a staged copy.
|
||||
/// </summary>
|
||||
internal sealed record RegularTreeEntry(string RelativePath, string FullPath, bool IsDirectory,
|
||||
public sealed record RegularTreeEntry(string RelativePath, string FullPath, bool IsDirectory,
|
||||
bool IsEmptyDirectory, long Length);
|
||||
|
||||
internal static class FileSystemUtilities
|
||||
public static class FileSystemUtilities
|
||||
{
|
||||
public static void CopyDirectory(string source, string destination,
|
||||
CancellationToken cancellationToken = default)
|
||||
+12
-6
@@ -1,9 +1,9 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
/// <summary>Reads and atomically writes the small JSON state documents kept inside an installation.</summary>
|
||||
internal static class JsonState
|
||||
/// <summary>Reads and atomically writes the small JSON state documents each installer keeps.</summary>
|
||||
public static class JsonState
|
||||
{
|
||||
private static readonly JsonSerializerOptions ReadOptions = new() { PropertyNameCaseInsensitive = true };
|
||||
private static readonly JsonSerializerOptions WriteOptions = new() { WriteIndented = true };
|
||||
@@ -17,11 +17,17 @@ internal static class JsonState
|
||||
catch
|
||||
{
|
||||
// A truncated or hand-edited state document must degrade into "unknown", which every
|
||||
// caller already treats as "assume stale and rebuild", not into a failed launch.
|
||||
// caller already treats as "assume stale and rebuild", not into a crash.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Write<T>(string path, T value) =>
|
||||
FileSystemUtilities.WriteAtomic(path, JsonSerializer.Serialize(value, WriteOptions));
|
||||
public static void Write<T>(string path, T value)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory);
|
||||
var tempPath = path + ".tmp-" + Guid.NewGuid().ToString("N");
|
||||
File.WriteAllText(tempPath, JsonSerializer.Serialize(value, WriteOptions));
|
||||
File.Move(tempPath, path, overwrite: true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
/// <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 <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.
|
||||
/// </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.Setup.Common;
|
||||
|
||||
/// <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.Setup.Common.Cli (invoked once at packaging time by both
|
||||
/// build-appimage.sh and Build-Installer.ps1 to acquire the copy each bundles).
|
||||
/// </summary>
|
||||
public static class NodToolProvider
|
||||
{
|
||||
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}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
+6
-54
@@ -1,4 +1,4 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
/// <summary>
|
||||
/// A portable installation is a self-contained directory tree the user can move or carry on removable media:
|
||||
@@ -9,8 +9,11 @@ namespace WiiCompiled.Setup;
|
||||
/// </code>
|
||||
/// The runtime finds the same root independently (<c>runtime/include/runtime_config.h</c>,
|
||||
/// <c>PortableRootDirectory</c>); this class must keep the same marker name, layout, and depth bound.
|
||||
/// Shared by both installers - Linux's CLI has no <c>--portable</c> flag, so it only ever calls
|
||||
/// <see cref="TryFind"/>/<see cref="UserDataDirectory"/>/<see cref="Contains"/> (always missing,
|
||||
/// since it never creates a marker file), not <see cref="Create"/>.
|
||||
/// </summary>
|
||||
internal static class PortableRoot
|
||||
public static class PortableRoot
|
||||
{
|
||||
public const string MarkerFileName = "portable.txt";
|
||||
public const string UserDataDirectoryName = "UserData";
|
||||
@@ -72,7 +75,7 @@ internal static class PortableRoot
|
||||
if (!File.Exists(marker))
|
||||
{
|
||||
File.WriteAllText(marker,
|
||||
$"{ProductInfo.Name} portable installation." + Environment.NewLine +
|
||||
"WiiCompiled portable installation." + Environment.NewLine +
|
||||
"This marker makes the runtime keep Config.toml, NAND, Cache, and Logs in UserData\\ " +
|
||||
"beside it instead of in %LOCALAPPDATA%." + Environment.NewLine +
|
||||
"Delete it to make this installation use per-user application data again." +
|
||||
@@ -90,54 +93,3 @@ internal static class PortableRoot
|
||||
|
||||
private static string Normalize(string path) => FileSystemUtilities.NormalizePath(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A portable root can be moved or renamed between operations. Every installed-host operation that
|
||||
/// reads <c>install-state.json</c> passes through here first so exactly one place decides what a
|
||||
/// moved installation means, and so a non-portable installation is never touched.
|
||||
/// </summary>
|
||||
internal static class PortableInstallHealing
|
||||
{
|
||||
/// <summary>
|
||||
/// Reconciles a moved portable installation with its recorded location: the state file adopts the
|
||||
/// directory it was actually found in, and the native build tree is discarded because its
|
||||
/// CMake cache holds absolute paths from the old location. Returns whether anything was healed.
|
||||
/// </summary>
|
||||
public static bool HealMovedInstall(Installation installation, IInstallReporter? reporter = null)
|
||||
{
|
||||
// Guard: an ordinary installation that disagrees with its state file is a real problem for
|
||||
// the operation to report, not something to silently rewrite.
|
||||
if (PortableRoot.TryFind(installation.Root) is null) return false;
|
||||
|
||||
var state = installation.ReadInstallState();
|
||||
if (state is not { SchemaVersion: 1 } || string.IsNullOrWhiteSpace(state.InstallDir)) return false;
|
||||
|
||||
string recorded;
|
||||
try
|
||||
{
|
||||
recorded = FileSystemUtilities.NormalizePath(state.InstallDir);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
|
||||
{
|
||||
recorded = state.InstallDir;
|
||||
}
|
||||
if (recorded.Equals(installation.Root, StringComparison.OrdinalIgnoreCase)) return false;
|
||||
|
||||
var previous = state.InstallDir;
|
||||
state.InstallDir = installation.Root;
|
||||
JsonState.Write(installation.InstallStatePath, state);
|
||||
|
||||
// The configured native build directory bakes absolute source, toolchain, and output paths
|
||||
// into CMakeCache.txt. After a move it is unusable and would fail the next configure rather
|
||||
// than being reused, so it is removed and reconfigured from scratch on the next build.
|
||||
var nativeBuild = Path.Combine(installation.WorkspaceDirectory, "native-build");
|
||||
var hadNativeBuild = Directory.Exists(nativeBuild);
|
||||
if (hadNativeBuild) FileSystemUtilities.DeleteDirectoryIfExists(nativeBuild);
|
||||
|
||||
reporter?.Diagnostic(
|
||||
$"This portable installation moved from {previous} to {installation.Root}. " +
|
||||
"The recorded location was updated" +
|
||||
(hadNativeBuild ? " and the location-bound native build cache was discarded." : "."));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -1,16 +1,17 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the one canonical Retro Rewind install. Wheel Wizard owns and passes it as
|
||||
/// <c>--retro-dir</c>; the backend only resolves, reads, and records it, never packages or copies it.
|
||||
/// <c>--retro-dir</c>; each installer only resolves, reads, and records it, never packages or
|
||||
/// copies it.
|
||||
/// </summary>
|
||||
internal static class RetroRewindSource
|
||||
public static class RetroRewindSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves the <c>RetroRewind6</c> folder from a selection that may be the folder itself or a
|
||||
/// parent containing exactly one <c>RetroRewind6/Binaries/Code.pul</c>.
|
||||
/// </summary>
|
||||
internal static string ResolveRetroRewind6(string selected)
|
||||
public static string ResolveRetroRewind6(string selected)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(selected))
|
||||
throw new InvalidDataException("Choose the canonical Retro Rewind folder.");
|
||||
+31
-111
@@ -1,20 +1,28 @@
|
||||
using System.Diagnostics;
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
internal static class InputValidation
|
||||
/// <summary>
|
||||
/// One validated, content-identified download in operation-owned scratch space. Callers use this
|
||||
/// exact directory for both the update decision and any resulting build.
|
||||
/// </summary>
|
||||
public sealed record RetroWfcPayloadSnapshot(string Directory, string Sha256, long ByteLength);
|
||||
|
||||
/// <summary>
|
||||
/// Downloads and verifies the Retro-WFC payload (a small, RSA-signed blob served from a single
|
||||
/// fixed endpoint). Shared by both installers - moved here from WiiCompiled.Setup.Windows's
|
||||
/// InputValidation.cs, which keeps every one of these method names as thin forwarding wrappers so
|
||||
/// its many existing call sites (ProductRepairService.cs, LocalBuildService.cs, Installation.cs,
|
||||
/// SelfTests.cs) needed no changes.
|
||||
/// </summary>
|
||||
public static class RetroWfcPayload
|
||||
{
|
||||
private const long MaximumRetroWfcPayloadBytes = 16L * 1024 * 1024;
|
||||
// The payload is tens of kilobytes from a single fixed endpoint 30s is good.
|
||||
private static readonly TimeSpan RetroWfcDownloadTimeout = TimeSpan.FromSeconds(30);
|
||||
private static readonly TimeSpan RetroWfcRetryDelay = TimeSpan.FromSeconds(1);
|
||||
private static readonly HashSet<string> SupportedDiscImageExtensions = new(
|
||||
[".iso", ".gcm", ".gcz", ".ciso", ".wbfs", ".wia", ".rvz"],
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public const string CurrentRetroWfcPayloadUri = "http://nas.play.rwfc.net/payload?g=RMCPD00";
|
||||
private static readonly string RetroWfcOfflinePayloadFile =
|
||||
@@ -37,43 +45,6 @@ internal static class InputValidation
|
||||
private const int RetroWfcPayloadSignatureOffset = 0x10;
|
||||
private const int RetroWfcPayloadMinimumBytes = 0x130;
|
||||
|
||||
public static void ValidateExtension(string gamePath)
|
||||
{
|
||||
if (!File.Exists(gamePath))
|
||||
throw new FileNotFoundException("The selected game image does not exist.", gamePath);
|
||||
var extension = Path.GetExtension(gamePath);
|
||||
if (!SupportedDiscImageExtensions.Contains(extension))
|
||||
throw new InvalidDataException(
|
||||
"Select a complete Wii disc image in ISO, GCM, GCZ, CISO, WBFS, WIA, or RVZ format.");
|
||||
}
|
||||
|
||||
public static async Task<DiscHeader> ReadDiscHeaderAsync(string dolphinTool, string gamePath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ValidateExtension(gamePath);
|
||||
var result = await ProcessRunner.RunAsync(dolphinTool,
|
||||
["header", "-i", Path.GetFullPath(gamePath), "-j"], null, cancellationToken);
|
||||
if (result.ExitCode != 0)
|
||||
throw new InvalidDataException("DolphinTool 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.");
|
||||
}
|
||||
|
||||
public static void EnsureCompatibleDisc(DiscHeader header, PayloadManifest manifest)
|
||||
{
|
||||
if (!header.GameId.Equals(manifest.ExpectedGameId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"This build supports Mario Kart Wii PAL ({manifest.ExpectedGameId}). " +
|
||||
$"The selected image is {header.GameId} ({header.InternalName}, {header.Region}).");
|
||||
}
|
||||
}
|
||||
|
||||
public static string ValidateStagedRetroWfcPayloadDirectory(string stagedDirectory,
|
||||
RSAParameters? signingKey = null)
|
||||
{
|
||||
@@ -82,9 +53,22 @@ internal static class InputValidation
|
||||
|
||||
var root = Path.GetFullPath(stagedDirectory);
|
||||
var payload = Path.Combine(root, RetroWfcOfflinePayloadFile);
|
||||
if (!File.Exists(payload))
|
||||
try
|
||||
{
|
||||
if ((File.GetAttributes(payload) & FileAttributes.Directory) != 0)
|
||||
throw new InvalidDataException(
|
||||
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
|
||||
}
|
||||
ValidateRetroWfcPayloadFile(payload, signingKey);
|
||||
return root;
|
||||
}
|
||||
@@ -186,7 +170,7 @@ internal static class InputValidation
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsTransientRetroWfcDownloadFailure(Exception exception,
|
||||
public static bool IsTransientRetroWfcDownloadFailure(Exception exception,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested) return false;
|
||||
@@ -232,73 +216,9 @@ internal static class InputValidation
|
||||
"The Retro-WFC payload is not signed by the pinned Retro-WFC signing key.");
|
||||
}
|
||||
|
||||
public static string Sha256File(string path)
|
||||
private static string Sha256File(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError)
|
||||
{
|
||||
public string CombinedOutput => StandardOutput + Environment.NewLine + StandardError;
|
||||
}
|
||||
|
||||
internal static class ProcessRunner
|
||||
{
|
||||
/// <summary>Runs a redirected child process to completion. <paramref name="configure"/> sets up a
|
||||
/// working directory or scrubbed environment; <paramref name="capture"/> is off for callers that only
|
||||
/// forward output live, so a build's output isn't buffered in memory for nobody to read.</summary>
|
||||
public static async Task<ProcessResult> RunAsync(string executable, IReadOnlyList<string> arguments,
|
||||
Action<string>? output, CancellationToken cancellationToken,
|
||||
Action<ProcessStartInfo>? configure = null, bool capture = true,
|
||||
Action<Exception>? onTerminationFailure = null)
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = executable,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
foreach (var argument in arguments) info.ArgumentList.Add(argument);
|
||||
configure?.Invoke(info);
|
||||
|
||||
using var process = new Process { StartInfo = info, EnableRaisingEvents = true };
|
||||
var stdout = new List<string>();
|
||||
var stderr = new List<string>();
|
||||
process.OutputDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stdout.Add(e.Data); output?.Invoke(e.Data); } };
|
||||
process.ErrorDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stderr.Add(e.Data); output?.Invoke(e.Data); } };
|
||||
if (!process.Start()) throw new InvalidOperationException($"Could not start {executable}.");
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
await WaitForExitAsync(process, cancellationToken, onTerminationFailure);
|
||||
return new ProcessResult(process.ExitCode, string.Join(Environment.NewLine, stdout),
|
||||
string.Join(Environment.NewLine, stderr));
|
||||
}
|
||||
|
||||
public static async Task WaitForExitAsync(Process process, CancellationToken cancellationToken,
|
||||
Action<Exception>? onTerminationFailure = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!process.HasExited) process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
onTerminationFailure?.Invoke(ex);
|
||||
}
|
||||
await process.WaitForExitAsync(CancellationToken.None);
|
||||
process.WaitForExit();
|
||||
throw;
|
||||
}
|
||||
process.WaitForExit();
|
||||
}
|
||||
}
|
||||
+18
-16
@@ -1,16 +1,19 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
internal sealed record RuntimeConfigSnapshot(bool Existed, byte[] Contents);
|
||||
public sealed record RuntimeConfigSnapshot(bool Existed, byte[] Contents);
|
||||
|
||||
/// <summary>
|
||||
/// Reads and writes the runtime's <c>Config.toml</c>. Every entry point takes the file it operates on
|
||||
/// since its location depends on the installation (portable <c>UserData</c> vs. per-user app data);
|
||||
/// callers obtain it once via <see cref="ResolveConfigPath"/>.
|
||||
/// callers obtain it once via <see cref="ResolveConfigPath"/>. Shared by both installers - Linux
|
||||
/// never creates a <see cref="PortableRoot.MarkerFileName"/> marker file, so
|
||||
/// <see cref="ResolveConfigPath"/>/<see cref="FormatPathValue"/>'s portable-root lookups always miss
|
||||
/// there and this degrades to the same plain per-user-app-data, always-absolute-path behavior a
|
||||
/// non-portable Windows install already gets.
|
||||
/// </summary>
|
||||
internal static class RuntimeConfiguration
|
||||
public static class RuntimeConfiguration
|
||||
{
|
||||
public const string ConfigFileName = "Config.toml";
|
||||
|
||||
@@ -51,7 +54,7 @@ internal static class RuntimeConfiguration
|
||||
File.Delete(configPath);
|
||||
}
|
||||
|
||||
internal static void SetDvdRoot(string configPath, string dvdRoot) =>
|
||||
public static void SetDvdRoot(string configPath, string dvdRoot) =>
|
||||
SetPath(configPath, "dvd_root", dvdRoot);
|
||||
|
||||
/// <summary>
|
||||
@@ -59,21 +62,21 @@ internal static class RuntimeConfiguration
|
||||
/// asset overlay by scanning this directory live at launch, so an asset-only Retro Rewind update
|
||||
/// needs no backend work: the next launch simply reads the new files.
|
||||
/// </summary>
|
||||
internal static void SetRetroRewindRoot(string configPath, string retroRewindRoot) =>
|
||||
public static void SetRetroRewindRoot(string configPath, string retroRewindRoot) =>
|
||||
SetPath(configPath, "retro_rewind_root", retroRewindRoot);
|
||||
|
||||
/// <summary>The canonical Retro Rewind root, or null when no installation has recorded one.</summary>
|
||||
public static string? GetRetroRewindRoot(string configPath) =>
|
||||
GetResolvedPath(configPath, "retro_rewind_root");
|
||||
|
||||
internal static void RemoveRetroRewindRootIfOwned(string configPath, string retroRewindRoot) =>
|
||||
public static void RemoveRetroRewindRootIfOwned(string configPath, string retroRewindRoot) =>
|
||||
RemovePathIfOwned(configPath, "retro_rewind_root", retroRewindRoot);
|
||||
|
||||
internal static void RemoveDvdRootIfOwned(string configPath, string dvdRoot) =>
|
||||
public static void RemoveDvdRootIfOwned(string configPath, string dvdRoot) =>
|
||||
RemovePathIfOwned(configPath, "dvd_root", dvdRoot);
|
||||
|
||||
/// <summary>The raw stored text of a <c>[paths]</c> key, exactly as the file holds it.</summary>
|
||||
internal static string? GetPath(string configPath, string key) =>
|
||||
public static string? GetPath(string configPath, string key) =>
|
||||
TryUnquoteToml(GetRawValue(configPath, "paths", key) ?? "", out var value) ? value : null;
|
||||
|
||||
/// <summary>
|
||||
@@ -81,17 +84,17 @@ internal static class RuntimeConfiguration
|
||||
/// <c>[paths]</c> value against the directory holding <c>Config.toml</c> (never the working
|
||||
/// directory), so the host must resolve it the same way before comparing or reading it.
|
||||
/// </summary>
|
||||
internal static string? GetResolvedPath(string configPath, string key)
|
||||
public static string? GetResolvedPath(string configPath, string key)
|
||||
{
|
||||
var stored = GetPath(configPath, key);
|
||||
return string.IsNullOrWhiteSpace(stored) ? null : ResolveAgainstConfig(configPath, stored);
|
||||
}
|
||||
|
||||
internal static string ConfigDirectory(string configPath) =>
|
||||
public static string ConfigDirectory(string configPath) =>
|
||||
Path.GetDirectoryName(Path.GetFullPath(configPath))
|
||||
?? throw new InvalidOperationException($"{configPath} has no containing directory.");
|
||||
|
||||
internal static string ResolveAgainstConfig(string configPath, string value) =>
|
||||
public static string ResolveAgainstConfig(string configPath, string value) =>
|
||||
Path.GetFullPath(value, ConfigDirectory(configPath));
|
||||
|
||||
private static void SetPath(string configPath, string key, string value)
|
||||
@@ -150,7 +153,7 @@ internal static class RuntimeConfiguration
|
||||
}
|
||||
|
||||
/// <summary>The raw TOML literal stored for a key, or null when the section or key is absent.</summary>
|
||||
internal static string? GetRawValue(string configPath, string section, string key)
|
||||
public static string? GetRawValue(string configPath, string section, string key)
|
||||
{
|
||||
if (!File.Exists(configPath)) return null;
|
||||
var header = $"[{section}]";
|
||||
@@ -265,7 +268,7 @@ internal static class RuntimeConfiguration
|
||||
private static string QuoteToml(string value) =>
|
||||
"\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
|
||||
|
||||
internal static bool TryUnquoteToml(string value, out string result)
|
||||
public static bool TryUnquoteToml(string value, out string result)
|
||||
{
|
||||
result = "";
|
||||
if (value.Length < 2) return false;
|
||||
@@ -292,5 +295,4 @@ internal static class RuntimeConfiguration
|
||||
result = builder.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
|
||||
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
|
||||
<Version>0.2.31</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
|
||||
<DebugType>embedded</DebugType>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,111 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes Launcher/local-build.sh and turns its stdout into progress reports. Replaces
|
||||
/// LocalBuildService.cs's hardcoded Windows PowerShell 5.1 invocation - there is no PowerShell
|
||||
/// dependency here at all, just bash.
|
||||
/// </summary>
|
||||
internal static class BuildRunner
|
||||
{
|
||||
public static async Task RunAsync(
|
||||
string workspace, string profile, string outputDir, string? baseOutputDir,
|
||||
string? retroDir, string? retroWfcOfflineDir, bool skipRetroWfcPayload,
|
||||
bool forceCleanBuild, string? translatorBin, string? ccBin, string? cxxBin, string? fuseLd,
|
||||
string? cmakeBin, string? ninjaBin, string? nativePrebuiltDir, IInstallReporter reporter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var script = Path.Combine(workspace, "Launcher", "local-build.sh");
|
||||
if (!File.Exists(script)) throw new FileNotFoundException("local-build.sh is missing", script);
|
||||
|
||||
var startInfo = new ProcessStartInfo("bash")
|
||||
{
|
||||
WorkingDirectory = workspace,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
startInfo.ArgumentList.Add(script);
|
||||
startInfo.ArgumentList.Add("--profile"); startInfo.ArgumentList.Add(profile);
|
||||
startInfo.ArgumentList.Add("--output-dir"); startInfo.ArgumentList.Add(outputDir);
|
||||
if (!string.IsNullOrEmpty(baseOutputDir))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--base-output-dir"); startInfo.ArgumentList.Add(baseOutputDir);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(retroDir))
|
||||
{
|
||||
// Still forwarded to local-build.sh under its own internal name -
|
||||
// --retro-rewind-package-dir - matching LocalBuild.ps1's own -RetroRewindPackageDirectory.
|
||||
startInfo.ArgumentList.Add("--retro-rewind-package-dir"); startInfo.ArgumentList.Add(retroDir);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(retroWfcOfflineDir))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--retro-wfc-offline-dir"); startInfo.ArgumentList.Add(retroWfcOfflineDir);
|
||||
}
|
||||
if (skipRetroWfcPayload) startInfo.ArgumentList.Add("--skip-retro-wfc-payload");
|
||||
if (forceCleanBuild) startInfo.ArgumentList.Add("--force-clean-build");
|
||||
if (!string.IsNullOrEmpty(translatorBin))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--translator-bin"); startInfo.ArgumentList.Add(translatorBin);
|
||||
}
|
||||
// Forwarded by AppRun so the AppImage's bundled clang/lld (see prepare-portable-clang.sh)
|
||||
// is used instead of local-build.sh's own default of whatever clang is on $PATH.
|
||||
if (!string.IsNullOrEmpty(ccBin))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--cc"); startInfo.ArgumentList.Add(ccBin);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(cxxBin))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--cxx"); startInfo.ArgumentList.Add(cxxBin);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(fuseLd))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--fuse-ld"); startInfo.ArgumentList.Add(fuseLd);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(cmakeBin))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--cmake"); startInfo.ArgumentList.Add(cmakeBin);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(ninjaBin))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--ninja"); startInfo.ArgumentList.Add(ninjaBin);
|
||||
}
|
||||
// Forwarded by AppRun so the AppImage's bundled precompiled aurora/third-party package (see
|
||||
// Prepare-NativePrebuilt.sh) is used instead of local-build.sh compiling aurora-main itself.
|
||||
if (!string.IsNullOrEmpty(nativePrebuiltDir))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--native-prebuilt-dir"); startInfo.ArgumentList.Add(nativePrebuiltDir);
|
||||
}
|
||||
|
||||
using var process = new Process { StartInfo = startInfo };
|
||||
var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96);
|
||||
|
||||
process.OutputDataReceived += (_, e) => { if (e.Data is not null) window.Observe(e.Data); };
|
||||
process.ErrorDataReceived += (_, e) => { if (e.Data is not null) reporter.Diagnostic(e.Data); };
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
KillProcessTree(process);
|
||||
throw;
|
||||
}
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"local-build.sh failed (exit {process.ExitCode}). See diagnostics above.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void KillProcessTree(Process process)
|
||||
{
|
||||
try { process.Kill(entireProcessTree: true); } catch { /* best-effort */ }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
/// <summary>
|
||||
/// freedesktop.org .desktop application-menu entries. Replaces ShellIntegration.cs's registry
|
||||
/// uninstall entry (no Linux analogue for an unpackaged tool - Windows already skips that step for
|
||||
/// portable installs, this just applies that same behavior universally) and .lnk shortcuts.
|
||||
/// </summary>
|
||||
internal static class DesktopEntry
|
||||
{
|
||||
private static string ApplicationsDirectory =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "applications");
|
||||
|
||||
private static string PathFor(string profile) =>
|
||||
Path.Combine(ApplicationsDirectory, $"wiicompiled-{profile}.desktop");
|
||||
|
||||
public static void Create(string profile, string displayName, string exePath)
|
||||
{
|
||||
// exePath is the installed native runtime binary itself (e.g.
|
||||
// .../Install/Base/WiiCompiled) - each profile already gets its own .desktop file here,
|
||||
// so there is no need to route through the setup tool's own launch-base/launch-retro
|
||||
// subcommand dispatch first. Unquoted: the Desktop Entry spec's Exec grammar doesn't take
|
||||
// a bare '"'-wrapped path, and none is needed here anyway - the only part of this path
|
||||
// that varies is the username, which Unix forbids containing whitespace.
|
||||
Directory.CreateDirectory(ApplicationsDirectory);
|
||||
var contents =
|
||||
"[Desktop Entry]\n" +
|
||||
"Type=Application\n" +
|
||||
$"Name={displayName}\n" +
|
||||
$"Exec={exePath}\n" +
|
||||
"Categories=Game;\n" +
|
||||
"Terminal=false\n";
|
||||
File.WriteAllText(PathFor(profile), contents);
|
||||
}
|
||||
|
||||
public static void Remove(string profile)
|
||||
{
|
||||
var path = PathFor(profile);
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System.Security.Cryptography;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
/// <summary>
|
||||
/// Validates and extracts the user's own Mario Kart Wii disc via `nodtool` (see
|
||||
/// WiiCompiled.Setup.Common/NodToolProvider.cs) - a prebuilt, MIT/Apache-2.0-licensed CLI from
|
||||
/// encounter/nod, replacing the earlier dependency on a system-installed `dolphin-tool`
|
||||
/// (GPL-2.0-or-later, and not reliably packaged standalone by every distro).
|
||||
/// </summary>
|
||||
internal static class DiscTool
|
||||
{
|
||||
public static async Task ValidateAndExtractAsync(
|
||||
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 info = NodToolInfoParser.Parse(await RunInfoAsync(nodTool, isoPath, cancellationToken));
|
||||
if (!string.Equals(info.GameId, manifest.GameId, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"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.");
|
||||
}
|
||||
|
||||
// Extracted straight into Assets/DATA (kept, not a scratch dir) - the runtime reads course/
|
||||
// texture/audio data from this directory live via [paths] dvd_root, not just at translation
|
||||
// time, so it has to survive past this install (see Program.cs, which points dvd_root here).
|
||||
reporter.Progress(InstallStages.ExtractDisc, "Extracting the disc image", 4);
|
||||
var dataDir = Path.Combine(assetsDirectory, "DATA");
|
||||
if (Directory.Exists(dataDir)) Directory.Delete(dataDir, recursive: true);
|
||||
await RunExtractAsync(nodTool, isoPath, dataDir, cancellationToken);
|
||||
|
||||
var dolPath = Path.Combine(dataDir, "sys", "main.dol");
|
||||
var relPath = Path.Combine(dataDir, "files", "rel", "StaticR.rel");
|
||||
if (!File.Exists(dolPath)) throw new FileNotFoundException("nodtool did not produce main.dol", dolPath);
|
||||
if (!File.Exists(relPath)) throw new FileNotFoundException("nodtool did not produce StaticR.rel", relPath);
|
||||
|
||||
var dolSha = Sha256Of(dolPath);
|
||||
var relSha = Sha256Of(relPath);
|
||||
if (!string.Equals(dolSha, manifest.DolSha256, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"main.dol sha256 mismatch: expected {manifest.DolSha256}, got {dolSha}. " +
|
||||
"This disc revision does not match what this project's manifest is pinned to.");
|
||||
}
|
||||
if (!string.Equals(relSha, manifest.RelSha256, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"StaticR.rel sha256 mismatch: expected {manifest.RelSha256}, got {relSha}. " +
|
||||
"This disc revision does not match what this project's manifest is pinned to.");
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(assetsDirectory);
|
||||
File.Copy(dolPath, Path.Combine(assetsDirectory, "main.dol"), overwrite: true);
|
||||
File.Copy(relPath, Path.Combine(assetsDirectory, "StaticR.rel"), overwrite: true);
|
||||
reporter.Progress(InstallStages.ExtractDisc, "Disc validated and extracted", 6);
|
||||
}
|
||||
|
||||
private static async Task<string> RunInfoAsync(string nodTool, string isoPath, CancellationToken cancellationToken)
|
||||
{
|
||||
var startInfo = new System.Diagnostics.ProcessStartInfo(nodTool)
|
||||
{
|
||||
ArgumentList = { "info", isoPath },
|
||||
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 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
// Ported near-verbatim from Launcher/WiiCompiled.Setup/InstallProgress.cs: this whole file is
|
||||
// platform-neutral (System.Text.Json + Console only), so the NDJSON --progress-json wire protocol
|
||||
// stays byte-for-byte the same shape a future GUI already speaks on Windows.
|
||||
|
||||
/// <summary>
|
||||
/// Stable stage identifiers reported by <c>--progress-json</c>. Kept intentionally small for this
|
||||
/// lean Linux installer (no toolkit-extraction/publish-transaction stages, since there is no
|
||||
/// bundled toolkit or staged workspace copy here - see the plan's "operate on a git checkout"
|
||||
/// scoping decision).
|
||||
/// </summary>
|
||||
internal static class InstallStages
|
||||
{
|
||||
public const string Validate = "validate";
|
||||
public const string ExtractDisc = "extract-disc";
|
||||
public const string Build = "build";
|
||||
public const string Shortcuts = "shortcuts";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Where an installation reports what it is doing. Progress is coarse and monotonic; raw translator
|
||||
/// and compiler output is a diagnostic, never progress, because it is unbounded and machine-hostile.
|
||||
/// </summary>
|
||||
internal interface IInstallReporter
|
||||
{
|
||||
void Progress(string stage, string message, int percent);
|
||||
void Diagnostic(string line);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <c>--progress-json</c> protocol: one JSON object per line on stdout, nothing else on stdout,
|
||||
/// diagnostics on stderr. The terminal <c>result</c> line is written exactly once.
|
||||
/// </summary>
|
||||
internal sealed class NdjsonInstallReporter : IInstallReporter
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new() { WriteIndented = false };
|
||||
private readonly object _gate = new();
|
||||
private int _lastPercent;
|
||||
private bool _finished;
|
||||
|
||||
public void Progress(string stage, string message, int percent)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_finished) return;
|
||||
// Percentages are clamped monotonic: a caller's progress bar must never walk backwards
|
||||
// because a later stage happened to estimate a lower number.
|
||||
_lastPercent = Math.Clamp(Math.Max(percent, _lastPercent), 0, 99);
|
||||
WriteLine(new { type = "progress", stage, message, percent = _lastPercent });
|
||||
}
|
||||
}
|
||||
|
||||
public void Diagnostic(string line) => Console.Error.WriteLine(line);
|
||||
|
||||
public void Success(string installDirectory)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
WriteLine(new { type = "result", success = true, version = ProductInfo.Version, installDir = installDirectory });
|
||||
}
|
||||
}
|
||||
|
||||
public void Failure(string error)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
WriteLine(new { type = "result", success = false, error });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The terminal result line is the caller's only completion signal, so no exit path may skip it.
|
||||
/// Callers invoke this from a finally block; it is a no-op once a result was already written.
|
||||
/// </summary>
|
||||
public void EnsureFinished(string errorIfUnfinished) => Failure(errorIfUnfinished);
|
||||
|
||||
private static void WriteLine(object value)
|
||||
{
|
||||
Console.Out.WriteLine(JsonSerializer.Serialize(value, Options));
|
||||
Console.Out.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Plain-text console reporting for a run without <c>--progress-json</c>.</summary>
|
||||
internal sealed class ConsoleInstallReporter : IInstallReporter
|
||||
{
|
||||
public void Progress(string stage, string message, int percent) =>
|
||||
Console.Out.WriteLine($"[{percent,3}%] {message}");
|
||||
|
||||
public void Diagnostic(string line) => Console.Out.WriteLine(line);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build step identifiers from local-build.sh's <c>MKWCBUILD:STEP:<id></c> lines - the id is
|
||||
/// the contract, matched against Launcher/local-build.sh's log_step() call sites.
|
||||
/// </summary>
|
||||
internal static class BuildStepIds
|
||||
{
|
||||
public const string BuildTranslator = "build-translator";
|
||||
public const string ReuseBaseTranslation = "reuse-base-translation";
|
||||
public const string RetranslateBase = "retranslate-base";
|
||||
public const string TranslateBase = "translate-base";
|
||||
public const string EmitBaseManifest = "emit-base-manifest";
|
||||
public const string TranslateMod = "translate-mod";
|
||||
public const string GenerateDataInit = "generate-data-init";
|
||||
public const string EmitBuildShards = "emit-build-shards";
|
||||
public const string ConfigureNative = "configure-native";
|
||||
public const string Compile = "compile";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps one local-build.sh run onto a slice of the overall percentage. local-build.sh announces
|
||||
/// every step it starts with an <c>MKWCBUILD:</c> prefix, so the slice can advance on real events
|
||||
/// instead of on a timer.
|
||||
/// </summary>
|
||||
internal sealed class BuildProgressWindow
|
||||
{
|
||||
private const string Marker = "MKWCBUILD:";
|
||||
private const string StepMarker = "STEP:";
|
||||
|
||||
/// <summary>The fraction the compile step reaches; beyond it, compiler output is a heartbeat.</summary>
|
||||
private const double CompileFraction = 0.58;
|
||||
|
||||
private static readonly (string Id, double Fraction, string Message)[] Steps =
|
||||
[
|
||||
(BuildStepIds.BuildTranslator, 0.04, "Building the translator"),
|
||||
(BuildStepIds.ReuseBaseTranslation, 0.30, "Reusing the completed base translation"),
|
||||
(BuildStepIds.RetranslateBase, 0.08, "The base translation is stale; retranslating it"),
|
||||
(BuildStepIds.TranslateBase, 0.10, "Translating Mario Kart Wii"),
|
||||
(BuildStepIds.EmitBaseManifest, 0.34, "Creating the translation manifest"),
|
||||
(BuildStepIds.TranslateMod, 0.38, "Translating the Retro Rewind Code.pul"),
|
||||
(BuildStepIds.GenerateDataInit, 0.44, "Generating game data initialization"),
|
||||
(BuildStepIds.EmitBuildShards, 0.48, "Preparing the native build"),
|
||||
(BuildStepIds.ConfigureNative, 0.52, "Configuring the compiler"),
|
||||
(BuildStepIds.Compile, CompileFraction, "Compiling the game. This is the longest step"),
|
||||
];
|
||||
|
||||
private readonly IInstallReporter _reporter;
|
||||
private readonly string _stage;
|
||||
private readonly int _start;
|
||||
private readonly int _end;
|
||||
private double _fraction;
|
||||
private string _message = "Preparing the local build";
|
||||
private int _reportedPercent = -1;
|
||||
|
||||
public BuildProgressWindow(IInstallReporter reporter, string stage, int start, int end)
|
||||
{
|
||||
_reporter = reporter;
|
||||
_stage = stage;
|
||||
_start = start;
|
||||
_end = end;
|
||||
}
|
||||
|
||||
public void Observe(string line)
|
||||
{
|
||||
var index = line.IndexOf(Marker, StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
var text = line[(index + Marker.Length)..].Trim();
|
||||
if (text.StartsWith(StepMarker, StringComparison.Ordinal))
|
||||
{
|
||||
var identifier = text[StepMarker.Length..];
|
||||
var end = identifier.IndexOf(' ');
|
||||
if (end >= 0) identifier = identifier[..end];
|
||||
foreach (var (id, fraction, message) in Steps)
|
||||
{
|
||||
if (!id.Equals(identifier, StringComparison.Ordinal)) continue;
|
||||
if (fraction > _fraction)
|
||||
{
|
||||
_fraction = fraction;
|
||||
_message = message;
|
||||
Emit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Anything else - a plain MKWCBUILD note, or raw tool output - stays a diagnostic and only
|
||||
// feeds the heartbeat below.
|
||||
_reporter.Diagnostic(line);
|
||||
// Compilation announces itself once and then emits thousands of compiler lines. Treat that
|
||||
// output as a heartbeat so the slice keeps creeping forward, but only publish a progress
|
||||
// line when the rounded percentage actually changes.
|
||||
if (_fraction >= CompileFraction)
|
||||
{
|
||||
_fraction = Math.Min(0.97, _fraction + 0.0015);
|
||||
Emit();
|
||||
}
|
||||
}
|
||||
|
||||
private void Emit()
|
||||
{
|
||||
var percent = Interpolate(_fraction);
|
||||
if (percent == _reportedPercent) return;
|
||||
_reportedPercent = percent;
|
||||
_reporter.Progress(_stage, _message, percent);
|
||||
}
|
||||
|
||||
private int Interpolate(double fraction) =>
|
||||
(int)Math.Round(_start + (_end - _start) * Math.Clamp(fraction, 0, 1));
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
internal static class ProductInfo
|
||||
{
|
||||
public const string Name = "WiiCompiled";
|
||||
public const string Version = "0.2.31";
|
||||
}
|
||||
|
||||
/// <summary>One installed product's record inside install-state.json.</summary>
|
||||
internal sealed class ProductInstallRecord
|
||||
{
|
||||
public string Profile { get; set; } = "";
|
||||
public string InstallDirectory { get; set; } = "";
|
||||
public string ExecutableName { get; set; } = "";
|
||||
public string DolSha256 { get; set; } = "";
|
||||
public string RelSha256 { get; set; } = "";
|
||||
public string BuiltUtc { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The whole flat state document this tool keeps at ~/.local/share/WiiCompiled/install-state.json.
|
||||
/// Deliberately not a fingerprint tree: local-build.sh already does its own incremental-rebuild
|
||||
/// caching, so this only needs to remember where things were installed and what they were built
|
||||
/// against, not decide when to rebuild.
|
||||
/// </summary>
|
||||
internal sealed class InstallState
|
||||
{
|
||||
public int SchemaVersion { get; set; } = 1;
|
||||
public string Workspace { get; set; } = "";
|
||||
public List<ProductInstallRecord> Products { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
using System.Security.Cryptography;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
private static async Task<int> Main(string[] args)
|
||||
{
|
||||
// Checked anywhere in argv, not just args[0]: AppRun (Launcher/build-appimage.sh) prepends
|
||||
// --workspace <cache> ahead of whatever the caller passed, so these can't assume position 0.
|
||||
if (args.Length == 0 || args.Contains("-h") || args.Contains("--help")) { PrintUsage(); return 0; }
|
||||
if (args.Contains("--version")) { Console.WriteLine(ProductInfo.Version); return 0; }
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
// Replaces CancellationSignal.cs's named-EventWaitHandle IPC (Windows-only): SIGINT/SIGTERM
|
||||
// are the portable, standard way for a parent (Wheel Wizard or a shell) to cancel this
|
||||
// process and the build it spawned.
|
||||
using var sigint = System.Runtime.InteropServices.PosixSignalRegistration.Create(
|
||||
System.Runtime.InteropServices.PosixSignal.SIGINT, context => { context.Cancel = true; cts.Cancel(); });
|
||||
using var sigterm = System.Runtime.InteropServices.PosixSignalRegistration.Create(
|
||||
System.Runtime.InteropServices.PosixSignal.SIGTERM, context => { context.Cancel = true; cts.Cancel(); });
|
||||
return await RunAsync(args, cts);
|
||||
}
|
||||
|
||||
private static async Task<int> RunAsync(string[] args, CancellationTokenSource cts)
|
||||
{
|
||||
// AppRun (Launcher/build-appimage.sh) invokes this as `wiicompiled-setup --workspace
|
||||
// <cache> <command> [options]` - a global flag ahead of the subcommand - so the command
|
||||
// word is whichever token isn't part of a --flag/value pair, not strictly args[0].
|
||||
var (command, flags) = ParseArgs(args);
|
||||
if (command is null) { PrintUsage(); return 1; }
|
||||
var progressJson = flags.ContainsKey("progress-json");
|
||||
IInstallReporter reporter = progressJson ? new NdjsonInstallReporter() : new ConsoleInstallReporter();
|
||||
|
||||
try
|
||||
{
|
||||
switch (command)
|
||||
{
|
||||
case "install":
|
||||
await InstallAsync(flags, reporter, cts.Token);
|
||||
break;
|
||||
case "uninstall":
|
||||
Uninstall();
|
||||
break;
|
||||
case "launch-base":
|
||||
return Launch("base", flags);
|
||||
case "launch-retro":
|
||||
return Launch("retro-rewind", flags);
|
||||
case "check-products":
|
||||
CheckProducts();
|
||||
break;
|
||||
default:
|
||||
Console.Error.WriteLine($"Unknown command: {command}");
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
(reporter as NdjsonInstallReporter)?.Success(flags.GetValueOrDefault("install-dir") ?? "");
|
||||
return 0;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Console.Error.WriteLine("Cancelled.");
|
||||
(reporter as NdjsonInstallReporter)?.Failure("cancelled");
|
||||
return 130;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"error: {ex.Message}");
|
||||
(reporter as NdjsonInstallReporter)?.Failure(ex.Message);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task InstallAsync(Dictionary<string, string?> flags, IInstallReporter reporter, CancellationToken token)
|
||||
{
|
||||
var retroDir = flags.GetValueOrDefault("retro-dir");
|
||||
var installsRetro = !string.IsNullOrEmpty(retroDir);
|
||||
var downloadPayload = flags.ContainsKey("download-retro-wfc-payload");
|
||||
var skipPayload = flags.ContainsKey("skip-retro-wfc-payload");
|
||||
if (installsRetro)
|
||||
{
|
||||
if (downloadPayload == skipPayload)
|
||||
throw new ArgumentException(
|
||||
"Choose exactly one Retro-WFC mode: --download-retro-wfc-payload or --skip-retro-wfc-payload.");
|
||||
}
|
||||
else if (downloadPayload || skipPayload)
|
||||
{
|
||||
throw new ArgumentException("A Retro-WFC payload option is valid only with --retro-dir.");
|
||||
}
|
||||
|
||||
// Canonicalizes to the exact RetroRewind6 folder (accepting a parent folder or a symlink),
|
||||
// the same validation Windows applies via this same shared method - local-build.sh's own
|
||||
// check further down is a simpler backstop, not the primary validation anymore.
|
||||
if (installsRetro) retroDir = RetroRewindSource.ResolveRetroRewind6(retroDir!);
|
||||
|
||||
var workspace = flags.GetValueOrDefault("workspace") ?? WorkspaceLocator.FindFrom(AppContext.BaseDirectory);
|
||||
var manifest = ProjectManifest.Load(Path.Combine(workspace, "projects", "mkwii", "recomp.yml"));
|
||||
var assetsDir = Path.Combine(workspace, "Assets");
|
||||
|
||||
reporter.Progress(InstallStages.Validate, "Checking prerequisites", 1);
|
||||
if (flags.TryGetValue("game", out var isoPath) && !string.IsNullOrEmpty(isoPath))
|
||||
{
|
||||
await DiscTool.ValidateAndExtractAsync(isoPath, manifest, assetsDir, workspace,
|
||||
flags.GetValueOrDefault("disc-tool-bin"), reporter, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
var dol = Path.Combine(assetsDir, "main.dol");
|
||||
var rel = Path.Combine(assetsDir, "StaticR.rel");
|
||||
if (!File.Exists(dol) || !File.Exists(rel))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"No --game ISO was given and Assets/main.dol + Assets/StaticR.rel are not already present. " +
|
||||
"Either pass --game <path-to-iso>, or extract them yourself first (see translator/README.md).");
|
||||
}
|
||||
}
|
||||
|
||||
var state = JsonState.TryRead<InstallState>(StatePath) ?? new InstallState { Workspace = workspace };
|
||||
state.Workspace = workspace;
|
||||
|
||||
var profile = installsRetro ? "both" : "base";
|
||||
var profiles = installsRetro ? new[] { "base", "retro-rewind" } : new[] { "base" };
|
||||
var baseInstallDir = installsRetro ? DefaultInstallDir("base") : null;
|
||||
var installDir = flags.GetValueOrDefault("install-dir") ?? DefaultInstallDir(installsRetro ? "retro-rewind" : "base");
|
||||
|
||||
string? retroWfcOfflineDir = null;
|
||||
if (downloadPayload)
|
||||
{
|
||||
// Reused if a previous install already downloaded and it's still valid - matches
|
||||
// Windows's own reuse-if-valid behavior instead of re-downloading on every install.
|
||||
var cacheDir = Path.Combine(workspace, "generated", "retro-wfc-payload");
|
||||
reporter.Progress(InstallStages.Validate, "Preparing the Retro-WFC payload", 1);
|
||||
try
|
||||
{
|
||||
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir);
|
||||
}
|
||||
catch (InvalidDataException)
|
||||
{
|
||||
await RetroWfcPayload.DownloadRetroWfcPayloadAsync(
|
||||
RetroWfcPayload.CurrentRetroWfcPayloadUri, cacheDir, token);
|
||||
}
|
||||
retroWfcOfflineDir = cacheDir;
|
||||
}
|
||||
|
||||
await BuildRunner.RunAsync(
|
||||
workspace, profile, installDir, baseInstallDir,
|
||||
retroDir,
|
||||
retroWfcOfflineDir,
|
||||
skipPayload,
|
||||
flags.ContainsKey("force-clean-build"),
|
||||
flags.GetValueOrDefault("translator-bin"),
|
||||
flags.GetValueOrDefault("cc"),
|
||||
flags.GetValueOrDefault("cxx"),
|
||||
flags.GetValueOrDefault("fuse-ld"),
|
||||
flags.GetValueOrDefault("cmake"),
|
||||
flags.GetValueOrDefault("ninja"),
|
||||
flags.GetValueOrDefault("native-prebuilt-dir"),
|
||||
reporter, token);
|
||||
|
||||
reporter.Progress(InstallStages.Shortcuts, "Creating shortcuts", 98);
|
||||
var dolSha = Sha256Of(Path.Combine(assetsDir, "main.dol"));
|
||||
var relSha = Sha256Of(Path.Combine(assetsDir, "StaticR.rel"));
|
||||
|
||||
foreach (var p in profiles)
|
||||
{
|
||||
var dir = p == "base" ? (baseInstallDir ?? installDir) : installDir;
|
||||
var exeName = p == "base" ? "WiiCompiled" : "RetroRewind";
|
||||
var displayName = p == "base" ? "WiiCompiled (base game)" : "WiiCompiled (Retro Rewind)";
|
||||
state.Products.RemoveAll(r => r.Profile == p);
|
||||
state.Products.Add(new ProductInstallRecord
|
||||
{
|
||||
Profile = p,
|
||||
InstallDirectory = dir,
|
||||
ExecutableName = exeName,
|
||||
DolSha256 = dolSha,
|
||||
RelSha256 = relSha,
|
||||
BuiltUtc = DateTime.UtcNow.ToString("O"),
|
||||
});
|
||||
DesktopEntry.Create(p, displayName, Path.Combine(dir, exeName));
|
||||
}
|
||||
JsonState.Write(StatePath, state);
|
||||
|
||||
// The runtime reads course/texture/audio data live from dvd_root at every launch, not just
|
||||
// at translation time - without this the game fatally errors the instant it needs any file
|
||||
// that isn't main.dol/StaticR.rel. Linux has no --portable flag, so this is always the
|
||||
// per-user Config.toml (RuntimeConfiguration.ResolveConfigPath's Windows-only portable-root
|
||||
// lookup has nothing to find here either way).
|
||||
var configPath = RuntimeConfiguration.ApplicationDataConfigPath;
|
||||
var dataDir = Path.Combine(assetsDir, "DATA");
|
||||
if (Directory.Exists(dataDir))
|
||||
{
|
||||
RuntimeConfiguration.SetDvdRoot(configPath, dataDir);
|
||||
}
|
||||
if (installsRetro)
|
||||
{
|
||||
RuntimeConfiguration.SetRetroRewindRoot(configPath, retroDir!);
|
||||
}
|
||||
|
||||
reporter.Progress(InstallStages.Shortcuts, "Install complete", 99);
|
||||
}
|
||||
|
||||
private static void Uninstall()
|
||||
{
|
||||
// Matches Windows: UninstallService.cs removes the whole install directory unconditionally -
|
||||
// there is no partial-product uninstall on either platform.
|
||||
var state = JsonState.TryRead<InstallState>(StatePath) ?? new InstallState();
|
||||
foreach (var record in state.Products.ToList())
|
||||
{
|
||||
if (Directory.Exists(record.InstallDirectory))
|
||||
{
|
||||
Directory.Delete(record.InstallDirectory, recursive: true);
|
||||
}
|
||||
DesktopEntry.Remove(record.Profile);
|
||||
state.Products.Remove(record);
|
||||
Console.WriteLine($"Removed {record.Profile} from {record.InstallDirectory}");
|
||||
}
|
||||
JsonState.Write(StatePath, state);
|
||||
}
|
||||
|
||||
private static int Launch(string profile, Dictionary<string, string?> flags)
|
||||
{
|
||||
var state = JsonState.TryRead<InstallState>(StatePath);
|
||||
var record = state?.Products.FirstOrDefault(r => r.Profile == profile);
|
||||
if (record is null)
|
||||
{
|
||||
var installHint = profile == "retro-rewind"
|
||||
? "install --retro-dir <RetroRewind6> {--download-retro-wfc-payload | --skip-retro-wfc-payload}"
|
||||
: $"install --profile {profile}";
|
||||
Console.Error.WriteLine($"{profile} is not installed. Run '{installHint}' first.");
|
||||
return 1;
|
||||
}
|
||||
var exePath = Path.Combine(record.InstallDirectory, record.ExecutableName);
|
||||
if (!File.Exists(exePath))
|
||||
{
|
||||
Console.Error.WriteLine($"Installed executable is missing: {exePath}. Run 'install --profile {profile}' again.");
|
||||
return 1;
|
||||
}
|
||||
var startInfo = new System.Diagnostics.ProcessStartInfo(exePath)
|
||||
{
|
||||
WorkingDirectory = record.InstallDirectory,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
using var process = System.Diagnostics.Process.Start(startInfo);
|
||||
process?.WaitForExit();
|
||||
return process?.ExitCode ?? 1;
|
||||
}
|
||||
|
||||
private static void CheckProducts()
|
||||
{
|
||||
var state = JsonState.TryRead<InstallState>(StatePath);
|
||||
if (state is null || state.Products.Count == 0)
|
||||
{
|
||||
Console.WriteLine("Nothing installed.");
|
||||
return;
|
||||
}
|
||||
var assetsDir = Path.Combine(state.Workspace, "Assets");
|
||||
var currentDol = Sha256IfExists(Path.Combine(assetsDir, "main.dol"));
|
||||
var currentRel = Sha256IfExists(Path.Combine(assetsDir, "StaticR.rel"));
|
||||
foreach (var record in state.Products)
|
||||
{
|
||||
var exePath = Path.Combine(record.InstallDirectory, record.ExecutableName);
|
||||
var present = File.Exists(exePath);
|
||||
var stale = present && (currentDol != record.DolSha256 || currentRel != record.RelSha256);
|
||||
var status = !present ? "MISSING" : stale ? "STALE (game assets changed since last build)" : "current";
|
||||
Console.WriteLine($"{record.Profile,-14} {status,-45} {record.InstallDirectory}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string Sha256Of(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string? Sha256IfExists(string path) => File.Exists(path) ? Sha256Of(path) : null;
|
||||
|
||||
private static string StatePath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WiiCompiled", "install-state.json");
|
||||
|
||||
private static string DefaultInstallDir(string profile) => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WiiCompiled", "Install",
|
||||
profile == "base" ? "Base" : "RetroRewind");
|
||||
|
||||
/// <summary>
|
||||
/// A single pass that finds both the command word and every --flag[=value] pair, regardless
|
||||
/// of order - a --flag may appear before or after the command (see the AppRun caller note in
|
||||
/// RunAsync). The first token that is neither a --flag nor a value already consumed by the
|
||||
/// preceding --flag is taken as the command.
|
||||
/// </summary>
|
||||
private static (string? Command, Dictionary<string, string?> Flags) ParseArgs(string[] args)
|
||||
{
|
||||
string? command = null;
|
||||
var flags = new Dictionary<string, string?>();
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
var arg = args[i];
|
||||
if (arg.StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
var name = arg[2..];
|
||||
if (i + 1 < args.Length && !args[i + 1].StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
flags[name] = args[++i];
|
||||
}
|
||||
else
|
||||
{
|
||||
flags[name] = null; // boolean flag
|
||||
}
|
||||
}
|
||||
else if (command is null)
|
||||
{
|
||||
command = arg;
|
||||
}
|
||||
}
|
||||
return (command, flags);
|
||||
}
|
||||
|
||||
private static void PrintUsage()
|
||||
{
|
||||
Console.WriteLine("""
|
||||
Usage: wiicompiled-setup <command> [options]
|
||||
|
||||
install [--game ISO_PATH] [--install-dir DIR] [--retro-dir DIR
|
||||
{--download-retro-wfc-payload | --skip-retro-wfc-payload}]
|
||||
[--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH]
|
||||
[--cc PATH] [--cxx PATH] [--fuse-ld NAME_OR_PATH] [--cmake PATH] [--ninja PATH]
|
||||
[--native-prebuilt-dir DIR] [--progress-json] [--workspace DIR]
|
||||
uninstall
|
||||
launch-base
|
||||
launch-retro
|
||||
check-products
|
||||
--version
|
||||
""");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
/// <summary>
|
||||
/// The handful of facts this tool needs out of projects/mkwii/recomp.yml. Parsed literally line by
|
||||
/// line - the same approach Launcher/NativeBuildFlags.ps1's Get-MkwProjectPins and
|
||||
/// Launcher/local-build.sh already use - rather than pulling in a YAML library, since the manifest
|
||||
/// is machine-written with a fixed shape.
|
||||
/// </summary>
|
||||
internal sealed class ProjectManifest
|
||||
{
|
||||
public required string GameId { get; init; }
|
||||
public required string Region { get; init; }
|
||||
public required string DolSha256 { get; init; }
|
||||
public required string RelSha256 { get; init; }
|
||||
|
||||
public static ProjectManifest Load(string path)
|
||||
{
|
||||
if (!File.Exists(path)) throw new FileNotFoundException("Translation project file is missing", path);
|
||||
|
||||
string? gameId = null, region = null, dolSha = null, relSha = null;
|
||||
string section = "";
|
||||
string inputKey = "";
|
||||
|
||||
foreach (var raw in File.ReadLines(path))
|
||||
{
|
||||
var line = Regex.Replace(raw, "#.*$", "");
|
||||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||
|
||||
var sectionMatch = Regex.Match(line, "^([A-Za-z0-9_]+):");
|
||||
if (sectionMatch.Success)
|
||||
{
|
||||
section = sectionMatch.Groups[1].Value;
|
||||
inputKey = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (section == "inputs")
|
||||
{
|
||||
var keyMatch = Regex.Match(line, @"^\s{2}([A-Za-z0-9_]+):\s*$");
|
||||
if (keyMatch.Success) { inputKey = keyMatch.Groups[1].Value; continue; }
|
||||
|
||||
var shaMatch = Regex.Match(line, @"^\s*sha256:\s*([0-9a-fA-F]{64})\s*$");
|
||||
if (shaMatch.Success)
|
||||
{
|
||||
var value = shaMatch.Groups[1].Value.ToLowerInvariant();
|
||||
if (inputKey == "dol") dolSha = value;
|
||||
else if (inputKey == "rel") relSha = value;
|
||||
}
|
||||
}
|
||||
else if (section == "project")
|
||||
{
|
||||
var idMatch = Regex.Match(line, @"^\s*game_id:\s*(\S+)\s*$");
|
||||
if (idMatch.Success) gameId = idMatch.Groups[1].Value;
|
||||
|
||||
var regionMatch = Regex.Match(line, @"^\s*region:\s*(\S+)\s*$");
|
||||
if (regionMatch.Success) region = regionMatch.Groups[1].Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (gameId is null || region is null || dolSha is null || relSha is null)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"{path} does not pin game_id/region/dol.sha256/rel.sha256; the project file is not the shape this tool expects.");
|
||||
}
|
||||
|
||||
return new ProjectManifest { GameId = gameId, Region = region, DolSha256 = dolSha, RelSha256 = relSha };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
|
||||
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
|
||||
<Version>0.2.31</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
|
||||
<DebugType>embedded</DebugType>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
/// <summary>
|
||||
/// Finds the repo checkout this tool is running from by walking up from its own directory looking
|
||||
/// for Launcher/local-build.sh - this tool operates directly on a git checkout (no bundled/staged
|
||||
/// workspace copy), so there is no installed "Toolkit" layout to anchor on the way the Windows
|
||||
/// installer's Installation.cs does.
|
||||
/// </summary>
|
||||
internal static class WorkspaceLocator
|
||||
{
|
||||
private const int MaxSearchDepth = 6;
|
||||
|
||||
public static string FindFrom(string startDirectory)
|
||||
{
|
||||
var current = new DirectoryInfo(startDirectory);
|
||||
for (var level = 0; level <= MaxSearchDepth && current is not null; level++, current = current.Parent)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "Launcher", "local-build.sh")))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException(
|
||||
"Could not find the WiiCompiled repository (looked for Launcher/local-build.sh walking up " +
|
||||
$"from {startDirectory}). Pass --workspace <path-to-checkout> explicitly.");
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Bridges a frontend-owned, named Windows event into the cancellation token used by setup.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal enum AppMode
|
||||
{
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal sealed record RetroRewindCompileInputs(
|
||||
string RetroRewindRoot,
|
||||
+4
-3
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal static class ConsoleCommands
|
||||
{
|
||||
@@ -63,8 +64,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();
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal static class GameLaunchService
|
||||
{
|
||||
@@ -0,0 +1,165 @@
|
||||
using System.Diagnostics;
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal static class InputValidation
|
||||
{
|
||||
private static readonly HashSet<string> SupportedDiscImageExtensions = new(
|
||||
[".iso", ".gcm", ".gcz", ".ciso", ".wbfs", ".wia", ".rvz"],
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static void ValidateExtension(string gamePath)
|
||||
{
|
||||
if (!File.Exists(gamePath))
|
||||
throw new FileNotFoundException("The selected game image does not exist.", gamePath);
|
||||
var extension = Path.GetExtension(gamePath);
|
||||
if (!SupportedDiscImageExtensions.Contains(extension))
|
||||
throw new InvalidDataException(
|
||||
"Select a complete Wii disc image in ISO, GCM, GCZ, CISO, WBFS, WIA, or RVZ format.");
|
||||
}
|
||||
|
||||
public static async Task<DiscHeader> ReadDiscHeaderAsync(string nodTool, string gamePath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ValidateExtension(gamePath);
|
||||
var result = await ProcessRunner.RunAsync(nodTool,
|
||||
["info", Path.GetFullPath(gamePath)], null, cancellationToken);
|
||||
if (result.ExitCode != 0)
|
||||
throw new InvalidDataException("nodtool could not read this disc image. " + result.CombinedOutput.Trim());
|
||||
|
||||
var info = NodToolInfoParser.Parse(result.StandardOutput);
|
||||
return new DiscHeader
|
||||
{
|
||||
GameId = info.GameId,
|
||||
InternalName = info.Title,
|
||||
Region = RegionFromGameId(info.GameId),
|
||||
Revision = info.Revision,
|
||||
};
|
||||
}
|
||||
|
||||
private static string RegionFromGameId(string gameId) => gameId.Length >= 4
|
||||
? gameId[3] switch
|
||||
{
|
||||
'P' => "PAL",
|
||||
'E' => "NTSC-U",
|
||||
'J' => "NTSC-J",
|
||||
'K' => "Korea",
|
||||
'W' => "Taiwan",
|
||||
_ => gameId[3].ToString(),
|
||||
}
|
||||
: "Unknown";
|
||||
|
||||
public static void EnsureCompatibleDisc(DiscHeader header, PayloadManifest manifest)
|
||||
{
|
||||
if (!header.GameId.Equals(manifest.ExpectedGameId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"This build supports Mario Kart Wii PAL ({manifest.ExpectedGameId}). " +
|
||||
$"The selected image is {header.GameId} ({header.InternalName}, {header.Region}).");
|
||||
}
|
||||
}
|
||||
|
||||
// Thin forwarding wrappers: the actual download/RSA-verification logic lives in
|
||||
// WiiCompiled.Setup.Common.RetroWfcPayload (shared with WiiCompiled.Setup.Linux) so there's one
|
||||
// copy of it, not two. Kept under these names so every existing call site here
|
||||
// (ProductRepairService.cs, LocalBuildService.cs, Installation.cs, SelfTests.cs) is unchanged.
|
||||
public const string CurrentRetroWfcPayloadUri = RetroWfcPayload.CurrentRetroWfcPayloadUri;
|
||||
|
||||
public static string ValidateStagedRetroWfcPayloadDirectory(string stagedDirectory,
|
||||
RSAParameters? signingKey = null) =>
|
||||
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(stagedDirectory, signingKey);
|
||||
|
||||
public static string ResolveRetroWfcPayloadFile(string stagedDirectory,
|
||||
RSAParameters? signingKey = null) =>
|
||||
RetroWfcPayload.ResolveRetroWfcPayloadFile(stagedDirectory, signingKey);
|
||||
|
||||
public static string ComputeRetroWfcPayloadSha256(string stagedDirectory,
|
||||
RSAParameters? signingKey = null) =>
|
||||
RetroWfcPayload.ComputeRetroWfcPayloadSha256(stagedDirectory, signingKey);
|
||||
|
||||
public static void ValidateRetroWfcPayloadUri(string uriText) =>
|
||||
RetroWfcPayload.ValidateRetroWfcPayloadUri(uriText);
|
||||
|
||||
public static Task<RetroWfcPayloadSnapshot> DownloadRetroWfcPayloadAsync(string uriText,
|
||||
string destinationDirectory, CancellationToken cancellationToken) =>
|
||||
RetroWfcPayload.DownloadRetroWfcPayloadAsync(uriText, destinationDirectory, cancellationToken);
|
||||
|
||||
internal static bool IsTransientRetroWfcDownloadFailure(Exception exception,
|
||||
CancellationToken cancellationToken) =>
|
||||
RetroWfcPayload.IsTransientRetroWfcDownloadFailure(exception, cancellationToken);
|
||||
|
||||
public static string Sha256File(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError)
|
||||
{
|
||||
public string CombinedOutput => StandardOutput + Environment.NewLine + StandardError;
|
||||
}
|
||||
|
||||
internal static class ProcessRunner
|
||||
{
|
||||
/// <summary>Runs a redirected child process to completion. <paramref name="configure"/> sets up a
|
||||
/// working directory or scrubbed environment; <paramref name="capture"/> is off for callers that only
|
||||
/// forward output live, so a build's output isn't buffered in memory for nobody to read.</summary>
|
||||
public static async Task<ProcessResult> RunAsync(string executable, IReadOnlyList<string> arguments,
|
||||
Action<string>? output, CancellationToken cancellationToken,
|
||||
Action<ProcessStartInfo>? configure = null, bool capture = true,
|
||||
Action<Exception>? onTerminationFailure = null)
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = executable,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
foreach (var argument in arguments) info.ArgumentList.Add(argument);
|
||||
configure?.Invoke(info);
|
||||
|
||||
using var process = new Process { StartInfo = info, EnableRaisingEvents = true };
|
||||
var stdout = new List<string>();
|
||||
var stderr = new List<string>();
|
||||
process.OutputDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stdout.Add(e.Data); output?.Invoke(e.Data); } };
|
||||
process.ErrorDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stderr.Add(e.Data); output?.Invoke(e.Data); } };
|
||||
if (!process.Start()) throw new InvalidOperationException($"Could not start {executable}.");
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
await WaitForExitAsync(process, cancellationToken, onTerminationFailure);
|
||||
return new ProcessResult(process.ExitCode, string.Join(Environment.NewLine, stdout),
|
||||
string.Join(Environment.NewLine, stderr));
|
||||
}
|
||||
|
||||
public static async Task WaitForExitAsync(Process process, CancellationToken cancellationToken,
|
||||
Action<Exception>? onTerminationFailure = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!process.HasExited) process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
onTerminationFailure?.Invoke(ex);
|
||||
}
|
||||
await process.WaitForExitAsync(CancellationToken.None);
|
||||
process.WaitForExit();
|
||||
throw;
|
||||
}
|
||||
process.WaitForExit();
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// A fail-fast, cross-process lock covering install, repair and launch operations for one install
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Stable stage identifiers reported by <c>--progress-json</c>. These are part of the public
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Owns one temporary directory for an install operation. The name carries the installation's scope
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal enum InstallTransactionEntryKind
|
||||
{
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>Provenance written by the bundled build script next to every product it produces.</summary>
|
||||
internal sealed class LocalBuildProvenance
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Names of the installed/staged layout. Not cosmetic: payload and toolkit identities hash relative paths
|
||||
@@ -26,7 +26,7 @@ internal static class InstalledLayout
|
||||
/// </summary>
|
||||
public static readonly string[] DependencyNames =
|
||||
[
|
||||
"abseil-cpp", "cppwinrt", "dawn_prebuilt", "fmt", "freetype", "imgui", "native_prebuilt",
|
||||
"abseil-cpp", "cppwinrt", "dawn_prebuilt", "fmt", "freetype", "imgui", "libusb", "native_prebuilt",
|
||||
"png", "SDL", "sqlite3", "tracy", "xxhash", "zlib", "zstd"
|
||||
];
|
||||
}
|
||||
+31
-18
@@ -1,4 +1,6 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal sealed class InstallerEngine
|
||||
{
|
||||
@@ -20,6 +22,9 @@ internal sealed class InstallerEngine
|
||||
var existing = new Installation(installDirectory);
|
||||
|
||||
PortableInstallHealing.HealMovedInstall(existing, _reporter);
|
||||
// Capture this before publishing anything. An existing installation may be repaired or
|
||||
// updated by this invocation, but those operations must not recreate the user's shortcuts.
|
||||
var firstInstall = !existing.IsPresent;
|
||||
var previousState = existing.ReadInstallState();
|
||||
using var scratch = Directory.Exists(installDirectory)
|
||||
? InstallScratchSpace.CreateInsideInstall(installDirectory, _reporter)
|
||||
@@ -57,9 +62,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 +82,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
|
||||
@@ -137,7 +142,7 @@ internal sealed class InstallerEngine
|
||||
updatedState.RetroRewindInstalled, candidateRuntimeAssetsFingerprint,
|
||||
remainingCancellation);
|
||||
Publish(staging, installDirectory, canonicalRetroRoot, updatedState,
|
||||
releaseEntries, remainingCancellation);
|
||||
releaseEntries, remainingCancellation, createShortcuts: firstInstall);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,19 +158,20 @@ internal sealed class InstallerEngine
|
||||
|
||||
if (reusableGameAssets is null)
|
||||
{
|
||||
await ExtractGameAssetsAsync(dolphinTool, options.GamePath,
|
||||
await ExtractGameAssetsAsync(nodTool, options.GamePath,
|
||||
Path.Combine(staging, "GameAssets"), manifest, cancellationToken);
|
||||
}
|
||||
|
||||
await PublishToolkitAndReconcileProductsAsync(existing, staging, workspace, manifest,
|
||||
previousState, options, canonicalRetroRoot, retroCompileInputs,
|
||||
publishGameAssets: reusableGameAssets is null, cancellationToken);
|
||||
publishGameAssets: reusableGameAssets is null, createShortcuts: firstInstall,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
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) =>
|
||||
@@ -184,7 +190,7 @@ internal sealed class InstallerEngine
|
||||
string stagedWorkspace, PayloadManifest manifest, InstallState? previousState,
|
||||
InstallOptions options, string? canonicalRetroRoot,
|
||||
RetroRewindCompileInputs? retroCompileInputs,
|
||||
bool publishGameAssets, CancellationToken cancellationToken)
|
||||
bool publishGameAssets, bool createShortcuts, CancellationToken cancellationToken)
|
||||
{
|
||||
var installDirectory = existing.Root;
|
||||
|
||||
@@ -212,7 +218,8 @@ internal sealed class InstallerEngine
|
||||
if (publishGameAssets) AddComponent(entries, staging, installDirectory, "GameAssets");
|
||||
|
||||
Publish(staging, installDirectory, canonicalRetroRoot, state,
|
||||
entries, cancellationToken, progressPercent: 8, completionPercent: 10);
|
||||
entries, cancellationToken, progressPercent: 8, completionPercent: 10,
|
||||
createShortcuts: createShortcuts);
|
||||
|
||||
_reporter.Progress(InstallStages.BuildBase,
|
||||
"Producing the installed products with the published toolkit...", 11);
|
||||
@@ -275,7 +282,7 @@ internal sealed class InstallerEngine
|
||||
private void Publish(string staging, string installDirectory,
|
||||
string? canonicalRetroRoot, InstallState state,
|
||||
List<InstallTransactionEntry> entries, CancellationToken cancellationToken,
|
||||
int progressPercent = 95, int completionPercent = 99)
|
||||
int progressPercent = 95, int completionPercent = 99, bool createShortcuts = false)
|
||||
{
|
||||
entries.Add(InstallTransactionEntry.Directory(Path.Combine(staging, "licenses"),
|
||||
Path.Combine(installDirectory, "licenses")));
|
||||
@@ -320,7 +327,8 @@ internal sealed class InstallerEngine
|
||||
{
|
||||
ShellIntegration.RegisterUninstaller(installDirectory, state.RetroRewindInstalled);
|
||||
}
|
||||
ShellIntegration.CreateShortcuts(installDirectory);
|
||||
if (createShortcuts)
|
||||
ShellIntegration.CreateShortcuts(installDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -469,18 +477,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)
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Both"/> runs one retro-aware translation and compiles the two products from a single
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal enum RetroWfcPayloadMode
|
||||
{
|
||||
@@ -54,12 +55,6 @@ internal sealed class PayloadManifest
|
||||
public string NativeToolchainFingerprint { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One validated, content-identified download in operation-owned scratch space. Callers use this
|
||||
/// exact directory for both the update decision and any resulting build.
|
||||
/// </summary>
|
||||
internal sealed record RetroWfcPayloadSnapshot(string Directory, string Sha256, long ByteLength);
|
||||
|
||||
internal sealed class DiscHeader
|
||||
{
|
||||
[JsonPropertyName("game_id")]
|
||||
+1
-1
@@ -2,7 +2,7 @@ using System.IO.Compression;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal sealed class PayloadArchive : IDisposable
|
||||
{
|
||||
@@ -0,0 +1,54 @@
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// A portable root can be moved or renamed between operations. Every installed-host operation that
|
||||
/// reads <c>install-state.json</c> passes through here first so exactly one place decides what a
|
||||
/// moved installation means, and so a non-portable installation is never touched.
|
||||
/// </summary>
|
||||
internal static class PortableInstallHealing
|
||||
{
|
||||
/// <summary>
|
||||
/// Reconciles a moved portable installation with its recorded location: the state file adopts the
|
||||
/// directory it was actually found in, and the native build tree is discarded because its
|
||||
/// CMake cache holds absolute paths from the old location. Returns whether anything was healed.
|
||||
/// </summary>
|
||||
public static bool HealMovedInstall(Installation installation, IInstallReporter? reporter = null)
|
||||
{
|
||||
// Guard: an ordinary installation that disagrees with its state file is a real problem for
|
||||
// the operation to report, not something to silently rewrite.
|
||||
if (PortableRoot.TryFind(installation.Root) is null) return false;
|
||||
|
||||
var state = installation.ReadInstallState();
|
||||
if (state is not { SchemaVersion: 1 } || string.IsNullOrWhiteSpace(state.InstallDir)) return false;
|
||||
|
||||
string recorded;
|
||||
try
|
||||
{
|
||||
recorded = FileSystemUtilities.NormalizePath(state.InstallDir);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
|
||||
{
|
||||
recorded = state.InstallDir;
|
||||
}
|
||||
if (recorded.Equals(installation.Root, StringComparison.OrdinalIgnoreCase)) return false;
|
||||
|
||||
var previous = state.InstallDir;
|
||||
state.InstallDir = installation.Root;
|
||||
JsonState.Write(installation.InstallStatePath, state);
|
||||
|
||||
// The configured native build directory bakes absolute source, toolchain, and output paths
|
||||
// into CMakeCache.txt. After a move it is unusable and would fail the next configure rather
|
||||
// than being reused, so it is removed and reconfigured from scratch on the next build.
|
||||
var nativeBuild = Path.Combine(installation.WorkspaceDirectory, "native-build");
|
||||
var hadNativeBuild = Directory.Exists(nativeBuild);
|
||||
if (hadNativeBuild) FileSystemUtilities.DeleteDirectoryIfExists(nativeBuild);
|
||||
|
||||
reporter?.Diagnostic(
|
||||
$"This portable installation moved from {previous} to {installation.Root}. " +
|
||||
"The recorded location was updated" +
|
||||
(hadNativeBuild ? " and the location-bound native build cache was discarded." : "."));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Reconciles installed products against the canonical Retro Rewind install Wheel Wizard owns: the
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
@@ -121,7 +121,7 @@ internal static class PlatformChecks
|
||||
internal static class ProductInfo
|
||||
{
|
||||
public const string Name = "WiiCompiled";
|
||||
public const string Version = "0.2.22";
|
||||
public const string Version = "0.2.31";
|
||||
|
||||
/// <summary>
|
||||
/// The setup executable is copied into the installation under this name. It is the launcher and
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Refuses to replace installed products while one of them is running: publishing renames the
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// The one path by which a product receives its copied runtime assets, shared by install and repair.
|
||||
+9
-8
@@ -1,7 +1,8 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Security.Cryptography;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal static class SelfTests
|
||||
{
|
||||
@@ -143,17 +144,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 +1011,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 +1178,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: {}");
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal static class ShellIntegration
|
||||
{
|
||||
+4
-3
@@ -1,7 +1,8 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Content identity of everything that decides what the locally produced executables contain.
|
||||
@@ -78,12 +79,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);
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal static class UninstallService
|
||||
{
|
||||
+5
-2
@@ -5,13 +5,16 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>WiiCompiled.Setup</AssemblyName>
|
||||
<RootNamespace>WiiCompiled.Setup</RootNamespace>
|
||||
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Version>0.2.22</Version>
|
||||
<Version>0.2.31</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Command-line installer and launcher for WiiCompiled</Description>
|
||||
<DebugType>embedded</DebugType>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
|
||||
internal static class WorkspaceTimestamps
|
||||
Executable
+303
@@ -0,0 +1,303 @@
|
||||
#!/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, 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.
|
||||
# A pruned native clang/lld/cmake/ninja toolchain (see prepare-portable-tools.sh) is bundled the
|
||||
# same way - AppRun passes --cc/--cxx/--fuse-ld/--cmake/--ninja so local-build.sh never has to find
|
||||
# a system compiler, CMake, or Ninja. It still shells out to system pkg-config and Vulkan headers,
|
||||
# matching Launcher/local-build.sh's own remaining prerequisites. A precompiled aurora +
|
||||
# third-party package (see Prepare-NativePrebuilt.sh) is bundled the same way too - AppRun passes
|
||||
# --native-prebuilt-dir so local-build.sh never compiles aurora-main from source at all.
|
||||
#
|
||||
# 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
|
||||
# out to a writable cache directory on first run, and only ever re-syncs the bundled directories
|
||||
# (runtime/, aurora-main/, projects/, local-build.sh) on a later run whose bundled version changed
|
||||
# - generated/native-build/Assets/PulsarPacks live only in that writable cache and are never
|
||||
# touched by the sync, so local-build.sh's own incremental caching survives across runs and across
|
||||
# AppImage updates. translator/ isn't part of this snapshot at all: it's published as its own
|
||||
# self-contained binary (usr/bin/translator-cli) below and never needs a writable copy. Neither
|
||||
# native-prebuilt/ nor the toolchain are copied into the cache either - both are large
|
||||
# (~90 MiB / ~500 MiB) and local-build.sh only ever reads from them - but AppRun does point
|
||||
# $CACHE/toolchain and $CACHE/native-prebuilt symlinks at the current mount on every single launch
|
||||
# (see AppRun's own comment): an AppImage's FUSE mount is at a fresh random /tmp/.mount_XXXXXX
|
||||
# every run, and CMake bakes whatever compiler/tool path it's given directly into each
|
||||
# build.ninja rule's command line, so referencing $HERE straight would change that command line -
|
||||
# and Ninja reruns any rule whose command line changed - forcing a full rebuild on every single
|
||||
# launch even though the compiler itself never actually changed. The symlink keeps the path
|
||||
# string CMake/Ninja see identical across runs while what it resolves to tracks the current mount
|
||||
# underneath.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/.." && pwd)
|
||||
|
||||
# `uname -m` reports the *kernel's* architecture, which can differ from userspace - an aarch64
|
||||
# kernel can run a 32-bit armhf userland (as shipped by 32-bit Raspberry Pi OS), same as an x86_64
|
||||
# kernel can run an i686 one. What matters here is which userspace binaries (dotnet, appimagetool)
|
||||
# will actually run, so this reads the ELF header of this script's own running bash interpreter -
|
||||
# real userspace - rather than trusting the kernel's self-report. /proc/$$/exe (not /proc/self/exe:
|
||||
# that would resolve inside the readlink subprocess below, to readlink itself, not to bash) is this
|
||||
# shell's own PID. EI_CLASS (byte 4: 1=32-bit, 2=64-bit) and e_machine (bytes 18-19: 3=EM_386,
|
||||
# 40=EM_ARM, 62=EM_X86_64, 183=EM_AARCH64) are read as plain little-endian bytes, which every
|
||||
# real-world x86/ARM Linux userland uses; ELF's big-endian encoding is a non-issue here since no
|
||||
# Linux distro ships a big-endian x86 or ARM userland.
|
||||
elf_exe=$(readlink -f "/proc/$$/exe")
|
||||
elf_class=$(od -An -t u1 -j 4 -N 1 "$elf_exe" | tr -d ' ')
|
||||
elf_machine_lo=$(od -An -t u1 -j 18 -N 1 "$elf_exe" | tr -d ' ')
|
||||
elf_machine_hi=$(od -An -t u1 -j 19 -N 1 "$elf_exe" | tr -d ' ')
|
||||
elf_machine=$(( elf_machine_hi * 256 + elf_machine_lo ))
|
||||
|
||||
# Mirrors the host-architecture detection NodToolProvider.cs already does (RuntimeInformation.
|
||||
# OSArchitecture) so this script's own dotnet RID and appimagetool selection agree with the
|
||||
# nodtool binary that same code path resolves below. local-build.sh needs no such mapping itself:
|
||||
# it just drives the native CMake configure, which already accepts x86_64 or aarch64 natively
|
||||
# (see runtime/CMakeLists.txt's CMAKE_SYSTEM_PROCESSOR check).
|
||||
case "$elf_class:$elf_machine" in
|
||||
2:62)
|
||||
dotnet_rid=linux-x64
|
||||
appimagetool_arch=x86_64
|
||||
;;
|
||||
2:183)
|
||||
dotnet_rid=linux-arm64
|
||||
appimagetool_arch=aarch64
|
||||
;;
|
||||
*)
|
||||
echo "build-appimage.sh: unsupported userspace architecture (ELF class $elf_class, machine $elf_machine) - WiiCompiled requires a 64-bit x86_64 or aarch64 userland" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
output_dir="$workspace/Launcher/dist"
|
||||
appimagetool_override=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output-dir) output_dir=$2; shift 2 ;;
|
||||
--appimagetool) appimagetool_override=$2; shift 2 ;;
|
||||
-h|--help)
|
||||
echo "Usage: build-appimage.sh [--output-dir DIR] [--appimagetool PATH]"
|
||||
exit 0
|
||||
;;
|
||||
*) echo "build-appimage.sh: unknown argument: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
appdir="$workspace/Launcher/artifacts/appimage-build/AppDir"
|
||||
rm -rf "$appdir"
|
||||
mkdir -p "$appdir/usr/bin" "$appdir/workspace/Launcher"
|
||||
|
||||
echo "Publishing the installer (self-contained $dotnet_rid)..."
|
||||
publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish"
|
||||
rm -rf "$publish_tmp"
|
||||
dotnet publish "$workspace/Launcher/WiiCompiled.Setup.Linux" -c Release -r "$dotnet_rid" \
|
||||
--self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
|
||||
-o "$publish_tmp"
|
||||
cp "$publish_tmp/WiiCompiled.Setup.Linux" "$appdir/usr/bin/wiicompiled-setup"
|
||||
chmod +x "$appdir/usr/bin/wiicompiled-setup"
|
||||
|
||||
# Published as a self-contained binary too, so an AppImage user never needs a `dotnet` SDK on
|
||||
# PATH at all - local-build.sh is told about it via --translator-bin and skips its own
|
||||
# dotnet-build-from-source step entirely (see local-build.sh's translator resolution branch).
|
||||
echo "Publishing the translator (self-contained $dotnet_rid)..."
|
||||
translator_publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish-translator"
|
||||
rm -rf "$translator_publish_tmp"
|
||||
dotnet publish "$workspace/translator/src/Translator.Cli" -c Release -r "$dotnet_rid" \
|
||||
--self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
|
||||
-o "$translator_publish_tmp"
|
||||
cp "$translator_publish_tmp/Translator.Cli" "$appdir/usr/bin/translator-cli"
|
||||
chmod +x "$appdir/usr/bin/translator-cli"
|
||||
|
||||
# Resolved via the shared WiiCompiled.Setup.Common.Cli helper (also used by Build-Installer.ps1 on
|
||||
# Windows) rather than a second curl/version-pin copy here: it downloads and caches the same way
|
||||
# NodToolProvider.cs always does (Launcher/artifacts/nodtool), so there is exactly one place that
|
||||
# knows the nodtool version/URL/platform-asset mapping.
|
||||
echo "Resolving nodtool..."
|
||||
nodtool_path=$(dotnet run --project "$workspace/Launcher/WiiCompiled.Setup.Common.Cli" -c Release -- \
|
||||
--workspace "$workspace" | tail -n1)
|
||||
cp "$nodtool_path" "$appdir/usr/bin/nodtool"
|
||||
chmod +x "$appdir/usr/bin/nodtool"
|
||||
|
||||
echo "Preparing the portable clang/lld/cmake/ninja toolchain ($appimagetool_arch)..."
|
||||
bash "$script_dir/prepare-portable-tools.sh" --arch "$appimagetool_arch"
|
||||
mkdir -p "$appdir/usr/toolchain"
|
||||
cp -a "$workspace/Launcher/artifacts/portable-tools/toolchain-$appimagetool_arch"/. "$appdir/usr/toolchain/"
|
||||
|
||||
# Precompiled aurora + third-party package (see Prepare-NativePrebuilt.sh) so a user's own
|
||||
# local-build.sh never has to compile aurora itself (~43% of local build CPU time). Re-harvesting
|
||||
# recompiles the whole aurora/Crypto++ closure with the toolchain above, so this is skipped unless
|
||||
# --print-fingerprint-only (a fast, build-free check) says the existing package no longer matches
|
||||
# the current compiler/flags/aurora/third_party sources.
|
||||
native_prebuilt_dir="$workspace/Launcher/artifacts/native-prebuilt-$appimagetool_arch"
|
||||
echo "Checking whether the precompiled aurora + third-party package ($appimagetool_arch) is current..."
|
||||
current_fingerprint=$(bash "$script_dir/Prepare-NativePrebuilt.sh" --arch "$appimagetool_arch" --print-fingerprint-only)
|
||||
package_current=0
|
||||
if [[ -f "$native_prebuilt_dir/provenance.json" ]]; then
|
||||
package_current=$(CURRENT_FINGERPRINT="$current_fingerprint" python3 - "$native_prebuilt_dir/provenance.json" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
provenance = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
current = dict(line.split("=", 1) for line in os.environ["CURRENT_FINGERPRINT"].splitlines() if line)
|
||||
fields = {
|
||||
"compiler_sha256": "CompilerSha256",
|
||||
"flag_fingerprint": "FlagFingerprint",
|
||||
"aurora_fingerprint": "AuroraSourceFingerprint",
|
||||
"third_party_fingerprint": "ThirdPartySourceFingerprint",
|
||||
}
|
||||
print(1 if all(provenance.get(v) == current.get(k) for k, v in fields.items()) else 0)
|
||||
PY
|
||||
)
|
||||
fi
|
||||
if [[ "$package_current" == "1" ]]; then
|
||||
echo "Native prebuilt package is current; reusing $native_prebuilt_dir"
|
||||
else
|
||||
echo "Native prebuilt package is missing or stale; harvesting a fresh one (compiles aurora once, can take a while)..."
|
||||
bash "$script_dir/Prepare-NativePrebuilt.sh" --arch "$appimagetool_arch"
|
||||
fi
|
||||
mkdir -p "$appdir/native-prebuilt"
|
||||
cp -a "$native_prebuilt_dir/." "$appdir/native-prebuilt/"
|
||||
|
||||
echo "Staging the bundled workspace snapshot..."
|
||||
for dir in runtime aurora-main projects; do
|
||||
cp -r "$workspace/$dir" "$appdir/workspace/$dir"
|
||||
done
|
||||
# Mirrors Build-Installer.ps1's own staging exclusions exactly: aurora-main/extern/CMakeLists.txt
|
||||
# is the real FetchContent driver and must ship, but any already-fetched dependency *subdirectory*
|
||||
# a developer's local checkout accumulated under extern/ is stale/large build output, not a
|
||||
# release input - only directories inside extern/ are stripped, never the file itself. runtime/build
|
||||
# is a plain developer build directory.
|
||||
find "$appdir/workspace/aurora-main/extern" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
|
||||
rm -rf "$appdir/workspace/runtime/build"
|
||||
cp "$workspace/Launcher/local-build.sh" "$appdir/workspace/Launcher/local-build.sh"
|
||||
|
||||
# AppRun re-syncs runtime/aurora-main/projects/local-build.sh into the writable cache only when
|
||||
# this changes, so it must change whenever any of those bundled paths actually did - a bare commit
|
||||
# hash gets this wrong for an uncommitted change (verified directly: rebuilding after editing
|
||||
# local-build.sh with no commit produced the same hash as the stale cache, so AppRun kept serving
|
||||
# the old script and failed on a flag that didn't exist yet). `git status --porcelain` catches both
|
||||
# modified tracked files and new untracked ones; appending a fresh timestamp when it's non-empty
|
||||
# guarantees this never matches a previous build's stamp, forcing a resync every time the tree is
|
||||
# dirty. A clean tree (a real tagged release) keeps the stable commit-hash behavior, so identical
|
||||
# reruns of the same release AppImage don't resync needlessly.
|
||||
if git -C "$workspace" rev-parse HEAD >/dev/null 2>&1; then
|
||||
version=$(git -C "$workspace" rev-parse HEAD)
|
||||
if [[ -n "$(git -C "$workspace" status --porcelain 2>/dev/null)" ]]; then
|
||||
version="$version-dirty-$(date -u +%s)"
|
||||
fi
|
||||
echo "$version" > "$appdir/workspace/.bundle-version"
|
||||
else
|
||||
date -u +%s > "$appdir/workspace/.bundle-version"
|
||||
fi
|
||||
|
||||
echo "Writing AppRun..."
|
||||
cat > "$appdir/AppRun" <<'APPRUN'
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
CACHE="${XDG_DATA_HOME:-$HOME/.local/share}/WiiCompiled/workspace"
|
||||
mkdir -p "$CACHE"
|
||||
if [ ! -f "$CACHE/.bundle-version" ] || \
|
||||
[ "$(cat "$HERE/workspace/.bundle-version")" != "$(cat "$CACHE/.bundle-version")" ]; then
|
||||
mkdir -p "$CACHE/Launcher"
|
||||
for dir in runtime aurora-main projects; do
|
||||
rm -rf "$CACHE/$dir"
|
||||
cp -r "$HERE/workspace/$dir" "$CACHE/$dir"
|
||||
done
|
||||
cp "$HERE/workspace/Launcher/local-build.sh" "$CACHE/Launcher/local-build.sh"
|
||||
cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version"
|
||||
fi
|
||||
# toolchain/ and native-prebuilt/ are NOT copied into the cache (they're large - ~500 MiB /
|
||||
# ~90 MiB - and local-build.sh only ever reads from them): $CACHE/toolchain and
|
||||
# $CACHE/native-prebuilt are symlinks re-pointed at the current mount on every single launch
|
||||
# (unconditionally, not gated on .bundle-version above, since the mount path itself - unlike the
|
||||
# bundled content - changes every run regardless). CMake bakes a compiler/tool path directly into
|
||||
# each build.ninja rule's command line and Ninja reruns any rule whose command line changed since
|
||||
# the last build (verified directly) - an AppImage's FUSE mount is at a fresh random
|
||||
# /tmp/.mount_XXXXXX every launch, so referencing $HERE straight would change that command line,
|
||||
# and therefore force a full rebuild, on every single run even though the compiler itself never
|
||||
# actually changed. A symlink keeps the *path string* CMake/Ninja see identical across runs while
|
||||
# what it resolves to tracks the current mount underneath (verified directly: CMake records
|
||||
# whatever path it's given as-is - including a symlink - without resolving it first).
|
||||
[ -L "$CACHE/toolchain" ] || rm -rf "$CACHE/toolchain"
|
||||
[ -L "$CACHE/native-prebuilt" ] || rm -rf "$CACHE/native-prebuilt"
|
||||
ln -sfn "$HERE/usr/toolchain" "$CACHE/toolchain"
|
||||
ln -sfn "$HERE/native-prebuilt" "$CACHE/native-prebuilt"
|
||||
exec "$HERE/usr/bin/wiicompiled-setup" --workspace "$CACHE" \
|
||||
--translator-bin "$HERE/usr/bin/translator-cli" \
|
||||
--disc-tool-bin "$HERE/usr/bin/nodtool" \
|
||||
--cc "$CACHE/toolchain/bin/clang" \
|
||||
--cxx "$CACHE/toolchain/bin/clang++" \
|
||||
--fuse-ld lld \
|
||||
--cmake "$CACHE/toolchain/bin/cmake" \
|
||||
--ninja "$CACHE/toolchain/bin/ninja" \
|
||||
--native-prebuilt-dir "$CACHE/native-prebuilt" "$@"
|
||||
APPRUN
|
||||
chmod +x "$appdir/AppRun"
|
||||
|
||||
echo "Writing desktop entry and icon..."
|
||||
cat > "$appdir/wiicompiled-setup.desktop" <<'DESKTOP'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=WiiCompiled Setup
|
||||
Comment=Translate, compile, and launch Mario Kart Wii natively on Linux
|
||||
Exec=AppRun
|
||||
Icon=wiicompiled-setup
|
||||
Categories=Game;
|
||||
Terminal=true
|
||||
DESKTOP
|
||||
|
||||
# No WiiCompiled logo/icon asset exists anywhere in this repo yet. appimagetool refuses to package
|
||||
# without one, so this is a minimal solid-color placeholder - a one-line swap for real branding
|
||||
# later (just replace this generated file with a real wiicompiled-setup.png before packaging).
|
||||
python3 - "$appdir/wiicompiled-setup.png" <<'PY'
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
path = sys.argv[1]
|
||||
|
||||
|
||||
def chunk(tag: bytes, data: bytes) -> bytes:
|
||||
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data))
|
||||
|
||||
|
||||
width = height = 256
|
||||
row = b"\x00" + bytes([0x3A, 0x5F, 0x8F, 0xFF]) * width # filter byte + opaque blue-grey pixels
|
||||
raw = row * height
|
||||
ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)
|
||||
idat = zlib.compress(raw, 9)
|
||||
|
||||
with open(path, "wb") as handle:
|
||||
handle.write(b"\x89PNG\r\n\x1a\n")
|
||||
handle.write(chunk(b"IHDR", ihdr))
|
||||
handle.write(chunk(b"IDAT", idat))
|
||||
handle.write(chunk(b"IEND", b""))
|
||||
PY
|
||||
|
||||
echo "Resolving appimagetool..."
|
||||
appimagetool="$appimagetool_override"
|
||||
if [[ -z "$appimagetool" ]]; then
|
||||
# Cache path is arch-tagged so a workspace shared or synced across an x86_64 and an aarch64
|
||||
# machine never picks up the wrong architecture's cached binary.
|
||||
appimagetool="$workspace/Launcher/artifacts/appimagetool-$appimagetool_arch"
|
||||
if [[ ! -x "$appimagetool" ]]; then
|
||||
echo "Downloading appimagetool ($appimagetool_arch)..."
|
||||
mkdir -p "$(dirname "$appimagetool")"
|
||||
curl -fsSL "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$appimagetool_arch.AppImage" \
|
||||
-o "$appimagetool"
|
||||
chmod +x "$appimagetool"
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$output_dir"
|
||||
echo "Packaging..."
|
||||
# appimagetool detects the target architecture from the first ELF executable it finds in the
|
||||
# AppDir; AppRun here is a shell script, not ELF, so ARCH must be set explicitly.
|
||||
output_name="WiiCompiled-Setup-$appimagetool_arch.AppImage"
|
||||
ARCH="$appimagetool_arch" "$appimagetool" "$appdir" "$output_dir/$output_name"
|
||||
echo "Built: $output_dir/$output_name"
|
||||
Executable
+139
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env bash
|
||||
# Native macOS build automation: optionally extract -> translate -> compile -> publish .app.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
default_workspace=$(cd "$script_dir/.." && pwd)
|
||||
|
||||
fail() { printf 'local-build-macos.command: error: %s\n' "$*" >&2; exit 1; }
|
||||
step() { printf 'MKWCBUILD:STEP:%s %s\n' "$1" "$2"; }
|
||||
assert_file() { [[ -f "$1" ]] || fail "$2 is missing: $1"; }
|
||||
sha256() { shasum -a 256 "$1" | awk '{ print $1 }'; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: local-build-macos.command --output-dir DIR [options]
|
||||
|
||||
--workspace DIR Repository root (default: this script's parent directory)
|
||||
--profile {base|retro-rewind|both} Product to build (default: base)
|
||||
--output-dir DIR Output .app directory (required; Retro Rewind for both)
|
||||
--base-output-dir DIR Base .app directory (required with --profile both)
|
||||
--game IMAGE --nodtool PATH Extract and verify a clean PAL RMCP01 disc image first
|
||||
--retro-rewind-package-dir DIR RetroRewind6 directory (required for Retro Rewind)
|
||||
--retro-wfc-offline-dir DIR Directory containing binary/payload.RMCPD00.bin
|
||||
--skip-retro-wfc-payload Build Retro Rewind without the shared Retro-WFC payload
|
||||
--force-clean-build Delete local generated and native-build-macos caches
|
||||
--parallel N Pin translation and build parallelism
|
||||
--cmake PATH --ninja PATH Override build tools
|
||||
--dotnet PATH Override dotnet
|
||||
--translator-bin PATH Use a self-contained Translator.Cli executable
|
||||
EOF
|
||||
}
|
||||
|
||||
workspace="$default_workspace"; profile=base; output_dir=""; base_output_dir=""
|
||||
game=""; nodtool=""; retro_root=""; retro_wfc=""; skip_retro_wfc=0; force_clean=0
|
||||
parallel=0; cmake_bin=cmake; ninja_bin=ninja; dotnet_bin=dotnet; translator_bin=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--workspace) workspace=${2:-}; shift 2 ;;
|
||||
--profile) profile=${2:-}; shift 2 ;;
|
||||
--output-dir) output_dir=${2:-}; shift 2 ;;
|
||||
--base-output-dir) base_output_dir=${2:-}; shift 2 ;;
|
||||
--game) game=${2:-}; shift 2 ;;
|
||||
--nodtool) nodtool=${2:-}; shift 2 ;;
|
||||
--retro-rewind-package-dir) retro_root=${2:-}; shift 2 ;;
|
||||
--retro-wfc-offline-dir) retro_wfc=${2:-}; shift 2 ;;
|
||||
--skip-retro-wfc-payload) skip_retro_wfc=1; shift ;;
|
||||
--force-clean-build) force_clean=1; shift ;;
|
||||
--parallel) parallel=${2:-}; shift 2 ;;
|
||||
--cmake) cmake_bin=${2:-}; shift 2 ;;
|
||||
--ninja) ninja_bin=${2:-}; shift 2 ;;
|
||||
--dotnet) dotnet_bin=${2:-}; shift 2 ;;
|
||||
--translator-bin) translator_bin=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ $(uname -s) == Darwin ]] || fail 'this build script is for macOS only'
|
||||
[[ $(uname -m) == arm64 ]] || fail 'the current macOS product target is Apple Silicon only'
|
||||
workspace=$(cd "$workspace" && pwd)
|
||||
[[ -n "$output_dir" ]] || fail '--output-dir is required'
|
||||
case "$profile" in base|retro-rewind|both) ;; *) fail '--profile must be base, retro-rewind, or both' ;; esac
|
||||
builds_retro=0; [[ "$profile" != base ]] && builds_retro=1
|
||||
if [[ "$profile" == both && -z "$base_output_dir" ]]; then fail '--base-output-dir is required with --profile both'; fi
|
||||
if [[ "$profile" != both && -n "$base_output_dir" ]]; then fail '--base-output-dir is valid only with --profile both'; fi
|
||||
if [[ -n "$game" || -n "$nodtool" ]]; then [[ -n "$game" && -n "$nodtool" ]] || fail '--game and --nodtool must be supplied together'; fi
|
||||
if (( builds_retro )); then
|
||||
[[ -n "$retro_root" ]] || fail '--retro-rewind-package-dir is required for Retro Rewind'
|
||||
[[ -n "$retro_wfc" ]] && (( skip_retro_wfc )) && fail 'choose only one Retro-WFC mode'
|
||||
[[ -n "$retro_wfc" || $skip_retro_wfc -eq 1 ]] || fail 'choose a Retro-WFC payload directory or --skip-retro-wfc-payload'
|
||||
fi
|
||||
for tool in "$cmake_bin" "$ninja_bin" clang clang++ shasum; do command -v "$tool" >/dev/null || fail "required tool not found: $tool"; done
|
||||
|
||||
project="$workspace/projects/mkwii/recomp.yml"; assets="$workspace/Assets"; generated="$workspace/generated"
|
||||
functions="$generated/functions"; metadata="$generated/base_translation_output.json"; manifest_dir="$workspace/build/base"
|
||||
manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"; native_build="$workspace/native-build-macos"
|
||||
assert_file "$project" 'translation project'
|
||||
if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --game "$game" --assets-dir "$assets" --nodtool "$nodtool"; fi
|
||||
assert_file "$assets/main.dol" 'extracted main.dol'; assert_file "$assets/StaticR.rel" 'extracted StaticR.rel'
|
||||
|
||||
if (( force_clean )); then
|
||||
step force-clean 'Discarding translation and native build caches'
|
||||
rm -rf "$generated" "$manifest_dir" "$native_build"
|
||||
fi
|
||||
|
||||
if [[ -n "$translator_bin" ]]; then
|
||||
assert_file "$translator_bin" 'Translator.Cli executable'
|
||||
translator() { "$translator_bin" "$@"; }
|
||||
else
|
||||
command -v "$dotnet_bin" >/dev/null || fail "required tool not found: $dotnet_bin"
|
||||
translator_dll="$workspace/translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll"
|
||||
if [[ ! -f "$translator_dll" ]]; then
|
||||
step build-translator 'Building the translator'
|
||||
"$dotnet_bin" build "$workspace/translator/src/Translator.Cli/Translator.Cli.csproj" -c Release
|
||||
fi
|
||||
translator() { "$dotnet_bin" "$translator_dll" "$@"; }
|
||||
fi
|
||||
|
||||
entry_point=$(awk '/^translation:/{inside=1} inside && /^[[:space:]]*-[[:space:]]*0[xX][0-9a-fA-F]+[[:space:]]*$/{gsub(/^[[:space:]]*-[[:space:]]*/, ""); print; exit}' "$project")
|
||||
[[ -n "$entry_point" ]] || fail 'could not find the translation entry point'
|
||||
cpu=$(sysctl -n hw.ncpu); mem_gib=$(( $(sysctl -n hw.memsize) / 1024 / 1024 / 1024 )); (( mem_gib < 1 )) && mem_gib=1
|
||||
if (( parallel > 0 )); then translator_threads=$parallel; translated_jobs=$parallel; global_jobs=$parallel
|
||||
else translator_threads=$(( cpu < 16 ? cpu : 16 )); translated_jobs=$(( cpu < mem_gib / 2 ? cpu : mem_gib / 2 )); (( translated_jobs < 1 )) && translated_jobs=1; global_jobs=$cpu; fi
|
||||
|
||||
if (( builds_retro )); then
|
||||
retro_root=$(cd "$retro_root" && pwd)
|
||||
if [[ ! -f "$retro_root/Binaries/Code.pul" && -f "$retro_root/RetroRewind6/Binaries/Code.pul" ]]; then
|
||||
retro_root="$retro_root/RetroRewind6"
|
||||
fi
|
||||
assert_file "$retro_root/Binaries/Code.pul" 'Retro Rewind Code.pul'
|
||||
stage="$workspace/PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries"
|
||||
mkdir -p "$stage"
|
||||
if [[ ! "$retro_root/Binaries/Code.pul" -ef "$stage/Code.pul" ]]; then
|
||||
cp -f "$retro_root/Binaries/Code.pul" "$stage/Code.pul"
|
||||
fi
|
||||
fi
|
||||
|
||||
step translate-base 'Translating the user-owned base game'
|
||||
rm -rf "$functions" "$metadata" "$manifest_dir"; mkdir -p "$functions" "$manifest_dir"
|
||||
translator translate-recursive "$entry_point" --project "$project" --outdir "$functions" --output-metadata "$metadata" --production-source-bundle "$generated/base_translation_sources.bin" --no-function-files --prune-stale --threads "$translator_threads"
|
||||
step emit-base-manifest 'Creating the local base translation manifest'
|
||||
translator emit-base-manifest --project "$project" --out "$manifest_dir" --functions-dir "$functions" --translation-output-metadata "$metadata" --region P
|
||||
if (( builds_retro )); then
|
||||
mod_out="$workspace/build/mods/retro_rewind_full_cpp"; args=(translate-mod --project "$project" --profile retro-rewind --base-manifest "$manifest" --base-translation-output-metadata "$metadata" --code-pul "$retro_root/Binaries/Code.pul" --mod-root "$retro_root" --mod-name 'Retro Rewind' --region P --out "$mod_out" --prefer-cached-inputs --emit-cpp --threads "$translator_threads")
|
||||
if (( skip_retro_wfc )); then args+=(--skip-retro-wfc); else offline_payload="$retro_wfc/binary/payload.RMCPD00.bin"; assert_file "$offline_payload" 'Offline Retro-WFC payload'; args+=(--retro-wfc-payload "$offline_payload"); fi
|
||||
step translate-mod 'Translating Retro Rewind'; translator "${args[@]}"
|
||||
fi
|
||||
step generate-data-init 'Generating local game data initialization'; translator generate-data-init --project "$project"
|
||||
args=(emit-build-shards --project "$project" --base-metadata "$metadata" --base-functions-dir "$functions" --native-source-dir "$workspace/runtime/src" --out "$shards")
|
||||
if (( builds_retro )); then args+=(--resolved-profile "$mod_out/resolved_dispatch_profile.json" --retro-cpp-dir "$mod_out/cpp"); fi
|
||||
step emit-build-shards 'Preparing native build shards'; translator "${args[@]}"
|
||||
|
||||
step configure-native 'Configuring the native toolchain'
|
||||
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
|
||||
targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind)
|
||||
step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs"
|
||||
if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}"; fi
|
||||
if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir"; fi
|
||||
printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir"
|
||||
Executable
+486
@@ -0,0 +1,486 @@
|
||||
#!/usr/bin/env bash
|
||||
# Linux build automation: translate -> emit build shards -> configure -> compile -> publish.
|
||||
#
|
||||
# This is the native-Linux counterpart to Launcher/LocalBuild.ps1. It is a from-scratch parallel
|
||||
# implementation, not a port of NativeBuildFlags.ps1: that file's canonical flags exist only for
|
||||
# the Windows/mingw toolchain's offline pinned dependencies, which this script does not use.
|
||||
# Without --native-prebuilt-dir, aurora is built from source, letting its own CMake auto-detect
|
||||
# Vulkan + vendor SDL3/Dawn via FetchContent - the same configuration already verified working by
|
||||
# hand. With it, aurora is not compiled at all - see Launcher/Prepare-NativePrebuilt.sh, which
|
||||
# harvests exactly that package (the Linux counterpart to Prepare-NativePrebuilt.ps1).
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
log_step() {
|
||||
# $1 = machine-readable step id, $2 = human sentence. Mirrors LocalBuild.ps1's
|
||||
# Write-MkwBuildStep: the id is a stable marker a future installer could parse from the log,
|
||||
# the sentence is for the human reading the terminal.
|
||||
printf 'MKWCBUILD:STEP:%s %s\n' "$1" "$2"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "local-build.sh: error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_file() {
|
||||
[[ -f "$1" ]] || fail "$2 is missing: $1"
|
||||
}
|
||||
|
||||
assert_dir() {
|
||||
[[ -d "$1" ]] || fail "$2 is missing: $1"
|
||||
}
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "required tool '$1' was not found on PATH (override with --$2)"
|
||||
}
|
||||
|
||||
sha256_of() {
|
||||
sha256sum "$1" | awk '{print $1}'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Argument parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/.." && pwd)
|
||||
profile=base
|
||||
output_dir=""
|
||||
base_output_dir=""
|
||||
retro_rewind_package_dir=""
|
||||
retro_wfc_offline_dir=""
|
||||
skip_retro_wfc_payload=0
|
||||
force_clean_build=0
|
||||
parallel_override=0
|
||||
cc_override=""
|
||||
cxx_override=""
|
||||
cmake_override=""
|
||||
ninja_override=""
|
||||
dotnet_override=""
|
||||
translator_dll_override=""
|
||||
translator_bin_override=""
|
||||
fuse_ld_override=""
|
||||
native_prebuilt_dir=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: local-build.sh --output-dir DIR [options]
|
||||
|
||||
--workspace DIR Repository root (default: this script's parent directory)
|
||||
--profile {base|retro-rewind|both} Build profile (default: base)
|
||||
--output-dir DIR Where the built product is published (required)
|
||||
--base-output-dir DIR Second output directory; required with --profile both
|
||||
--retro-rewind-package-dir DIR Retro Rewind source tree (default: PulsarPacks/completed/RetroRewind/RetroRewind6)
|
||||
--retro-wfc-offline-dir DIR Offline Retro-WFC payload directory
|
||||
--skip-retro-wfc-payload Build Retro Rewind without a Retro-WFC payload
|
||||
--force-clean-build Discard every translation/build cache first
|
||||
--parallel N Pin translator threads, translated-shard job pool, and Ninja parallelism to N
|
||||
--cc PATH / --cxx PATH C/C++ compiler (default: cc/c++ on PATH)
|
||||
--fuse-ld NAME_OR_PATH Linker passed to clang as -fuse-ld=NAME_OR_PATH (default: clang's own default linker)
|
||||
--cmake PATH / --ninja PATH Build tools (default: on PATH)
|
||||
--dotnet PATH dotnet executable (default: on PATH)
|
||||
--translator-dll PATH Pre-built Translator.Cli.dll (skips building the translator; still needs --dotnet to run it)
|
||||
--translator-bin PATH Self-contained Translator.Cli executable (skips building AND needs no dotnet at all)
|
||||
--native-prebuilt-dir DIR Precompiled aurora/third-party package (see Prepare-NativePrebuilt.sh);
|
||||
skips compiling aurora-main from source entirely
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--workspace) workspace=$(cd "$2" && pwd); shift 2 ;;
|
||||
--profile) profile=$2; shift 2 ;;
|
||||
--output-dir) output_dir=$2; shift 2 ;;
|
||||
--base-output-dir) base_output_dir=$2; shift 2 ;;
|
||||
--retro-rewind-package-dir) retro_rewind_package_dir=$2; shift 2 ;;
|
||||
--retro-wfc-offline-dir) retro_wfc_offline_dir=$2; shift 2 ;;
|
||||
--skip-retro-wfc-payload) skip_retro_wfc_payload=1; shift ;;
|
||||
--force-clean-build) force_clean_build=1; shift ;;
|
||||
--parallel) parallel_override=$2; shift 2 ;;
|
||||
--cc) cc_override=$2; shift 2 ;;
|
||||
--cxx) cxx_override=$2; shift 2 ;;
|
||||
--fuse-ld) fuse_ld_override=$2; shift 2 ;;
|
||||
--cmake) cmake_override=$2; shift 2 ;;
|
||||
--ninja) ninja_override=$2; shift 2 ;;
|
||||
--dotnet) dotnet_override=$2; shift 2 ;;
|
||||
--translator-dll) translator_dll_override=$2; shift 2 ;;
|
||||
--translator-bin) translator_bin_override=$2; shift 2 ;;
|
||||
--native-prebuilt-dir) native_prebuilt_dir=$2; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$output_dir" ]] || { usage; fail "--output-dir is required"; }
|
||||
case "$profile" in
|
||||
base|retro-rewind|both) ;;
|
||||
*) fail "--profile must be base, retro-rewind, or both" ;;
|
||||
esac
|
||||
|
||||
builds_retro=0
|
||||
[[ "$profile" == "retro-rewind" || "$profile" == "both" ]] && builds_retro=1
|
||||
has_offline_retro_wfc=0
|
||||
[[ -n "$retro_wfc_offline_dir" ]] && has_offline_retro_wfc=1
|
||||
|
||||
if [[ "$builds_retro" -eq 0 ]]; then
|
||||
if [[ "$has_offline_retro_wfc" -eq 1 || "$skip_retro_wfc_payload" -eq 1 ]]; then
|
||||
fail "Retro-WFC payload options are valid only for a Retro Rewind build."
|
||||
fi
|
||||
if [[ -n "$retro_rewind_package_dir" ]]; then
|
||||
fail "--retro-rewind-package-dir is valid only for a Retro Rewind build."
|
||||
fi
|
||||
else
|
||||
if [[ "$has_offline_retro_wfc" -eq "$skip_retro_wfc_payload" ]]; then
|
||||
fail "Choose exactly one Retro-WFC mode: --retro-wfc-offline-dir or --skip-retro-wfc-payload."
|
||||
fi
|
||||
fi
|
||||
if [[ "$profile" == "both" && -z "$base_output_dir" ]]; then
|
||||
fail "--base-output-dir is required with --profile both; --output-dir receives the Retro Rewind product."
|
||||
fi
|
||||
if [[ "$profile" != "both" && -n "$base_output_dir" ]]; then
|
||||
fail "--base-output-dir is valid only with --profile both."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool resolution and prerequisite checks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
dotnet_bin=${dotnet_override:-dotnet}
|
||||
cmake_bin=${cmake_override:-cmake}
|
||||
ninja_bin=${ninja_override:-ninja}
|
||||
cc_bin=${cc_override:-clang}
|
||||
cxx_bin=${cxx_override:-clang++}
|
||||
|
||||
# A self-contained --translator-bin needs no dotnet at all (it bundles its own runtime); dotnet is
|
||||
# only required when the translator has to be built from source or run as a plain .dll.
|
||||
if [[ -z "$translator_bin_override" ]]; then
|
||||
require_command "$dotnet_bin" dotnet
|
||||
fi
|
||||
require_command "$cmake_bin" cmake
|
||||
require_command "$ninja_bin" ninja
|
||||
require_command "$cc_bin" cc
|
||||
require_command "$cxx_bin" cxx
|
||||
|
||||
if [[ -n "$native_prebuilt_dir" ]]; then
|
||||
assert_file "$native_prebuilt_dir/native_prebuilt.cmake" "Native prebuilt package"
|
||||
fi
|
||||
|
||||
project=$workspace/projects/mkwii/recomp.yml
|
||||
assets=$workspace/Assets
|
||||
generated=$workspace/generated
|
||||
functions=$generated/functions
|
||||
base_metadata=$generated/base_translation_output.json
|
||||
base_manifest_dir=$workspace/build/base
|
||||
base_manifest=$base_manifest_dir/mkwii_base_manifest.json
|
||||
shards=$generated/build_shards
|
||||
build=$workspace/native-build
|
||||
translation_provenance=$generated/translation-provenance.json
|
||||
toolchain_provenance=$build/toolchain-provenance.json
|
||||
retro_root=${retro_rewind_package_dir:-$workspace/PulsarPacks/completed/RetroRewind/RetroRewind6}
|
||||
|
||||
assert_file "$project" "Translation project"
|
||||
assert_file "$assets/main.dol" "Extracted main.dol (see translator/README.md - owning the game is required)"
|
||||
assert_file "$assets/StaticR.rel" "Extracted StaticR.rel (see translator/README.md - owning the game is required)"
|
||||
|
||||
# Literal line matching against the manifest's fixed shape, not a YAML dependency - the same
|
||||
# approach NativeBuildFlags.ps1's Get-MkwProjectPins uses on Windows, kept here only for the one
|
||||
# field this script actually needs from the manifest.
|
||||
entry_point=$(awk '
|
||||
/^translation:/ { in_translation = 1 }
|
||||
in_translation && /^[[:space:]]*-[[:space:]]*0[xX][0-9a-fA-F]+[[:space:]]*$/ {
|
||||
gsub(/^[[:space:]]*-[[:space:]]*/, ""); gsub(/[[:space:]]*$/, ""); print; exit
|
||||
}
|
||||
' "$project")
|
||||
[[ -n "$entry_point" ]] || fail "Could not find a translation entry point in $project"
|
||||
|
||||
translator_bin=$translator_bin_override
|
||||
translator_dll=$translator_dll_override
|
||||
if [[ -n "$translator_bin" ]]; then
|
||||
assert_file "$translator_bin" "Translator.Cli executable"
|
||||
translator() { "$translator_bin" "$@"; }
|
||||
else
|
||||
if [[ -z "$translator_dll" ]]; then
|
||||
translator_dll=$workspace/translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll
|
||||
log_step build-translator "Building the translator"
|
||||
"$dotnet_bin" build "$workspace/translator/src/Translator.Cli/Translator.Cli.csproj" -c Release
|
||||
fi
|
||||
assert_file "$translator_dll" "Translator.Cli.dll"
|
||||
translator() { "$dotnet_bin" "$translator_dll" "$@"; }
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parallelism: three independent knobs, same reasoning as LocalBuild.ps1 -
|
||||
# translator_threads (translation's own worker threads), translated_jobs (the real RAM guard,
|
||||
# capping concurrent compiles of memory-hungry translated TUs via Ninja's MKW_TRANSLATED_COMPILE_JOBS
|
||||
# pool), global_jobs (Ninja's overall parallelism). --parallel pins all three.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
cpu_count=$(nproc)
|
||||
mem_gib=$(( $(awk '/^MemTotal:/{print $2}' /proc/meminfo) / 1024 / 1024 ))
|
||||
(( mem_gib < 1 )) && mem_gib=1
|
||||
|
||||
if (( parallel_override > 0 )); then
|
||||
translator_threads=$parallel_override
|
||||
translated_jobs=$parallel_override
|
||||
global_jobs=$parallel_override
|
||||
else
|
||||
translator_threads=$(( cpu_count < 16 ? cpu_count : 16 ))
|
||||
(( translator_threads < 1 )) && translator_threads=1
|
||||
mem_based_cap=$(( mem_gib / 2 ))
|
||||
(( mem_based_cap < 1 )) && mem_based_cap=1
|
||||
translated_jobs=$(( cpu_count < mem_based_cap ? cpu_count : mem_based_cap ))
|
||||
(( translated_jobs < 1 )) && translated_jobs=1
|
||||
global_jobs=$(( translated_jobs > cpu_count ? translated_jobs : cpu_count ))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Translation cache: this script is the only owner of the reuse decision (unlike LocalBuild.ps1,
|
||||
# which is handed caller-computed fingerprints by the Windows installer - there is no Linux
|
||||
# installer yet to supply anything). Hash the game inputs the translation actually depends on;
|
||||
# a match plus every expected output file present means the previous translation is still good.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if (( force_clean_build )); then
|
||||
log_step force-clean "A clean build was requested; discarding every translation and build cache"
|
||||
rm -rf "$generated" "$base_manifest_dir" "$build"
|
||||
fi
|
||||
|
||||
translation_fingerprint=$(cat "$assets/main.dol" "$assets/StaticR.rel" "$project" | sha256sum | awk '{print $1}')
|
||||
reuse_base=0
|
||||
if [[ -f "$translation_provenance" ]]; then
|
||||
recorded=$(grep -o '"TranslationFingerprint" *: *"[^"]*"' "$translation_provenance" 2>/dev/null | sed 's/.*"\([0-9a-f]*\)"$/\1/' || true)
|
||||
if [[ "$recorded" == "$translation_fingerprint" && -f "$base_metadata" && -f "$base_manifest" ]]; then
|
||||
reuse_base=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( builds_retro )); then
|
||||
# The translator discovers the mod through the project file's workspace-relative profile
|
||||
# paths, and both the base and mod leg block leaf inlining at every address the profile
|
||||
# patches - so the selected Code.pul must sit at the profile's mod_root before either leg runs.
|
||||
source_pul=$retro_root/Binaries/Code.pul
|
||||
assert_file "$source_pul" "Retro Rewind Code.pul"
|
||||
staged_binaries=$workspace/PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries
|
||||
mkdir -p "$staged_binaries"
|
||||
staged_pul=$staged_binaries/Code.pul
|
||||
if [[ "$(cd "$(dirname "$source_pul")" && pwd)/$(basename "$source_pul")" != "$(cd "$(dirname "$staged_pul")" && pwd)/$(basename "$staged_pul")" ]]; then
|
||||
cp -f "$source_pul" "$staged_pul"
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( reuse_base )) && (( builds_retro )); then
|
||||
# A base tree that never saw this Code.pul would silently bake vanilla code into the modded
|
||||
# product - check-base-mod-awareness fails closed (anything but exit 0 forces a retranslation).
|
||||
retro_code_pul=$retro_root/Binaries/Code.pul
|
||||
assert_file "$retro_code_pul" "Retro Rewind Code.pul"
|
||||
pul_sha=$(sha256_of "$retro_code_pul")
|
||||
if ! grep -q "\"codePulSha256\":\"$pul_sha\"" "$base_metadata"; then
|
||||
if ! translator check-base-mod-awareness --project "$project" --profile retro-rewind \
|
||||
--translation-output-metadata "$base_metadata" --code-pul "$retro_code_pul"; then
|
||||
log_step retranslate-base "The base translation is stale; retranslating the base game for the new Code.pul"
|
||||
reuse_base=0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( reuse_base )); then
|
||||
log_step reuse-base-translation "Reusing the completed base translation"
|
||||
else
|
||||
rm -f "$translation_provenance"
|
||||
mkdir -p "$generated" "$base_manifest_dir"
|
||||
|
||||
log_step translate-base "Translating the user-owned base game"
|
||||
translator translate-recursive "$entry_point" --project "$project" \
|
||||
--outdir "$functions" --output-metadata "$base_metadata" \
|
||||
--production-source-bundle "$generated/base_translation_sources.bin" \
|
||||
--no-function-files --prune-stale --threads "$translator_threads"
|
||||
|
||||
log_step emit-base-manifest "Creating the local base translation manifest"
|
||||
translator emit-base-manifest --project "$project" --out "$base_manifest_dir" \
|
||||
--functions-dir "$functions" --translation-output-metadata "$base_metadata" --region P
|
||||
|
||||
printf '{"SchemaVersion":1,"TranslationFingerprint":"%s"}' "$translation_fingerprint" \
|
||||
> "$translation_provenance"
|
||||
fi
|
||||
|
||||
if (( builds_retro )); then
|
||||
code_pul=$retro_root/Binaries/Code.pul
|
||||
assert_file "$code_pul" "Retro Rewind Code.pul"
|
||||
retro_out=$workspace/build/mods/retro_rewind_full_cpp
|
||||
translate_mod_args=(translate-mod --project "$project" --profile retro-rewind
|
||||
--base-manifest "$base_manifest" --base-translation-output-metadata "$base_metadata"
|
||||
--code-pul "$code_pul" --mod-root "$retro_root" --mod-name "Retro Rewind"
|
||||
--region P --out "$retro_out" --prefer-cached-inputs --emit-cpp
|
||||
--threads "$translator_threads")
|
||||
if (( skip_retro_wfc_payload )); then
|
||||
translate_mod_args+=(--skip-retro-wfc)
|
||||
else
|
||||
offline_payload=$retro_wfc_offline_dir/binary/payload.RMCPD00.bin
|
||||
assert_file "$offline_payload" "Offline Retro-WFC shared payload"
|
||||
translate_mod_args+=(--retro-wfc-payload "$offline_payload")
|
||||
fi
|
||||
log_step translate-mod "Translating the selected Retro Rewind Code.pul"
|
||||
translator "${translate_mod_args[@]}"
|
||||
fi
|
||||
|
||||
log_step generate-data-init "Generating local game data initialization"
|
||||
translator generate-data-init --project "$project"
|
||||
|
||||
shard_args=(emit-build-shards --project "$project" --base-metadata "$base_metadata"
|
||||
--base-functions-dir "$functions" --native-source-dir "$workspace/runtime/src" --out "$shards")
|
||||
if (( builds_retro )); then
|
||||
retro_out=$workspace/build/mods/retro_rewind_full_cpp
|
||||
shard_args+=(--resolved-profile "$retro_out/resolved_dispatch_profile.json"
|
||||
--retro-cpp-dir "$retro_out/cpp")
|
||||
fi
|
||||
log_step emit-build-shards "Preparing local native build shards"
|
||||
translator "${shard_args[@]}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Native configure + build. Deliberately not passing -DAURORA_DAWN_PROVIDER=package or
|
||||
# -DFETCHCONTENT_FULLY_DISCONNECTED=ON: those exist for the Windows prebuilt-package/offline-
|
||||
# dependencies workflow this script does not build. aurora's own CMake auto-detects Linux and
|
||||
# picks Vulkan + vendors SDL3/Dawn via FetchContent, exactly as already verified working by hand.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
keep_native_build=0
|
||||
stale_reason=""
|
||||
if [[ -f "$build/CMakeCache.txt" ]]; then
|
||||
expected_home=$workspace/runtime
|
||||
cache_home=$(grep '^CMAKE_HOME_DIRECTORY:INTERNAL=' "$build/CMakeCache.txt" | cut -d= -f2- || true)
|
||||
if [[ -n "$cache_home" && "$(cd "$cache_home" 2>/dev/null && pwd)" == "$expected_home" ]]; then
|
||||
keep_native_build=1
|
||||
# CMake auto-detects and caches auxiliary tool paths (CMAKE_AR/RANLIB/LINKER/ASM_COMPILER
|
||||
# and their per-language *_AR/*_RANLIB variants) only once, the first time a language's
|
||||
# compiler is checked - unlike CMAKE_C_COMPILER/CMAKE_CXX_COMPILER, which get overwritten by
|
||||
# the -D flags below on every configure, these are never refreshed on a plain reconfigure.
|
||||
# An AppImage's own AppRun works around this at the source by keeping --cc/--cxx/--cmake/
|
||||
# --ninja pointed at a stable symlink it re-targets at the current mount every launch
|
||||
# (see build-appimage.sh), so the *path string* CMake caches never actually changes run to
|
||||
# run - but this check stays as a general fallback for any tool path that goes stale some
|
||||
# other way (a moved/removed system toolchain, a relocated portable-tools directory, etc):
|
||||
# any :FILEPATH= cache entry whose recorded path no longer exists means this cache belongs
|
||||
# to a toolchain location that's gone - not just a workspace/path mismatch.
|
||||
while IFS= read -r tool_path; do
|
||||
[[ -n "$tool_path" ]] || continue
|
||||
# CMake's own sentinel for "this optional tool was legitimately never found" (e.g.
|
||||
# clang-scan-deps, not required here) - not a path at all, and not a sign of anything
|
||||
# stale. Without this exclusion, every configure wiped the cache unconditionally.
|
||||
[[ "$tool_path" != *-NOTFOUND ]] || continue
|
||||
if [[ ! -e "$tool_path" ]]; then
|
||||
keep_native_build=0
|
||||
stale_reason=" (cached tool path no longer exists: $tool_path)"
|
||||
break
|
||||
fi
|
||||
done < <(grep -o ':FILEPATH=.*' "$build/CMakeCache.txt" | sed 's/^:FILEPATH=//')
|
||||
fi
|
||||
fi
|
||||
if [[ -d "$build" && "$keep_native_build" -eq 0 ]]; then
|
||||
echo "MKWCBUILD: The native build cache does not belong to this workspace path or toolchain; rebuilding from scratch$stale_reason"
|
||||
rm -rf "$build"
|
||||
elif [[ "$keep_native_build" -eq 1 ]]; then
|
||||
echo "MKWCBUILD: Reusing the incremental native build directory"
|
||||
fi
|
||||
|
||||
configure_args=(-S "$workspace/runtime" -B "$build" -G Ninja
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DCMAKE_C_COMPILER="$cc_bin" -DCMAKE_CXX_COMPILER="$cxx_bin"
|
||||
-DCMAKE_MAKE_PROGRAM="$ninja_bin"
|
||||
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs")
|
||||
# CMAKE_C_COMPILER/CXX_COMPILER stay stable across AppImage runs on their own (they're exactly
|
||||
# what's passed via -D above, and AppRun points --cc/--cxx at a symlink it re-targets at the
|
||||
# current mount every launch - see build-appimage.sh). CMAKE_AR/RANLIB/LINKER/ASM_COMPILER do NOT
|
||||
# inherit that stability just because $cc_bin does: verified directly that even with a stable
|
||||
# --cc symlink, CMake's own auto-detection of these still resolved to the *real*, ephemeral mount
|
||||
# path underneath (clang's own driver locates its sibling llvm-ar/ld.lld tools by resolving its own
|
||||
# invoked path, symlinks included, rather than trusting the symlink CMake invoked it through) -
|
||||
# each subsequent run's differing command line then made Ninja rebuild every object from scratch
|
||||
# even though nothing had actually changed. Deriving these from $cc_bin (itself already stable)
|
||||
# and re-passing them explicitly every configure keeps them pinned to the same stable value too.
|
||||
if [[ "$cc_bin" == */* ]]; then
|
||||
toolchain_bin=$(dirname "$cc_bin")
|
||||
[[ -x "$toolchain_bin/llvm-ar" ]] && configure_args+=(-DCMAKE_AR="$toolchain_bin/llvm-ar" -DCMAKE_ASM_COMPILER_AR="$toolchain_bin/llvm-ar" -DCMAKE_C_COMPILER_AR="$toolchain_bin/llvm-ar" -DCMAKE_CXX_COMPILER_AR="$toolchain_bin/llvm-ar")
|
||||
[[ -x "$toolchain_bin/llvm-ranlib" ]] && configure_args+=(-DCMAKE_RANLIB="$toolchain_bin/llvm-ranlib" -DCMAKE_ASM_COMPILER_RANLIB="$toolchain_bin/llvm-ranlib" -DCMAKE_C_COMPILER_RANLIB="$toolchain_bin/llvm-ranlib" -DCMAKE_CXX_COMPILER_RANLIB="$toolchain_bin/llvm-ranlib")
|
||||
[[ -x "$toolchain_bin/ld.lld" ]] && configure_args+=(-DCMAKE_LINKER="$toolchain_bin/ld.lld")
|
||||
configure_args+=(-DCMAKE_ASM_COMPILER="$cc_bin")
|
||||
fi
|
||||
if [[ -n "$fuse_ld_override" ]]; then
|
||||
configure_args+=(-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=$fuse_ld_override")
|
||||
fi
|
||||
if [[ -n "$native_prebuilt_dir" ]]; then
|
||||
configure_args+=(-DMKW_NATIVE_PREBUILT_DIR="$native_prebuilt_dir")
|
||||
fi
|
||||
|
||||
log_step configure-native "Configuring the native toolchain"
|
||||
"$cmake_bin" "${configure_args[@]}"
|
||||
|
||||
case "$profile" in
|
||||
base) targets=(WiiCompiled) ;;
|
||||
retro-rewind) targets=(RetroRewind) ;;
|
||||
both) targets=(WiiCompiled RetroRewind) ;;
|
||||
esac
|
||||
build_args=(--build "$build")
|
||||
for target in "${targets[@]}"; do build_args+=(--target "$target"); done
|
||||
build_args+=(--parallel "$global_jobs")
|
||||
log_step compile "Compiling ${targets[*]} locally"
|
||||
"$cmake_bin" "${build_args[@]}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Publish: the Linux build statically links SDL3/Dawn/etc (verified this session), so unlike
|
||||
# LocalBuild.ps1's DLL-copying dance there is nothing to copy beside the binary except the
|
||||
# runtime's own first-run assets.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
dol_sha=$(sha256_of "$assets/main.dol")
|
||||
rel_sha=$(sha256_of "$assets/StaticR.rel")
|
||||
compiler_version=$("$cxx_bin" --version | head -1)
|
||||
|
||||
publish_built_product() {
|
||||
local target=$1 destination=$2 provenance_profile=$3
|
||||
mkdir -p "$destination"
|
||||
local exe=$build/$target
|
||||
assert_file "$exe" "Locally compiled game executable"
|
||||
cp -f "$exe" "$destination/$target"
|
||||
for name in dsp_coef.bin initial_pipeline_cache.db; do
|
||||
[[ -f "$build/$name" ]] && cp -f "$build/$name" "$destination/"
|
||||
done
|
||||
[[ -d "$build/wii_bootstrap" ]] && cp -rf "$build/wii_bootstrap" "$destination/"
|
||||
|
||||
local is_retro=0 code_pul_sha=null
|
||||
if [[ "$provenance_profile" == "retro-rewind" ]]; then
|
||||
is_retro=1
|
||||
code_pul_sha=\"$(sha256_of "$retro_root/Binaries/Code.pul")\"
|
||||
fi
|
||||
local built_utc
|
||||
built_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
cat > "$destination/local-build.json" <<JSON
|
||||
{
|
||||
"SchemaVersion": 1,
|
||||
"Profile": "$provenance_profile",
|
||||
"BuiltUtc": "$built_utc",
|
||||
"DolSha256": "$dol_sha",
|
||||
"RelSha256": "$rel_sha",
|
||||
"CodePulSha256": $code_pul_sha,
|
||||
"Compiler": "$compiler_version"
|
||||
}
|
||||
JSON
|
||||
}
|
||||
|
||||
case "$profile" in
|
||||
both)
|
||||
publish_built_product WiiCompiled "$base_output_dir" base
|
||||
publish_built_product RetroRewind "$output_dir" retro-rewind
|
||||
;;
|
||||
retro-rewind)
|
||||
publish_built_product RetroRewind "$output_dir" retro-rewind
|
||||
;;
|
||||
base)
|
||||
publish_built_product WiiCompiled "$output_dir" base
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "MKWCBUILD:OUTPUT=$output_dir"
|
||||
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
# Maintainer release builder. It packages setup/source/tooling only -- never a
|
||||
# translated executable, extracted DATA tree, disc image, or Retro Rewind data.
|
||||
set -euo pipefail
|
||||
|
||||
fail() { printf 'build-setup-pkg.command: error: %s\n' "$*" >&2; exit 1; }
|
||||
# Release payloads must not inherit Finder metadata, resource forks, or a
|
||||
# downloaded-file quarantine bit from a maintainer's working volume.
|
||||
copy_clean() { DITTONORSRC=1 ditto --norsrc --noqtn "$@"; }
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: build-setup-pkg.command --nodtool PATH --translator PATH --cmake-root DIR --ninja PATH --output PKG [options]
|
||||
|
||||
Creates a game-code-free WiiCompiled Setup.pkg. The supplied tools must be
|
||||
maintainer-verified, redistributable macOS arm64 artifacts. The resulting pkg
|
||||
is unsigned unless --installer-identity is supplied; releases should sign and
|
||||
notarize it with a Developer ID Installer certificate.
|
||||
|
||||
--workspace DIR Repository root (default: script's grandparent)
|
||||
--version VERSION Bundle/package version (default: 0.1.0)
|
||||
--installer-identity NAME Developer ID Installer identity for productbuild
|
||||
EOF
|
||||
}
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/../.." && pwd); nodtool=""; translator=""; cmake_root=""; ninja=""; output=""; version=0.1.0; identity=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--workspace) workspace=${2:-}; shift 2 ;;
|
||||
--nodtool) nodtool=${2:-}; shift 2 ;;
|
||||
--translator) translator=${2:-}; shift 2 ;;
|
||||
--cmake-root) cmake_root=${2:-}; shift 2 ;;
|
||||
--ninja) ninja=${2:-}; shift 2 ;;
|
||||
--output) output=${2:-}; shift 2 ;;
|
||||
--version) version=${2:-}; shift 2 ;;
|
||||
--installer-identity) identity=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
version=${version#v}
|
||||
[[ "$version" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || fail '--version must contain one to three period-separated integers'
|
||||
IFS=. read -r version_major version_minor version_patch <<< "$version"
|
||||
short_version="$version_major.${version_minor:-0}.${version_patch:-0}"
|
||||
for tool in pkgbuild productbuild ditto codesign; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
|
||||
[[ -x "$nodtool" ]] || fail '--nodtool must name an executable'
|
||||
[[ -x "$translator" ]] || fail '--translator must name an executable'
|
||||
[[ -x "$cmake_root/bin/cmake" ]] || fail '--cmake-root must contain bin/cmake'
|
||||
[[ -x "$ninja" ]] || fail '--ninja must name an executable'
|
||||
"$nodtool" --version >/dev/null || fail '--nodtool did not run successfully'
|
||||
workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output")
|
||||
stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX")
|
||||
trap 'rm -rf "$stage"' EXIT
|
||||
app="$stage/root/Applications/WiiCompiled Setup.app"
|
||||
resources="$app/Contents/Resources"
|
||||
mkdir -p "$app/Contents/MacOS" "$resources/tools"
|
||||
cat > "$app/Contents/Info.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>CFBundleExecutable</key><string>WiiCompiledSetup</string>
|
||||
<key>CFBundleIdentifier</key><string>org.wiicompiled.setup</string>
|
||||
<key>CFBundleName</key><string>WiiCompiled Setup</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key><string>$short_version</string>
|
||||
<key>CFBundleVersion</key><string>$version</string>
|
||||
<key>LSMinimumSystemVersion</key><string>14.0</string>
|
||||
</dict></plist>
|
||||
EOF
|
||||
cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
resources="$(cd "$(dirname "$0")/../Resources" && pwd)"
|
||||
|
||||
# Finder launches an app with no terminal attached. The setup work deliberately
|
||||
# writes human-readable build progress to stdout, so run its .command entry
|
||||
# point in Terminal instead of discarding that output behind an inert app icon.
|
||||
exec /usr/bin/osascript - "$resources/setup.command" "$@" <<'APPLESCRIPT'
|
||||
on run argv
|
||||
set commandLine to quoted form of (item 1 of argv)
|
||||
if (count of argv) > 1 then
|
||||
repeat with argumentIndex from 2 to (count of argv)
|
||||
set commandLine to commandLine & " " & quoted form of (item argumentIndex of argv)
|
||||
end repeat
|
||||
end if
|
||||
tell application "Terminal"
|
||||
activate
|
||||
do script commandLine
|
||||
end tell
|
||||
end run
|
||||
APPLESCRIPT
|
||||
EOF
|
||||
chmod +x "$app/Contents/MacOS/WiiCompiledSetup"
|
||||
copy_clean "$script_dir/setup.command" "$resources/setup.command"; chmod +x "$resources/setup.command"
|
||||
# Copy only the build inputs. This deliberately avoids a maintainer's ignored
|
||||
# output directories, local disc extraction, and developer-only packaging.
|
||||
mkdir -p "$resources/workspace"
|
||||
for source in aurora-main projects runtime translator; do
|
||||
[[ -d "$workspace/$source" ]] || fail "required workspace directory is missing: $source"
|
||||
copy_clean "$workspace/$source" "$resources/workspace/$source"
|
||||
done
|
||||
mkdir -p "$resources/workspace/Launcher/macos"
|
||||
copy_clean "$workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/local-build-macos.command"
|
||||
copy_clean "$workspace/Launcher/macos/extract-disc.command" "$resources/workspace/Launcher/macos/extract-disc.command"
|
||||
copy_clean "$workspace/Launcher/macos/publish-app.command" "$resources/workspace/Launcher/macos/publish-app.command"
|
||||
chmod +x "$resources/workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/macos/"*.command
|
||||
mkdir -p "$resources/tools/cmake"
|
||||
copy_clean "$nodtool" "$resources/tools/nodtool"; chmod +x "$resources/tools/nodtool"
|
||||
copy_clean "$translator" "$resources/tools/Translator.Cli"; chmod +x "$resources/tools/Translator.Cli"
|
||||
copy_clean "$cmake_root" "$resources/tools/cmake"
|
||||
copy_clean "$ninja" "$resources/tools/ninja"; chmod +x "$resources/tools/ninja"
|
||||
copy_clean "$workspace/LICENSE" "$resources/LICENSE"
|
||||
copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md"
|
||||
codesign --force --deep --sign - "$app"
|
||||
pkg="$stage/WiiCompiled-Setup-unsigned.pkg"
|
||||
DITTONORSRC=1 COPYFILE_DISABLE=1 pkgbuild --root "$stage/root" --identifier org.wiicompiled.setup --version "$version" --install-location / "$pkg"
|
||||
if [[ -n "$identity" ]]; then productbuild --sign "$identity" --package "$pkg" "$output"; else ditto "$pkg" "$output"; fi
|
||||
printf 'Created game-code-free package: %s\n' "$output"
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# Extract a user-owned Mario Kart Wii PAL (RMCP01) disc image for a local build.
|
||||
# This script deliberately contains no game data and is intended for the macOS
|
||||
# setup application and for maintainers testing that setup path.
|
||||
set -euo pipefail
|
||||
|
||||
readonly EXPECTED_DOL_SHA256=80d18895b39c63bd80f457398bfcbb91b7d16ac116a41a88967e954080155b05
|
||||
readonly EXPECTED_REL_SHA256=16d9d146112541fefea701ecb5bc1a496f9d50e4a752fbb5b6778e7c6399f67d
|
||||
|
||||
fail() { printf 'extract-disc.command: error: %s\n' "$*" >&2; exit 1; }
|
||||
sha256() { shasum -a 256 "$1" | awk '{ print $1 }'; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: extract-disc.command --game IMAGE --assets-dir DIR --nodtool PATH
|
||||
|
||||
Extracts a user-owned Mario Kart Wii PAL RMCP01 image into DIR. The final
|
||||
layout is DIR/main.dol, DIR/StaticR.rel, and DIR/DATA. Existing data is left
|
||||
untouched unless the complete new extraction passes both content hash checks.
|
||||
EOF
|
||||
}
|
||||
|
||||
game=""
|
||||
assets_dir=""
|
||||
nodtool=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--game) game=${2:-}; shift 2 ;;
|
||||
--assets-dir) assets_dir=${2:-}; shift 2 ;;
|
||||
--nodtool) nodtool=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -f "$game" ]] || fail "disc image does not exist: $game"
|
||||
[[ -n "$assets_dir" ]] || fail "--assets-dir is required"
|
||||
[[ -x "$nodtool" ]] || fail "nodtool is not executable: $nodtool"
|
||||
|
||||
assets_dir=$(mkdir -p "$assets_dir" && cd "$assets_dir" && pwd)
|
||||
scratch=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-disc.XXXXXX")
|
||||
cleanup() { rm -rf "$scratch"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
printf 'MKWCBUILD:STEP:validate-disc Checking the selected disc image\n'
|
||||
"$nodtool" info "$game" >/dev/null
|
||||
printf 'MKWCBUILD:STEP:extract-disc Extracting the user-owned disc image\n'
|
||||
"$nodtool" extract "$game" "$scratch/extracted"
|
||||
|
||||
dol=$(find "$scratch/extracted" -type f -path '*/sys/main.dol' -print -quit)
|
||||
rel=$(find "$scratch/extracted" -type f -path '*/files/rel/StaticR.rel' -print -quit)
|
||||
[[ -n "$dol" ]] || fail 'nodtool extraction did not contain sys/main.dol'
|
||||
[[ -n "$rel" ]] || fail 'nodtool extraction did not contain files/rel/StaticR.rel'
|
||||
[[ $(sha256 "$dol") == "$EXPECTED_DOL_SHA256" ]] || fail 'disc is not the supported clean PAL RMCP01 Mario Kart Wii image'
|
||||
[[ $(sha256 "$rel") == "$EXPECTED_REL_SHA256" ]] || fail 'disc has an unexpected StaticR.rel; use a clean PAL RMCP01 image'
|
||||
|
||||
data_root=$(dirname "$(dirname "$dol")")
|
||||
[[ -d "$data_root/files" ]] || fail 'nodtool extraction did not contain the Wii files directory'
|
||||
|
||||
# Stage beside the destination so the final replacement stays on one volume.
|
||||
stage="$assets_dir/.extract-stage-$$"
|
||||
rm -rf "$stage"
|
||||
mkdir -p "$stage"
|
||||
ditto "$data_root" "$stage/DATA"
|
||||
ditto "$dol" "$stage/main.dol"
|
||||
ditto "$rel" "$stage/StaticR.rel"
|
||||
|
||||
backup="$assets_dir/.previous-extraction-$(date +%Y%m%d-%H%M%S)"
|
||||
if [[ -e "$assets_dir/DATA" || -e "$assets_dir/main.dol" || -e "$assets_dir/StaticR.rel" ]]; then
|
||||
mkdir -p "$backup"
|
||||
for item in DATA main.dol StaticR.rel; do
|
||||
[[ -e "$assets_dir/$item" ]] && mv "$assets_dir/$item" "$backup/$item"
|
||||
done
|
||||
fi
|
||||
mv "$stage/DATA" "$assets_dir/DATA"
|
||||
mv "$stage/main.dol" "$assets_dir/main.dol"
|
||||
mv "$stage/StaticR.rel" "$assets_dir/StaticR.rel"
|
||||
rmdir "$stage"
|
||||
rm -rf "$backup"
|
||||
|
||||
printf 'MKWCBUILD:STEP:disc-ready Verified and extracted clean PAL RMCP01 game assets\n'
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# Turn one locally compiled macOS product into a self-contained .app bundle.
|
||||
set -euo pipefail
|
||||
|
||||
fail() { printf 'publish-app.command: error: %s\n' "$*" >&2; exit 1; }
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR
|
||||
|
||||
Copies a locally built product and its runtime assets into OUTPUT-DIR/<product>.app.
|
||||
It bundles non-system dylibs, rewrites their install names, and ad-hoc signs the
|
||||
result. This is suitable for local use; a release must replace ad-hoc signing
|
||||
with the project's Developer ID signing and notarization process.
|
||||
EOF
|
||||
}
|
||||
|
||||
build_dir=""; product=""; output_dir=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--build-dir) build_dir=${2:-}; shift 2 ;;
|
||||
--product) product=${2:-}; shift 2 ;;
|
||||
--output-dir) output_dir=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind'
|
||||
for tool in codesign ditto install_name_tool otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
|
||||
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
|
||||
|
||||
app="$output_dir/$product.app"
|
||||
macos="$app/Contents/MacOS"
|
||||
frameworks="$app/Contents/Frameworks"
|
||||
resources="$app/Contents/Resources"
|
||||
rm -rf "$app"
|
||||
mkdir -p "$macos" "$frameworks" "$resources"
|
||||
cat > "$app/Contents/Info.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>CFBundleDevelopmentRegion</key><string>en</string>
|
||||
<key>CFBundleExecutable</key><string>$product</string>
|
||||
<key>CFBundleIdentifier</key><string>org.wiicompiled.$product</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
|
||||
<key>CFBundleName</key><string>$product</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key><string>0.1.0</string>
|
||||
<key>CFBundleVersion</key><string>1</string>
|
||||
<key>LSMinimumSystemVersion</key><string>14.0</string>
|
||||
<key>NSHighResolutionCapable</key><true/>
|
||||
</dict></plist>
|
||||
EOF
|
||||
ditto "$build_dir/$product" "$macos/$product"
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do
|
||||
ditto "$build_dir/$asset" "$resources/$asset"
|
||||
ln -s "../Resources/$asset" "$macos/$asset"
|
||||
done
|
||||
|
||||
# Build a closure of Homebrew dylibs. System libraries remain system references.
|
||||
queue=("$macos/$product")
|
||||
while ((${#queue[@]})); do
|
||||
current=${queue[0]}
|
||||
queue=("${queue[@]:1}")
|
||||
while IFS= read -r dependency; do
|
||||
[[ "$dependency" == /opt/homebrew/* || "$dependency" == /usr/local/* ]] || continue
|
||||
[[ -f "$dependency" ]] || continue
|
||||
name=$(basename "$dependency")
|
||||
if [[ ! -f "$frameworks/$name" ]]; then
|
||||
ditto "$dependency" "$frameworks/$name"
|
||||
install_name_tool -id "@rpath/$name" "$frameworks/$name"
|
||||
queue+=("$frameworks/$name")
|
||||
fi
|
||||
done < <(otool -L "$current" | tail -n +2 | awk '{print $1}')
|
||||
done
|
||||
while IFS= read -r binary; do
|
||||
while IFS= read -r old; do
|
||||
[[ "$old" == /opt/homebrew/* || "$old" == /usr/local/* ]] || continue
|
||||
name=$(basename "$old")
|
||||
[[ -f "$frameworks/$name" ]] || continue
|
||||
if [[ "$binary" == "$macos/$product" ]]; then
|
||||
install_name_tool -change "$old" "@executable_path/../Frameworks/$name" "$binary"
|
||||
else
|
||||
install_name_tool -change "$old" "@loader_path/$name" "$binary"
|
||||
fi
|
||||
done < <(otool -L "$binary" | tail -n +2 | awk '{print $1}')
|
||||
done < <(find "$frameworks" -type f -print; printf '%s\n' "$macos/$product")
|
||||
|
||||
find "$frameworks" -type f -exec codesign --force --sign - {} +
|
||||
codesign --force --deep --sign - "$app"
|
||||
codesign --verify --deep --strict "$app"
|
||||
printf 'MKWCBUILD:APP=%s\n' "$app"
|
||||
Executable
+140
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bash
|
||||
# Entry point bundled in WiiCompiled Setup.app. The package contains source and
|
||||
# tools only; a user's own verified disc is extracted into Application Support.
|
||||
set -euo pipefail
|
||||
|
||||
resources=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace_source="$resources/workspace"
|
||||
nodtool="$resources/tools/nodtool"
|
||||
translator="$resources/tools/Translator.Cli"
|
||||
cmake_bin="$resources/tools/cmake/bin/cmake"
|
||||
ninja_bin="$resources/tools/ninja"
|
||||
support_root="$HOME/Library/Application Support/WiiCompiled"
|
||||
workspace="$support_root/BuildWorkspace"
|
||||
products="$support_root/Products"
|
||||
|
||||
fail() { printf 'WiiCompiled Setup: %s\n' "$*" >&2; exit 1; }
|
||||
notice() { /usr/bin/osascript -e "display dialog \"${1//\"/\\\"}\" buttons {\"OK\"} default button \"OK\" with icon caution" >/dev/null; }
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: setup.command --game IMAGE [--retro-dir DIR] [--install-location {user|applications}]
|
||||
|
||||
Without arguments this script opens file pickers. It is normally launched by
|
||||
WiiCompiled Setup.app, not run directly.
|
||||
EOF
|
||||
}
|
||||
|
||||
game=""; retro_dir=""; install_location=applications
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--game) game=${2:-}; shift 2 ;;
|
||||
--retro-dir) retro_dir=${2:-}; shift 2 ;;
|
||||
--install-location) install_location=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
[[ "$install_location" == user || "$install_location" == applications ]] || fail '--install-location must be user or applications'
|
||||
|
||||
if [[ -z "$game" ]]; then
|
||||
game=$(/usr/bin/osascript <<'APPLESCRIPT'
|
||||
set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii PAL (RMCP01) disc image"
|
||||
POSIX path of selectedFile
|
||||
APPLESCRIPT
|
||||
) || exit 0
|
||||
choice=$(/usr/bin/osascript -e 'button returned of (display dialog "Would you like to build Retro Rewind too?" buttons {"Base game only", "Choose Retro Rewind folder"} default button "Base game only")')
|
||||
if [[ "$choice" == 'Choose Retro Rewind folder' ]]; then
|
||||
retro_dir=$(/usr/bin/osascript <<'APPLESCRIPT'
|
||||
set selectedFolder to choose folder with prompt "Choose the RetroRewind6 folder (or its parent folder)"
|
||||
POSIX path of selectedFolder
|
||||
APPLESCRIPT
|
||||
) || exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ -x "$nodtool" ]] || fail 'the packaged nodtool is missing or not executable'
|
||||
[[ -x "$translator" ]] || fail 'the packaged Translator.Cli is missing or not executable'
|
||||
[[ -x "$cmake_bin" && -x "$ninja_bin" ]] || fail 'the packaged CMake or Ninja tool is missing'
|
||||
if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then
|
||||
notice 'Xcode Command Line Tools are required once to compile WiiCompiled. Click OK, complete the Apple installer, then run WiiCompiled Setup again.'
|
||||
/usr/bin/xcode-select --install || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$support_root" "$products"
|
||||
if [[ ! -d "$workspace/.git" && ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
|
||||
printf 'Preparing the local build workspace...\n'
|
||||
rm -rf "$workspace"
|
||||
/usr/bin/ditto "$workspace_source" "$workspace"
|
||||
fi
|
||||
|
||||
profile=base
|
||||
build_args=(--workspace "$workspace" --game "$game" --nodtool "$nodtool" --output-dir "$products")
|
||||
if [[ -n "$retro_dir" ]]; then
|
||||
profile=both
|
||||
# Online play needs the shared Retro-WFC payload. Keep it in the per-user
|
||||
# support directory rather than the packaged app or build workspace, then
|
||||
# verify its pinned signature before publishing it into the local cache.
|
||||
retro_wfc_dir="$support_root/RetroWfcPayload"
|
||||
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
|
||||
printf 'Discarding an invalid cached Retro-WFC payload...\n' >&2
|
||||
rm -f "$retro_wfc_payload"
|
||||
fi
|
||||
if [[ ! -f "$retro_wfc_payload" ]]; then
|
||||
printf 'Downloading the Retro-WFC payload needed for online play...\n'
|
||||
mkdir -p "$retro_wfc_dir"
|
||||
payload_stage=$(mktemp -d "$retro_wfc_dir/.payload-download.XXXXXX")
|
||||
temporary_payload="$payload_stage/binary/payload.RMCPD00.bin"
|
||||
mkdir -p "$(dirname "$temporary_payload")"
|
||||
trap 'rm -rf "$payload_stage"' EXIT
|
||||
/usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \
|
||||
--retry 1 --output "$temporary_payload" \
|
||||
'http://nas.play.rwfc.net/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play'
|
||||
"$translator" validate-retro-wfc-payload --directory "$payload_stage" || \
|
||||
fail 'downloaded Retro-WFC payload failed signature validation'
|
||||
mkdir -p "$retro_wfc_dir/binary"
|
||||
mv "$temporary_payload" "$retro_wfc_payload"
|
||||
rmdir "$payload_stage/binary" "$payload_stage"
|
||||
trap - EXIT
|
||||
fi
|
||||
build_args+=(--profile both --base-output-dir "$products" --retro-rewind-package-dir "$retro_dir" --retro-wfc-offline-dir "$retro_wfc_dir")
|
||||
fi
|
||||
"$workspace/Launcher/local-build-macos.command" "${build_args[@]}" --profile "$profile" --cmake "$cmake_bin" --ninja "$ninja_bin" --translator-bin "$translator"
|
||||
|
||||
config="$support_root/Config.toml"
|
||||
toml_string() { printf '%s' "$1" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g'; }
|
||||
set_path() {
|
||||
local key=$1 value=$2 encoded line temporary
|
||||
encoded=$(toml_string "$value"); line="$key = \"$encoded\""; temporary="$config.tmp"
|
||||
touch "$config"
|
||||
if grep -q '^[[:space:]]*\[paths\][[:space:]]*$' "$config"; then
|
||||
awk -v key="$key" -v line="$line" '
|
||||
/^[[:space:]]*\[paths\][[:space:]]*$/ { print; print line; inside = 1; next }
|
||||
inside && /^[[:space:]]*\[/ { inside = 0 }
|
||||
inside && $0 ~ "^[[:space:]]*" key "[[:space:]]*=" { next }
|
||||
{ print }
|
||||
' "$config" > "$temporary"
|
||||
else
|
||||
{ cat "$config"; printf '\n[paths]\n%s\n' "$line"; } > "$temporary"
|
||||
fi
|
||||
mv "$temporary" "$config"
|
||||
}
|
||||
set_path dvd_root "$workspace/Assets/DATA"
|
||||
[[ -n "$retro_dir" ]] && set_path retro_rewind_root "$retro_dir"
|
||||
|
||||
destination="$HOME/Applications"
|
||||
if [[ "$install_location" == applications ]]; then destination=/Applications; fi
|
||||
install_app() {
|
||||
local app=$1
|
||||
[[ -d "$products/$app" ]] || return 0
|
||||
if [[ "$destination" == /Applications ]]; then
|
||||
command="mkdir -p /Applications && rm -rf '/Applications/$app' && ditto '$products/$app' '/Applications/$app'"
|
||||
/usr/bin/osascript -e "do shell script \"$command\" with administrator privileges"
|
||||
else
|
||||
mkdir -p "$destination"; rm -rf "$destination/$app"; /usr/bin/ditto "$products/$app" "$destination/$app"
|
||||
fi
|
||||
}
|
||||
install_app WiiCompiled.app
|
||||
[[ "$profile" == both ]] && install_app RetroRewind.app
|
||||
notice "Installation complete. Your apps are in $destination."
|
||||
Executable
+237
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prepares a self-contained native-Linux build toolchain bundled into the AppImage by
|
||||
# build-appimage.sh, so a user needs no `clang`/`cmake`/`ninja` of their own to build the
|
||||
# translated game (mirrors why Windows bundles llvm-mingw + CMake + Ninja via
|
||||
# Prepare-PortableTools.ps1 - this is that script's Linux counterpart, for the same reason).
|
||||
#
|
||||
# clang/lld/llvm-ar: pruned from the official llvm.org GitHub release tarball (NOT llvm-mingw -
|
||||
# that project targets Windows/mingw, never native Linux) down to just what's needed to compile
|
||||
# and link: clang, lld, llvm-ar, the clang resource dir (builtin headers + compiler-rt), and
|
||||
# libc++/libc++abi/libunwind (so the toolchain never has to fall back to the host's system
|
||||
# libstdc++ headers). The raw release is ~1.9 GiB per arch (every LLVM backend, mlir, flang, lldb,
|
||||
# docs, tests); pruned it is ~500 MiB uncompressed / ~100 MiB compressed, verified against a real
|
||||
# build of this project.
|
||||
#
|
||||
# cmake: pruned from the official Kitware GitHub release tarball down to bin/cmake (not
|
||||
# ccmake/cmake-gui/cpack/ctest, which local-build.sh never invokes) plus the Modules/Templates
|
||||
# CMake needs at runtime (found relative to bin/cmake via CMAKE_ROOT auto-detection - Help/doc/man/
|
||||
# the desktop-integration files under share/ are documentation/GUI-only and dropped). Verified with
|
||||
# a real configure+build using the pruned cmake+ninja+clang together.
|
||||
#
|
||||
# ninja: the official ninja-build GitHub release zip, used as-is - it is already a single small
|
||||
# (~130 KiB compressed) static-ish binary with nothing to prune.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/.." && pwd)
|
||||
|
||||
llvm_version=22.1.8
|
||||
cmake_version=4.3.3
|
||||
ninja_version=1.13.2
|
||||
destination="$script_dir/artifacts/portable-tools"
|
||||
arch=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: prepare-portable-tools.sh --arch {x86_64|aarch64} [--destination DIR]
|
||||
|
||||
--arch ARCH Target architecture (required)
|
||||
--destination DIR Where the toolchain is written, as DIR/toolchain-ARCH
|
||||
(default: Launcher/artifacts/portable-tools)
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--arch) arch=$2; shift 2 ;;
|
||||
--destination) destination=$2; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "prepare-portable-tools.sh: unknown argument: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$arch" in
|
||||
x86_64) llvm_release_arch=X64; target_triple=x86_64-unknown-linux-gnu
|
||||
llvm_release_sha256=df0e1ecf16caf3489a272a5eea4eec9b0d82878f6477fa309504f918a0006384
|
||||
cmake_release_arch=x86_64
|
||||
cmake_sha256=927b2368a946c37269c3a66225ab00544e756459cdd0b5d0da438694fb9ff802
|
||||
ninja_asset=ninja-linux.zip
|
||||
ninja_sha256=5749cbc4e668273514150a80e387a957f933c6ed3f5f11e03fb30955e2bbead6 ;;
|
||||
aarch64) llvm_release_arch=ARM64; target_triple=aarch64-unknown-linux-gnu
|
||||
llvm_release_sha256=805efad2bb91cb4967fa569e0881d10c0f69c04461cf671cccbae19f547acc34
|
||||
cmake_release_arch=aarch64
|
||||
cmake_sha256=9ea38356dbd3e32e51029a3e09a0f2f8e117ef4fbcaad7a21ffb36409bbd5cb4
|
||||
ninja_asset=ninja-linux-aarch64.zip
|
||||
ninja_sha256=fd2cacc8050a7f12a16a2e48f9e06fca5c14fc4c2bee2babb67b58be17a607fc ;;
|
||||
*) echo "prepare-portable-tools.sh: --arch must be x86_64 or aarch64" >&2; usage; exit 1 ;;
|
||||
esac
|
||||
|
||||
destination=$(mkdir -p "$destination" && cd "$destination" && pwd)
|
||||
toolchain_dir="$destination/toolchain-$arch"
|
||||
downloads="$script_dir/artifacts/downloads"
|
||||
mkdir -p "$downloads"
|
||||
|
||||
sha256_of() { sha256sum "$1" | awk '{print $1}'; }
|
||||
|
||||
download_verified() {
|
||||
# $1 = destination path, $2 = URL, $3 = expected sha256
|
||||
local dest=$1 url=$2 expected=$3
|
||||
if [[ -f "$dest" ]] && [[ "$(sha256_of "$dest")" == "$expected" ]]; then return; fi
|
||||
echo "prepare-portable-tools.sh: downloading $(basename "$dest")..."
|
||||
local tmp="$dest.partial"
|
||||
rm -f "$tmp"
|
||||
curl -fL --progress-bar -o "$tmp" "$url"
|
||||
local actual
|
||||
actual=$(sha256_of "$tmp")
|
||||
if [[ "$actual" != "$expected" ]]; then
|
||||
echo "prepare-portable-tools.sh: $(basename "$dest") hash mismatch: expected $expected, got $actual" >&2
|
||||
rm -f "$tmp"
|
||||
exit 1
|
||||
fi
|
||||
mv "$tmp" "$dest"
|
||||
}
|
||||
|
||||
if [[ -x "$toolchain_dir/bin/clang" && -x "$toolchain_dir/bin/ninja" && -x "$toolchain_dir/bin/cmake" ]]; then
|
||||
echo "prepare-portable-tools.sh: reusing existing toolchain at $toolchain_dir"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
work="$destination/.building-toolchain-$arch"
|
||||
rm -rf "$work"
|
||||
mkdir -p "$work/bin" "$work/lib/$target_triple" "$work/include/$target_triple/c++/v1"
|
||||
|
||||
# --- clang/lld/llvm-ar, pruned from the official LLVM release ---
|
||||
|
||||
llvm_archive_name="LLVM-$llvm_version-Linux-$llvm_release_arch.tar.xz"
|
||||
llvm_archive="$downloads/$llvm_archive_name"
|
||||
download_verified "$llvm_archive" \
|
||||
"https://github.com/llvm/llvm-project/releases/download/llvmorg-$llvm_version/$llvm_archive_name" \
|
||||
"$llvm_release_sha256"
|
||||
|
||||
extract_root="$script_dir/artifacts/.extract-clang-$arch"
|
||||
rm -rf "$extract_root"
|
||||
mkdir -p "$extract_root"
|
||||
echo "prepare-portable-tools.sh: extracting $llvm_archive_name (this is the full ~1.9 GiB release; only a fraction is kept)..."
|
||||
tar -xf "$llvm_archive" -C "$extract_root"
|
||||
src="$extract_root/LLVM-$llvm_version-Linux-$llvm_release_arch"
|
||||
[[ -d "$src" ]] || { echo "prepare-portable-tools.sh: unexpected archive layout, expected $src" >&2; exit 1; }
|
||||
|
||||
echo "prepare-portable-tools.sh: pruning to the minimal compile+link toolchain..."
|
||||
|
||||
# clang: the real driver executable plus the clang/clang++ symlinks CMake/local-build.sh invoke.
|
||||
# Stripped: debug symbols are dead weight for a bundled compiler nobody will debug.
|
||||
cp -a "$src/bin/clang-22" "$work/bin/"
|
||||
strip "$work/bin/clang-22"
|
||||
ln -s clang-22 "$work/bin/clang"
|
||||
ln -s clang "$work/bin/clang++"
|
||||
|
||||
# lld: linked via -fuse-ld=lld, which clang resolves by looking for ld.lld next to itself first -
|
||||
# see local-build.sh's --fuse-ld option.
|
||||
cp -a "$src/bin/lld" "$work/bin/"
|
||||
strip "$work/bin/lld"
|
||||
ln -s lld "$work/bin/ld.lld"
|
||||
|
||||
# llvm-ar/llvm-ranlib: CMake's archiver for the many static libraries this project builds
|
||||
# (aurora, Crypto++, SDL3, Dawn's dependency closure, the translated game shards).
|
||||
cp -a "$src/bin/llvm-ar" "$work/bin/"
|
||||
strip "$work/bin/llvm-ar"
|
||||
ln -s llvm-ar "$work/bin/llvm-ranlib"
|
||||
|
||||
# Clang's resource directory: builtin headers (stddef.h, immintrin.h, ...) and compiler-rt
|
||||
# (builtins, sanitizer runtimes). `clang -print-resource-dir` must find this at lib/clang/<ver>/.
|
||||
cp -a "$src/lib/clang" "$work/lib/"
|
||||
|
||||
# libc++/libc++abi/libunwind: so this toolchain never has to fall back to whatever libstdc++ the
|
||||
# host distro happens to have installed. Not the default yet (local-build.sh still resolves the
|
||||
# system libstdc++ unless -stdlib=libc++ is passed), but bundled so that option exists.
|
||||
cp -a "$src/include/c++" "$work/include/"
|
||||
cp -a "$src/include/$target_triple/c++/v1/__config_site" "$work/include/$target_triple/c++/v1/"
|
||||
cp -a "$src/lib/$target_triple"/libc++.a "$src/lib/$target_triple"/libc++abi.a "$src/lib/$target_triple"/libunwind.a "$work/lib/$target_triple/"
|
||||
cp -a "$src/lib/$target_triple"/libc++.so* "$src/lib/$target_triple"/libc++abi.so* "$src/lib/$target_triple"/libunwind.so* "$work/lib/$target_triple/"
|
||||
|
||||
rm -rf "$extract_root"
|
||||
|
||||
# --- cmake, pruned from the official Kitware release ---
|
||||
|
||||
cmake_share_version=${cmake_version%.*}
|
||||
cmake_archive_name="cmake-$cmake_version-linux-$cmake_release_arch.tar.gz"
|
||||
cmake_archive="$downloads/$cmake_archive_name"
|
||||
download_verified "$cmake_archive" \
|
||||
"https://github.com/Kitware/CMake/releases/download/v$cmake_version/$cmake_archive_name" \
|
||||
"$cmake_sha256"
|
||||
|
||||
cmake_extract_root="$script_dir/artifacts/.extract-cmake-$arch"
|
||||
rm -rf "$cmake_extract_root"
|
||||
mkdir -p "$cmake_extract_root"
|
||||
echo "prepare-portable-tools.sh: extracting $cmake_archive_name..."
|
||||
tar -xzf "$cmake_archive" -C "$cmake_extract_root"
|
||||
cmake_src="$cmake_extract_root/cmake-$cmake_version-linux-$cmake_release_arch"
|
||||
[[ -d "$cmake_src" ]] || { echo "prepare-portable-tools.sh: unexpected archive layout, expected $cmake_src" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$work/share/cmake-$cmake_share_version"
|
||||
cp -a "$cmake_src/bin/cmake" "$work/bin/"
|
||||
cp -a "$cmake_src/share/cmake-$cmake_share_version/Modules" "$cmake_src/share/cmake-$cmake_share_version/Templates" \
|
||||
"$work/share/cmake-$cmake_share_version/"
|
||||
rm -rf "$cmake_extract_root"
|
||||
|
||||
# --- ninja, used as-is ---
|
||||
|
||||
ninja_archive="$downloads/ninja-$ninja_version-$arch.zip"
|
||||
download_verified "$ninja_archive" \
|
||||
"https://github.com/ninja-build/ninja/releases/download/v$ninja_version/$ninja_asset" \
|
||||
"$ninja_sha256"
|
||||
echo "prepare-portable-tools.sh: staging ninja $ninja_version..."
|
||||
unzip -oq "$ninja_archive" -d "$work/bin"
|
||||
chmod +x "$work/bin/ninja"
|
||||
|
||||
cat > "$work/LICENSE.txt" <<EOF
|
||||
Portable build tools bundled by WiiCompiled
|
||||
|
||||
clang/lld/llvm-ar $llvm_version (pruned from the official LLVM release for Linux/$llvm_release_arch)
|
||||
https://github.com/llvm/llvm-project/releases/tag/llvmorg-$llvm_version
|
||||
Apache License v2.0 with LLVM Exceptions:
|
||||
https://github.com/llvm/llvm-project/blob/llvmorg-$llvm_version/LICENSE.TXT
|
||||
|
||||
CMake $cmake_version
|
||||
https://github.com/Kitware/CMake
|
||||
BSD 3-Clause License
|
||||
|
||||
Ninja $ninja_version
|
||||
https://github.com/ninja-build/ninja
|
||||
Apache License 2.0
|
||||
EOF
|
||||
|
||||
echo "prepare-portable-tools.sh: testing the toolchain..."
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
cat > "$test_dir/t.cpp" <<'EOF'
|
||||
#include <vector>
|
||||
#include <cstdio>
|
||||
int main() {
|
||||
std::vector<int> v{1, 2, 3};
|
||||
int sum = 0;
|
||||
for (int x : v) sum += x;
|
||||
return sum == 6 ? 0 : 1;
|
||||
}
|
||||
EOF
|
||||
"$work/bin/clang++" -std=c++20 -fuse-ld=lld "$test_dir/t.cpp" -o "$test_dir/t"
|
||||
"$test_dir/t"
|
||||
|
||||
# Also exercised together through CMake+Ninja, exactly how local-build.sh drives them - a plain
|
||||
# clang++ invocation above would not catch a broken CMAKE_ROOT (Modules/Templates) or a Ninja that
|
||||
# can't find the compiler.
|
||||
cat > "$test_dir/CMakeLists.txt" <<'EOF'
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(test CXX)
|
||||
add_executable(test t.cpp)
|
||||
EOF
|
||||
"$work/bin/cmake" -S "$test_dir" -B "$test_dir/build" -G Ninja \
|
||||
-DCMAKE_MAKE_PROGRAM="$work/bin/ninja" -DCMAKE_CXX_COMPILER="$work/bin/clang++" >/dev/null
|
||||
"$work/bin/cmake" --build "$test_dir/build" >/dev/null
|
||||
"$test_dir/build/test"
|
||||
|
||||
rm -rf "$test_dir"
|
||||
trap - EXIT
|
||||
|
||||
mv "$work" "$toolchain_dir"
|
||||
echo "prepare-portable-tools.sh: toolchain ready at $toolchain_dir ($(du -sh "$toolchain_dir" | cut -f1))"
|
||||
@@ -46,24 +46,51 @@ Press **F10** while the game window has focus:
|
||||
- Internal resolution
|
||||
- FPS counter
|
||||
- Controller assignment for all four ports
|
||||
- Full per-controller button mapping
|
||||
- Full per-controller button mapping, including the bumpers
|
||||
- Dolphin-syntax input expressions and GCPadNew.ini import
|
||||
- Controller vibration on/off
|
||||
- Volume, instant mute, and the music ducking toggle
|
||||
|
||||
Everything you change is saved to `Config.toml` on the spot and restored next launch.
|
||||
|
||||
**Real controller support.**
|
||||
Controllers are fed to the game as a GameCube controller.
|
||||
The port does NOT pretend to be a Wii Remote or Classic Controller.
|
||||
Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the
|
||||
same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra
|
||||
inputs like paddles, touchpads and share buttons show up when the hardware reports them.
|
||||
|
||||
**Dolphin-compatible input expressions.**
|
||||
Each GameCube control can carry an expression in Dolphin's input syntax, with the same operators
|
||||
and the same functions.
|
||||
A Dolphin `GCPadNew.ini` can be imported directly from the F10 bar.
|
||||
|
||||
**Vibration toggle.**
|
||||
Force feedback can be turned off for every port at once.
|
||||
The official Wii U / Switch GameCube adapter (WUP-028) works too; as with Dolphin, on Windows the
|
||||
adapter must be switched to the WinUSB driver once (Zadig).
|
||||
|
||||
**Real Wii Remotes over Bluetooth.**
|
||||
Pair a Wii Remote with Windows (Settings > Bluetooth > Add device, press 1+2 or SYNC, leave the
|
||||
PIN empty)
|
||||
|
||||
Known limitations of the Wii Remote path:
|
||||
- No IR pointer yet: menus are navigated with the D-pad and A (the game treats the remote as
|
||||
pointing away from the screen).
|
||||
- Battery level is not reported to the game and the remote's speaker is not implemented.
|
||||
- Only the Wii Remote's own accelerometer is calibrated; the Nunchuk's uses SDL's fixed zero point.
|
||||
- The Classic Controller's L/R triggers reach the game as digital (full pull on click): SDL does not
|
||||
expose their analog travel.
|
||||
- Turn the Wii Remote support off in that menu if you use a Mayflash DolphinBar, which already
|
||||
presents the remote as a regular gamepad.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Windows 10 or 11, 64-bit
|
||||
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher
|
||||
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
|
||||
- About 20 GB of free disk space during installation
|
||||
- About 20 GB of free disk space during installation (Final game size ~5 GB)
|
||||
- macOS 14 (Sonoma) or later on Apple Silicon
|
||||
- On macOS, Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
|
||||
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
|
||||
GCZ, CISO, WBFS, WIA and RVZ are accepted.
|
||||
|
||||
@@ -84,6 +111,7 @@ image under Settings, turn on **WiiCompiled (beta)**, and hit install from the H
|
||||
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
|
||||
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
|
||||
|
||||
|
||||
> [!CAUTION]
|
||||
> Only take builds from this repository's
|
||||
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
|
||||
|
||||
+21
-12
@@ -26,10 +26,6 @@ Aurora itself vendors:
|
||||
(shagkur) and Dave Murphy (WinterMute). `aurora-main/lib/card/SRAM.hpp`.
|
||||
Source: <https://github.com/devkitPro/libogc>
|
||||
|
||||
> [!NOTE]
|
||||
> Upstream aurora ships `assets/screenshot.png`, a rendered frame from a different Nintendo
|
||||
> title. It is intentionally omitted from this repository.
|
||||
|
||||
### Dolphin Emulator data files - GPL-2.0-or-later
|
||||
|
||||
Copyright (c) 2003+ Dolphin Emulator Project.
|
||||
@@ -103,14 +99,26 @@ Source: <https://github.com/ToruNiina/toml11/tree/v4.4.0>. Full license text:
|
||||
Copyright (c) Antoine Aubry and contributors.
|
||||
Referenced by `translator/src/Translator.Core`. Source: <https://github.com/aaubry/YamlDotNet>
|
||||
|
||||
### libco - ISC (valgrind.h: BSD-style)
|
||||
|
||||
Copyright byuu and the higan team.
|
||||
Non-Windows builds use libco's symmetric stackful coroutines in place of Win32 Fibers for guest
|
||||
OSThread scheduling (`runtime/src/fiber_manager.cpp`). Vendored in full (all non-Windows
|
||||
CPU-architecture backends - amd64, x86, arm, aarch64, ppc, ppc64v2, plus the portable sjlj
|
||||
fallback - though this project's x86_64-only target only ever compiles amd64.c) in
|
||||
`runtime/third_party/libco` from commit `e18e09d634d612a01781168ad4d76be10a7e3bad`.
|
||||
Source: <https://github.com/higan-emu/libco>. Full license text:
|
||||
`runtime/third_party/libco/LICENSE`.
|
||||
|
||||
---
|
||||
|
||||
## Fetched at build time and redistributed in release builds
|
||||
|
||||
These are pinned in `aurora-main/extern/CMakeLists.txt` and
|
||||
These are pinned in `aurora-main/extern/CMakeLists.txt`, `aurora-main/CMakeLists.txt` and
|
||||
`aurora-main/cmake/AuroraDawnProvider.cmake`. They are not stored in this repository; the build
|
||||
downloads them, and release installers carry the resulting binaries. Their license texts are
|
||||
included in the installer's `licenses/` folder.
|
||||
included in the installer's `licenses/` folder. The Windows installer bundles the pinned source
|
||||
trees themselves (fetched by `Launcher/Prepare-Dependencies.ps1`) so end-user builds run offline.
|
||||
|
||||
| Component | Version | License | Upstream |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -118,6 +126,7 @@ included in the installer's `licenses/` folder.
|
||||
| Tint (part of Dawn) | with Dawn | BSD-3-Clause | <https://dawn.googlesource.com/dawn> |
|
||||
| DirectXShaderCompiler (`dxcompiler.dll`) | with Dawn | NCSA / University of Illinois Open Source | <https://github.com/microsoft/DirectXShaderCompiler> |
|
||||
| SDL | 3.4.4 | zlib | <https://github.com/libsdl-org/SDL> |
|
||||
| libusb (linked into SDL on Windows) | 1.0.30 | LGPL-2.1-or-later | <https://github.com/libusb/libusb> |
|
||||
| Abseil | LTS 20240722.0 | Apache-2.0 | <https://github.com/abseil/abseil-cpp> |
|
||||
| Dear ImGui | 1.91.9b-docking | MIT | <https://github.com/ocornut/imgui> |
|
||||
| {fmt} | 11.1.4 | MIT | <https://github.com/fmtlib/fmt> |
|
||||
@@ -129,6 +138,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
|
||||
|
||||
@@ -153,16 +163,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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ option(AURORA_CACHE_USE_ZSTD "Compress WebGPU cache entries with zstd" ON)
|
||||
set(AURORA_DAWN_VERSION "v20260603.191052" CACHE STRING "Dawn version tag (https://github.com/encounter/dawn-build/releases)")
|
||||
set(AURORA_SDL3_VERSION "3.4.4" CACHE STRING "SDL3 version tag (https://github.com/libsdl-org/SDL/releases)")
|
||||
set(AURORA_NOD_VERSION "v2.0.0-alpha.8" CACHE STRING "nod version tag (https://github.com/encounter/nod/releases)")
|
||||
set(AURORA_LIBUSB_VERSION "1.0.30" CACHE STRING "libusb version tag (https://github.com/libusb/libusb/releases)")
|
||||
|
||||
# Platform-specific defaults
|
||||
if (CMAKE_CROSSCOMPILING)
|
||||
@@ -44,6 +45,7 @@ set(AURORA_SDL3_PROVIDER "${_default_provider}" CACHE STRING
|
||||
set_property(CACHE AURORA_SDL3_PROVIDER PROPERTY STRINGS auto vendor system package)
|
||||
set(AURORA_SDL3_LINKAGE "${_default_linkage}" CACHE STRING "SDL3 linkage type preference")
|
||||
set_property(CACHE AURORA_SDL3_LINKAGE PROPERTY STRINGS shared static)
|
||||
option(AURORA_SDL3_LIBUSB "Build the vendored SDL3 with libusb on Windows (official GameCube adapter support)" ON)
|
||||
|
||||
# nod (if AURORA_ENABLE_DVD)
|
||||
set(AURORA_NOD_PROVIDER "${_default_provider}" CACHE STRING
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# libusb for SDL3's HIDAPI joystick drivers on Windows.
|
||||
#
|
||||
# The official GameCube adapter (WUP-028) is a vendor-specific USB device, not HID, so SDL3
|
||||
# can only reach it through libusb - which the official SDL3 packages leave out. The vendored
|
||||
# SDL3 build compiles libusb from the pinned upstream release and links it in statically.
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(libusb
|
||||
URL "https://github.com/libusb/libusb/releases/download/v${AURORA_LIBUSB_VERSION}/libusb-${AURORA_LIBUSB_VERSION}.tar.bz2"
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
)
|
||||
# Upstream ships no CMakeLists.txt, so this only populates the source tree.
|
||||
FetchContent_MakeAvailable(libusb)
|
||||
|
||||
set(_libusb_root "${libusb_SOURCE_DIR}/libusb")
|
||||
add_library(usb-1.0 STATIC
|
||||
"${_libusb_root}/core.c"
|
||||
"${_libusb_root}/descriptor.c"
|
||||
"${_libusb_root}/hotplug.c"
|
||||
"${_libusb_root}/io.c"
|
||||
"${_libusb_root}/strerror.c"
|
||||
"${_libusb_root}/sync.c"
|
||||
"${_libusb_root}/os/events_windows.c"
|
||||
"${_libusb_root}/os/threads_windows.c"
|
||||
"${_libusb_root}/os/windows_common.c"
|
||||
"${_libusb_root}/os/windows_usbdk.c"
|
||||
"${_libusb_root}/os/windows_winusb.c"
|
||||
)
|
||||
target_include_directories(usb-1.0
|
||||
PUBLIC "${_libusb_root}"
|
||||
PRIVATE "${CMAKE_CURRENT_LIST_DIR}/libusb" "${_libusb_root}/os"
|
||||
)
|
||||
set_target_properties(usb-1.0 PROPERTIES UNITY_BUILD OFF)
|
||||
add_library(LibUSB::LibUSB ALIAS usb-1.0)
|
||||
|
||||
# SDL's FindLibUSB expects an installed copy. Satisfy its presence checks with this target
|
||||
# instead: the alias pre-empts the imported target it would otherwise create, and the link
|
||||
# probe is answered up front because the archive does not exist until build time.
|
||||
set(LibUSB_INCLUDE_PATH "${_libusb_root}" CACHE PATH "" FORCE)
|
||||
set(LibUSB_LIBRARY "usb-1.0" CACHE STRING "" FORCE)
|
||||
set(HAVE_LIBUSB_H 1 CACHE INTERNAL "" FORCE)
|
||||
set(SDL_HIDAPI_LIBUSB ON CACHE BOOL "" FORCE)
|
||||
set(SDL_HIDAPI_LIBUSB_SHARED OFF CACHE BOOL "" FORCE)
|
||||
@@ -0,0 +1,126 @@
|
||||
# Source-level fixes applied to the vendored SDL3 tree before it is built.
|
||||
#
|
||||
# Each patch is an exact string replacement, checked and idempotent: a tree that
|
||||
# already carries the fix is left alone, a tree where the anchor text is missing
|
||||
# (a different SDL version) stops the configure with a clear message instead of
|
||||
# silently building without the fix.
|
||||
#
|
||||
# Used two ways:
|
||||
# - included by AuroraSDL3Provider.cmake, which calls aurora_sdl3_apply_patches()
|
||||
# on a pre-provided source tree (FETCHCONTENT_SOURCE_DIR_SDL);
|
||||
# - run as `cmake -DSDL_SOURCE_DIR=<dir> -P AuroraSDL3Patches.cmake` from the
|
||||
# FetchContent PATCH_COMMAND for a freshly extracted tarball.
|
||||
|
||||
function(_aurora_sdl3_replace file description old new)
|
||||
file(READ "${file}" _content)
|
||||
string(FIND "${_content}" "${new}" _already)
|
||||
if (NOT _already EQUAL -1)
|
||||
return()
|
||||
endif ()
|
||||
string(FIND "${_content}" "${old}" _anchor)
|
||||
if (_anchor EQUAL -1)
|
||||
message(FATAL_ERROR "aurora: SDL3 patch '${description}' does not apply to ${file}; "
|
||||
"the vendored SDL version changed, review AuroraSDL3Patches.cmake")
|
||||
endif ()
|
||||
string(REPLACE "${old}" "${new}" _content "${_content}")
|
||||
file(WRITE "${file}" "${_content}")
|
||||
message(STATUS "aurora: SDL3 patch applied: ${description}")
|
||||
endfunction()
|
||||
|
||||
function(aurora_sdl3_apply_patches sdl_source_dir)
|
||||
set(_wii "${sdl_source_dir}/src/joystick/hidapi/SDL_hidapi_wii.c")
|
||||
if (NOT EXISTS "${_wii}")
|
||||
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapi_wii.c")
|
||||
endif ()
|
||||
|
||||
# Wii Remote: rebuild the joystick in place after an extension change.
|
||||
#
|
||||
# When a Nunchuk or Classic Controller is plugged in or pulled out, the driver
|
||||
# flags the joystick as disconnected so it can come back with the new name and
|
||||
# capabilities. But the HID device stays open, and HIDAPI only removes a
|
||||
# joystick-less device once its handle is closed, so nothing ever re-creates
|
||||
# the joystick: the remote is gone for good until the application toggles the
|
||||
# driver hint (which closes and re-opens the Bluetooth HID handle, something
|
||||
# some Windows Bluetooth stacks answer by dropping the link). Instead, when the
|
||||
# device has no joystick, probe the extension again (two bounded attempts, at
|
||||
# most once a second) and connect a new joystick on the still-open handle.
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (context field)"
|
||||
[==[ Uint64 m_ulNextMotionPlusCheck;
|
||||
bool m_bDisconnected;
|
||||
]==]
|
||||
[==[ Uint64 m_ulNextMotionPlusCheck;
|
||||
bool m_bDisconnected;
|
||||
Uint64 m_ulNextReconnect; /* WiiCompiled: see HIDAPI_DriverWii_UpdateDevice */
|
||||
]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (bounded extension probe)"
|
||||
[==[static EWiiExtensionControllerType ReadExtensionControllerType(SDL_HIDAPI_Device *device)
|
||||
{
|
||||
SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;
|
||||
EWiiExtensionControllerType eExtensionControllerType = k_eWiiExtensionControllerType_Unknown;
|
||||
const int MAX_ATTEMPTS = 20;
|
||||
int attempts = 0;
|
||||
]==]
|
||||
[==[static EWiiExtensionControllerType ReadExtensionControllerTypeAttempts(SDL_HIDAPI_Device *device, int MAX_ATTEMPTS)
|
||||
{
|
||||
SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;
|
||||
EWiiExtensionControllerType eExtensionControllerType = k_eWiiExtensionControllerType_Unknown;
|
||||
int attempts = 0;
|
||||
]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (probe wrapper)"
|
||||
[==[static void UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
|
||||
{
|
||||
]==]
|
||||
[==[static EWiiExtensionControllerType ReadExtensionControllerType(SDL_HIDAPI_Device *device)
|
||||
{
|
||||
return ReadExtensionControllerTypeAttempts(device, 20);
|
||||
}
|
||||
|
||||
static void UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
|
||||
{
|
||||
]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (UpdateDevice)"
|
||||
[==[ if (device->num_joysticks > 0) {
|
||||
joystick = SDL_GetJoystickFromID(device->joysticks[0]);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
now = SDL_GetTicks();
|
||||
]==]
|
||||
[==[ if (device->num_joysticks > 0) {
|
||||
joystick = SDL_GetJoystickFromID(device->joysticks[0]);
|
||||
} else {
|
||||
/* WiiCompiled: the joystick was dropped (extension plugged or unplugged,
|
||||
* or a failed read) but the HID device is still open, and the device
|
||||
* list only removes a joystick-less device once its handle is closed.
|
||||
* Rebuild the joystick in place, so an extension change behaves like it
|
||||
* does on the console: the remote never goes away, it just changes
|
||||
* type. If the remote does not answer, keep the device and retry. */
|
||||
now = SDL_GetTicks();
|
||||
if (ctx->m_ulNextReconnect != 0 && now < ctx->m_ulNextReconnect) {
|
||||
return false;
|
||||
}
|
||||
ctx->m_ulNextReconnect = now + 1000;
|
||||
{
|
||||
EWiiExtensionControllerType type = ReadExtensionControllerTypeAttempts(device, 2);
|
||||
if (type == k_eWiiExtensionControllerType_Unknown) {
|
||||
return false;
|
||||
}
|
||||
ctx->m_bDisconnected = false;
|
||||
ctx->m_ulLastInput = now;
|
||||
ctx->m_ulNextReconnect = 0;
|
||||
ctx->m_eExtensionControllerType = type;
|
||||
UpdateDeviceIdentity(device);
|
||||
SDL_LogDebug(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Reconnected joystick as %s", device->name);
|
||||
return HIDAPI_JoystickConnected(device, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
now = SDL_GetTicks();
|
||||
]==])
|
||||
endfunction()
|
||||
|
||||
# Script mode (FetchContent PATCH_COMMAND).
|
||||
if (CMAKE_SCRIPT_MODE_FILE AND DEFINED SDL_SOURCE_DIR)
|
||||
aurora_sdl3_apply_patches("${SDL_SOURCE_DIR}")
|
||||
endif ()
|
||||
@@ -129,12 +129,24 @@ elseif (_aurora_sdl3_provider STREQUAL "vendor")
|
||||
endif ()
|
||||
if (WIN32)
|
||||
set(SDL_LIBC ON CACHE BOOL "Use the system C library" FORCE)
|
||||
if (AURORA_SDL3_LIBUSB)
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/AuroraLibUSB.cmake")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
include(FetchContent)
|
||||
# Source fixes for the vendored SDL (see AuroraSDL3Patches.cmake). A freshly
|
||||
# downloaded tarball is patched by the PATCH_COMMAND; a tree handed in through
|
||||
# FETCHCONTENT_SOURCE_DIR_SDL skips the download steps, so patch it here.
|
||||
set(_aurora_sdl3_patches "${CMAKE_CURRENT_LIST_DIR}/AuroraSDL3Patches.cmake")
|
||||
include("${_aurora_sdl3_patches}")
|
||||
if (DEFINED FETCHCONTENT_SOURCE_DIR_SDL AND EXISTS "${FETCHCONTENT_SOURCE_DIR_SDL}")
|
||||
aurora_sdl3_apply_patches("${FETCHCONTENT_SOURCE_DIR_SDL}")
|
||||
endif ()
|
||||
FetchContent_Declare(SDL
|
||||
URL "https://github.com/libsdl-org/SDL/releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz"
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
PATCH_COMMAND "${CMAKE_COMMAND}" -DSDL_SOURCE_DIR=<SOURCE_DIR> -P "${_aurora_sdl3_patches}"
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(SDL)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/* libusb build configuration for the Windows backend (MinGW/Clang). */
|
||||
#pragma once
|
||||
|
||||
#define PLATFORM_WINDOWS 1
|
||||
#define ENABLE_LOGGING 1
|
||||
#define DEFAULT_VISIBILITY
|
||||
#define HAVE_STRUCT_TIMESPEC 1
|
||||
#define PRINTF_FORMAT(a, b) __attribute__((__format__(__printf__, a, b)))
|
||||
@@ -255,6 +255,14 @@ FrameWorkerState g_frameWorker;
|
||||
bool frame_worker_requested() noexcept {
|
||||
#ifdef AURORA_ENABLE_GX
|
||||
static const bool enabled = [] {
|
||||
#if defined(__APPLE__)
|
||||
// ImGui's SDL backend may raise an SDL window from ImGui::NewFrame(). On
|
||||
// macOS that reaches AppKit, whose window operations are main-thread-only;
|
||||
// doing it on the frame worker terminates the process with EXC_BREAKPOINT.
|
||||
// Keep all SDL/ImGui work on the calling thread until the worker no longer
|
||||
// owns frame preparation on Apple platforms.
|
||||
return false;
|
||||
#endif
|
||||
#if defined(_WIN32)
|
||||
// RenderDoc's D3D12 layer is injected before Aurora starts and needs device and command
|
||||
// ownership on one thread, so keep frame submission synchronous there.
|
||||
@@ -1516,6 +1524,15 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
|
||||
|
||||
// Phase 3: hand the encoded group to whoever owns presentation.
|
||||
void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool interpolationActive) {
|
||||
#if defined(__APPLE__)
|
||||
(void)interpolationActive;
|
||||
// Presenting reaches SDL/AppKit, whose window operations must stay on the
|
||||
// main thread. Interpolation normally starts the presenter worker, so keep
|
||||
// its jobs synchronous on Apple platforms.
|
||||
for (const auto& job : presentationJobs) {
|
||||
present_presentation_job(job);
|
||||
}
|
||||
#else
|
||||
// Keep presentation on the presenter whenever the async frame worker runs, even with
|
||||
// interpolation off, so every mode shares one surface/resize path. RenderDoc keeps the sync path.
|
||||
if (frame_worker_requested() || interpolationActive ||
|
||||
@@ -1526,6 +1543,7 @@ void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool
|
||||
present_presentation_job(job);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void record_frame_telemetry() {
|
||||
|
||||
@@ -111,7 +111,9 @@ ECardResult CardGciFolder::createFile(const char* filename, size_t size, FileHan
|
||||
}
|
||||
|
||||
gciFileHeader->swapEndian();
|
||||
m_files.push_back({*gciFileHeader, fileSize, reinterpret_cast<const char8_t*>(gciFilename.c_str()), false}); // push non-endian swapped header first
|
||||
// push non-endian swapped header first
|
||||
m_files.push_back({*gciFileHeader, fileSize,
|
||||
std::u8string(gciFilename.begin(), gciFilename.end()), false});
|
||||
handleOut = FileHandle(m_files.size() - 1, 0);
|
||||
|
||||
return ECardResult::READY;
|
||||
|
||||
@@ -175,7 +175,7 @@ void CARDInit(const char* game, const char* maker) {
|
||||
|
||||
std::filesystem::path cardWorkingDir;
|
||||
if (aurora::g_config.userPath != nullptr)
|
||||
cardWorkingDir = reinterpret_cast<const char8_t*>(aurora::g_config.userPath);
|
||||
cardWorkingDir = fs_path_from_string(aurora::g_config.userPath);
|
||||
else
|
||||
cardWorkingDir = std::filesystem::current_path();
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "../../fs_helper.hpp"
|
||||
#include "../../input.hpp"
|
||||
#include "../../internal.hpp"
|
||||
#include <dolphin/pad.h>
|
||||
#include <dolphin/si.h>
|
||||
#include <SDL3/SDL_mouse.h>
|
||||
#include <SDL3/SDL_joystick.h>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <sys/stat.h>
|
||||
#include <ranges>
|
||||
|
||||
@@ -191,6 +194,32 @@ std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsJoyPair{{
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT},
|
||||
}};
|
||||
|
||||
// Wii U Pro Controllers through SDL's HIDAPI Wii driver. No SDL_GamepadType
|
||||
// singles them out, so they are picked by the name the driver gives them (see
|
||||
// __PADSetDefaultMapping). Wii Remotes, with or without a Nunchuk or Classic
|
||||
// Controller, are read by the game through KPAD instead and never get a
|
||||
// GameCube mapping (the runtime hides those ports from PADRead).
|
||||
|
||||
// Nintendo's labelled layout: A on the right accelerates, B at the bottom
|
||||
// brakes. SDL's Wii driver reports ZL/ZR as the LEFT_TRIGGER/RIGHT_TRIGGER
|
||||
// axes, never as shoulder buttons, so they are left unbound here and picked up
|
||||
// by aurora's default axis mapping (g_defaultAxes) the same way every
|
||||
// analog-trigger pad's L/R is.
|
||||
std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsWiiUPro{{
|
||||
{SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_A},
|
||||
{SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_B},
|
||||
{SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_X},
|
||||
{SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_Y},
|
||||
{SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START},
|
||||
{SDL_GAMEPAD_BUTTON_BACK, PAD_TRIGGER_Z},
|
||||
{PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_L},
|
||||
{PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_R},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT},
|
||||
}};
|
||||
|
||||
std::array<PADKeyButtonBinding, PAD_BUTTON_COUNT> g_defaultKeys{{
|
||||
{PAD_KEY_INVALID, PAD_BUTTON_A},
|
||||
{PAD_KEY_INVALID, PAD_BUTTON_B},
|
||||
@@ -283,7 +312,7 @@ constexpr PADCLampRegion ClampRegion{
|
||||
|
||||
bool g_initialized;
|
||||
bool g_keyboardBindingsLoaded = false;
|
||||
bool g_blockPAD = false;
|
||||
std::atomic_bool g_blockPAD{false};
|
||||
bool g_suppressHeldOnRead = false;
|
||||
std::array<PADButton, PAD_CHANMAX> g_suppressedButtons{};
|
||||
std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
|
||||
@@ -407,8 +436,26 @@ static void reset_alt_button_mapping(aurora::input::GameController* controller)
|
||||
}
|
||||
}
|
||||
|
||||
// SDL's hidapi Wii driver names the pad "Nintendo Wii U Pro Controller"; the
|
||||
// other names it produces are Wii Remotes, which the game reads through KPAD
|
||||
// and which therefore never take a GameCube mapping.
|
||||
static bool wii_default_mapping(const aurora::input::GameController* controller,
|
||||
std::array<PADButtonMapping, PAD_BUTTON_COUNT>& out) {
|
||||
const char* name = SDL_GetGamepadName(controller->m_controller);
|
||||
if (name == nullptr || SDL_strstr(name, "Wii U Pro Controller") == nullptr) {
|
||||
return false;
|
||||
}
|
||||
out = g_defaultButtonsWiiUPro;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Picks the default button table for a controller by name (Wii pads) or SDL gamepad type.
|
||||
void __PADSetDefaultMapping(aurora::input::GameController* controller) /* NOLINT(*-reserved-identifier) */
|
||||
{
|
||||
if (wii_default_mapping(controller, controller->m_buttonMapping)) {
|
||||
reset_alt_button_mapping(controller);
|
||||
return;
|
||||
}
|
||||
switch (SDL_GetGamepadType(controller->m_controller)) {
|
||||
case SDL_GAMEPAD_TYPE_XBOX360:
|
||||
controller->m_buttonMapping = g_defaultButtonsXBox360;
|
||||
@@ -491,7 +538,7 @@ void __PADLoadMapping(aurora::input::GameController* controller) /* NOLINT(*-re
|
||||
return;
|
||||
}
|
||||
|
||||
std::string basePath{aurora::g_config.userPath};
|
||||
const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
|
||||
if (!controller->m_mappingLoaded) {
|
||||
__PADSetDefaultMapping(controller);
|
||||
controller->m_axisMapping = g_defaultAxes;
|
||||
@@ -499,8 +546,9 @@ void __PADLoadMapping(aurora::input::GameController* controller) /* NOLINT(*-re
|
||||
|
||||
controller->m_mappingLoaded = true;
|
||||
|
||||
const auto path = fmt::format("{}/{}_{:04X}_{:04X}.controller", basePath, PADGetName(playerIndex), controller->m_vid,
|
||||
controller->m_pid);
|
||||
const auto path = fs_path_to_string(
|
||||
basePath / fmt::format("{}_{:04X}_{:04X}.controller", PADGetName(playerIndex), controller->m_vid,
|
||||
controller->m_pid));
|
||||
SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "rb");
|
||||
if (file == nullptr) {
|
||||
return;
|
||||
@@ -659,7 +707,8 @@ u32 PADRead(PADStatus* status) {
|
||||
|
||||
int numKeys = 0;
|
||||
const bool* kbState = SDL_GetKeyboardState(&numKeys);
|
||||
const bool captureHeldInput = g_suppressHeldOnRead && !g_blockPAD;
|
||||
const bool inputBlocked = g_blockPAD.load(std::memory_order_acquire);
|
||||
const bool captureHeldInput = g_suppressHeldOnRead && !inputBlocked;
|
||||
g_suppressHeldOnRead = false;
|
||||
|
||||
uint32_t rumbleSupport = 0;
|
||||
@@ -741,6 +790,47 @@ u32 PADRead(PADStatus* status) {
|
||||
|
||||
if (controller) {
|
||||
EnsureMappingLoaded(controller);
|
||||
|
||||
// Wii U Pro Controller raw D-pad fallback. SDL's HIDAPI Wii driver posts
|
||||
// the D-pad as joystick buttons 11-14 (the SDL_GAMEPAD_BUTTON_DPAD_*
|
||||
// values) and never as a hat, but the mapping SDL generates for HIDAPI
|
||||
// pads binds the D-pad to hat 0, so SDL_GetGamepadButton(DPAD_*) stays
|
||||
// false. Keep this restricted to the Wii driver's pad so raw button
|
||||
// indices don't interfere with other controller types.
|
||||
const char* name = SDL_GetGamepadName(controller->m_controller);
|
||||
const bool isWiiUPro = name != nullptr && SDL_strstr(name, "Wii U Pro Controller") != nullptr;
|
||||
|
||||
if (isWiiUPro) {
|
||||
SDL_Joystick* joystick =
|
||||
SDL_GetGamepadJoystick(controller->m_controller);
|
||||
|
||||
uint32_t raw = 0;
|
||||
const int buttonCount = SDL_GetNumJoystickButtons(joystick);
|
||||
|
||||
for (int b = 0; b < buttonCount && b < 32; ++b) {
|
||||
if (SDL_GetJoystickButton(joystick, b)) {
|
||||
raw |= (1u << b);
|
||||
}
|
||||
}
|
||||
|
||||
// Up = button 11
|
||||
if (raw & (1u << 11)) {
|
||||
status[i].button |= PAD_BUTTON_UP;
|
||||
}
|
||||
// Down = button 12
|
||||
if (raw & (1u << 12)) {
|
||||
status[i].button |= PAD_BUTTON_DOWN;
|
||||
}
|
||||
// Left = button 13
|
||||
if (raw & (1u << 13)) {
|
||||
status[i].button |= PAD_BUTTON_LEFT;
|
||||
}
|
||||
// Right = button 14
|
||||
if (raw & (1u << 14)) {
|
||||
status[i].button |= PAD_BUTTON_RIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
bool leftTriggerSet = false;
|
||||
bool rightTriggerSet = false;
|
||||
std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet,
|
||||
@@ -774,6 +864,7 @@ u32 PADRead(PADStatus* status) {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// TODO: Add serializable mappings for these (probably not necessary)?
|
||||
static constexpr std::array<std::pair<SDL_GamepadButton, PADExtButton>, PAD_EXT_BUTTON_COUNT> kExtButtonMappings{{
|
||||
{SDL_GAMEPAD_BUTTON_BACK, PAD_BUTTON_BACK},
|
||||
@@ -882,7 +973,7 @@ u32 PADRead(PADStatus* status) {
|
||||
}
|
||||
}
|
||||
|
||||
if (g_blockPAD) {
|
||||
if (inputBlocked) {
|
||||
neutralize_status(status[i]);
|
||||
} else {
|
||||
apply_unblock_suppression(status[i], i, captureHeldInput);
|
||||
@@ -1247,8 +1338,8 @@ constexpr uint32_t k_keyboardMagic = SBIG('KBND');
|
||||
constexpr int32_t k_keyboardVersion = 3;
|
||||
|
||||
static void load_keyboard_bindings() {
|
||||
const auto filePath = std::filesystem::path{aurora::g_config.userPath} / "keyboard_bindings.dat";
|
||||
SDL_IOStream* file = SDL_IOFromFile(filePath.string().c_str(), "rb");
|
||||
const auto filePath = fs_path_from_string(aurora::g_config.userPath) / "keyboard_bindings.dat";
|
||||
SDL_IOStream* file = SDL_IOFromFile(fs_path_to_string(filePath).c_str(), "rb");
|
||||
if (file == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -1317,10 +1408,11 @@ static void load_keyboard_bindings() {
|
||||
}
|
||||
|
||||
static void save_keyboard_bindings() {
|
||||
const auto filePath = std::filesystem::path{aurora::g_config.userPath} / "keyboard_bindings.dat";
|
||||
SDL_IOStream* file = SDL_IOFromFile(filePath.string().c_str(), "wb");
|
||||
const auto filePath = fs_path_from_string(aurora::g_config.userPath) / "keyboard_bindings.dat";
|
||||
const auto filePathStr = fs_path_to_string(filePath);
|
||||
SDL_IOStream* file = SDL_IOFromFile(filePathStr.c_str(), "wb");
|
||||
if (file == nullptr) {
|
||||
aurora::input::Log.warn("save_keyboard_bindings: failed to open {} for writing", filePath.string());
|
||||
aurora::input::Log.warn("save_keyboard_bindings: failed to open {} for writing", filePathStr);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1344,14 +1436,14 @@ void __PADWriteDeadZones(SDL_IOStream* file, // NOLINT(*-reserved-identifier)
|
||||
}
|
||||
|
||||
void PADSerializeMappings() {
|
||||
const std::filesystem::path basePath{aurora::g_config.userPath};
|
||||
const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
|
||||
|
||||
for (auto& controller : aurora::input::g_GameControllers | std::views::values) {
|
||||
EnsureMappingLoaded(&controller);
|
||||
const auto filePath =
|
||||
basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index),
|
||||
controller.m_vid, controller.m_pid);
|
||||
std::string filePathStr = filePath.string();
|
||||
std::string filePathStr = fs_path_to_string(filePath);
|
||||
|
||||
// don't truncate the file if it already exists
|
||||
const char* openMode = std::filesystem::exists(filePath) ? "r+b" : "wb";
|
||||
@@ -1370,7 +1462,7 @@ void PADSerializeMappings() {
|
||||
// start writing data at next 32-byte aligned offset
|
||||
const int64_t dataStart = SDL_TellIO(file) + 31 & ~31;
|
||||
if (dataStart == -1) {
|
||||
aurora::input::Log.warn("Unable to seek in controller bindings! Path: \"{}\"", filePath.string());
|
||||
aurora::input::Log.warn("Unable to seek in controller bindings! Path: \"{}\"", filePathStr);
|
||||
return;
|
||||
}
|
||||
SDL_SeekIO(file, dataStart, SDL_IO_SEEK_SET);
|
||||
@@ -1530,12 +1622,12 @@ void PADRestoreDefaultMapping(const u32 port) {
|
||||
}
|
||||
|
||||
void PADBlockInput(const bool block) {
|
||||
if (g_blockPAD && !block) {
|
||||
if (g_blockPAD.exchange(block, std::memory_order_acq_rel) && !block) {
|
||||
g_suppressHeldOnRead = true;
|
||||
}
|
||||
g_blockPAD = block;
|
||||
}
|
||||
|
||||
|
||||
SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) {
|
||||
const auto* ctrl = __PADGetControllerForIndex(index);
|
||||
if (ctrl == nullptr) {
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
/**
|
||||
* Converts a std::filesystem::path to a std::string, UTF-8, without exploding on Windows.
|
||||
* Narrow path strings crossing the aurora boundary are UTF-8. path::string() and the
|
||||
* char path constructor go through the ANSI codepage on Windows, so they must not be
|
||||
* used for anything the host handed us or hands back to SDL, sqlite or ImGui.
|
||||
*/
|
||||
inline std::string fs_path_to_string(const std::filesystem::path& path) {
|
||||
const auto u8str = path.u8string();
|
||||
return { reinterpret_cast<const char*>(u8str.c_str()) };
|
||||
return { reinterpret_cast<const char*>(u8str.c_str()), u8str.size() };
|
||||
}
|
||||
|
||||
inline std::filesystem::path fs_path_from_string(std::string_view utf8) {
|
||||
return std::filesystem::path(std::u8string(utf8.begin(), utf8.end()));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "clear.hpp"
|
||||
#include "../gx/pipeline.hpp"
|
||||
#include "../fs_helper.hpp"
|
||||
#include "../sqlite_utils.hpp"
|
||||
#include "../webgpu/gpu.hpp"
|
||||
|
||||
@@ -715,7 +716,7 @@ static bool prepare_pipeline_cache_db() {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto path = (std::filesystem::path{g_config.pipelineCachePath} / "pipeline_cache.db").string();
|
||||
const auto path = fs_path_to_string(fs_path_from_string(g_config.pipelineCachePath) / "pipeline_cache.db");
|
||||
auto ret = sqlite3_open(path.c_str(), &g_pipelineCacheDb);
|
||||
if (ret != SQLITE_OK) {
|
||||
Log.error("Failed to open pipeline cache database: {}", sqlite3_errmsg(g_pipelineCacheDb));
|
||||
|
||||
@@ -506,8 +506,8 @@ void build_index() noexcept {
|
||||
return;
|
||||
}
|
||||
|
||||
auto userPath = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.userPath)};
|
||||
auto cachePath = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.cachePath)};
|
||||
auto userPath = fs_path_from_string(g_config.userPath);
|
||||
auto cachePath = fs_path_from_string(g_config.cachePath);
|
||||
|
||||
s_replacementRoot = userPath / "texture_replacements";
|
||||
s_dumpRoot = cachePath / "texture_dumps";
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <SDL3/SDL_render.h>
|
||||
|
||||
#include "fs_helper.hpp"
|
||||
#include "internal.hpp"
|
||||
#include "webgpu/gpu.hpp"
|
||||
#include "window.hpp"
|
||||
@@ -37,7 +38,7 @@ void remove_legacy_ini_file(const char* basePath) noexcept {
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(std::filesystem::path{basePath} / "imgui.ini", ec);
|
||||
std::filesystem::remove(fs_path_from_string(basePath) / "imgui.ini", ec);
|
||||
}
|
||||
|
||||
void create_context() noexcept {
|
||||
|
||||
@@ -255,6 +255,18 @@ IdentityMatch identity_match(const ControllerIdentity& saved, const ControllerId
|
||||
: IdentityMatch::None;
|
||||
}
|
||||
|
||||
void assign_player_index(GameController& controller, int32_t port) {
|
||||
SDL_SetGamepadPlayerIndex(controller.m_controller, port);
|
||||
controller.m_playerIndex = port;
|
||||
}
|
||||
|
||||
// SDL forgets the index for devices mapped after connect, so player_index() falls
|
||||
// back to the cached copy; both have to move together or a port looks doubly taken.
|
||||
int32_t effective_player_index(const GameController& controller) {
|
||||
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
|
||||
return player >= 0 ? player : controller.m_playerIndex;
|
||||
}
|
||||
|
||||
bool is_instance_claimed(const std::array<Uint32, PAD_MAX_CONTROLLERS>& claimedControllers, size_t claimedCount,
|
||||
Uint32 instance) {
|
||||
return std::find(claimedControllers.begin(), claimedControllers.begin() + claimedCount, instance) !=
|
||||
@@ -269,10 +281,10 @@ void apply_port_preferences() noexcept {
|
||||
}
|
||||
|
||||
for (auto& [instance, controller] : g_GameControllers) {
|
||||
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
|
||||
const int32_t player = effective_player_index(controller);
|
||||
if (player >= 0 && player < PAD_MAX_CONTROLLERS && g_portPreferences[player].state != PortPreferenceState::Unset) {
|
||||
// Keep SDL's default player assignment from taking explicitly configured ports
|
||||
SDL_SetGamepadPlayerIndex(controller.m_controller, -1);
|
||||
assign_player_index(controller, -1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +305,7 @@ void apply_port_preferences() noexcept {
|
||||
|
||||
switch (identity_match(preference.identity, controller_identity(controller))) {
|
||||
case IdentityMatch::Exact:
|
||||
SDL_SetGamepadPlayerIndex(controller.m_controller, static_cast<int32_t>(port));
|
||||
assign_player_index(controller, static_cast<int32_t>(port));
|
||||
claimedControllers[claimedCount++] = instance;
|
||||
fallbackController = nullptr;
|
||||
break;
|
||||
@@ -311,11 +323,45 @@ void apply_port_preferences() noexcept {
|
||||
}
|
||||
|
||||
if (fallbackController != nullptr) {
|
||||
SDL_SetGamepadPlayerIndex(fallbackController->m_controller, static_cast<int32_t>(port));
|
||||
assign_player_index(*fallbackController, static_cast<int32_t>(port));
|
||||
claimedControllers[claimedCount++] = fallbackInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SDL only hands out a player index when the device already had a gamepad mapping
|
||||
// at connect time, so anything mapped later (the setup wizard) stays at -1.
|
||||
void ensure_player_index(GameController& controller) noexcept {
|
||||
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
|
||||
if (player >= 0) {
|
||||
controller.m_playerIndex = player;
|
||||
return;
|
||||
}
|
||||
if (controller.m_playerIndex >= 0) {
|
||||
return;
|
||||
}
|
||||
ensure_port_preferences_loaded();
|
||||
const auto claim = [&](bool skipConfiguredPorts) {
|
||||
for (int32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||
if (skipConfiguredPorts && g_portPreferences[port].state != PortPreferenceState::Unset) {
|
||||
continue;
|
||||
}
|
||||
const bool taken = std::any_of(g_GameControllers.begin(), g_GameControllers.end(), [&](const auto& entry) {
|
||||
return entry.second.m_controller != controller.m_controller && effective_player_index(entry.second) == port;
|
||||
});
|
||||
if (!taken) {
|
||||
assign_player_index(controller, port);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// Explicitly configured ports are only used as a last resort so a hot-plugged
|
||||
// controller cannot steal the port its preferred device will claim.
|
||||
if (!claim(true)) {
|
||||
claim(false);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GameController* get_controller_for_player(uint32_t player) noexcept {
|
||||
@@ -355,6 +401,8 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
|
||||
return -1;
|
||||
}
|
||||
controller.m_isGameCube = controller.m_vid == 0x057E && controller.m_pid == 0x0337;
|
||||
const char* serial = SDL_GetGamepadSerial(ctrl);
|
||||
controller.m_gameCubeUseOrdinaryStop = controller.m_isGameCube && serial && "GCP+"sv == serial;
|
||||
if (controller.m_isGameCube ||
|
||||
(SDL_GetGamepadType(ctrl) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO && controller.m_pid == 0x2073)) {
|
||||
controller.m_deadZones.emulateTriggers = false;
|
||||
@@ -364,6 +412,7 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
|
||||
controller.m_hasRgbLed = SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false);
|
||||
SDL_JoystickID instance = SDL_GetJoystickID(SDL_GetGamepadJoystick(ctrl));
|
||||
g_GameControllers[instance] = controller;
|
||||
ensure_player_index(g_GameControllers[instance]);
|
||||
apply_port_preferences();
|
||||
return instance;
|
||||
}
|
||||
@@ -371,6 +420,19 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool refresh_controller(SDL_JoystickID instance) noexcept {
|
||||
const auto it = g_GameControllers.find(instance);
|
||||
if (it == g_GameControllers.end()) {
|
||||
return false;
|
||||
}
|
||||
// The SDL mapping changed underneath us; drop the cached PAD bindings so they
|
||||
// are rebuilt from the new one.
|
||||
it->second.m_mappingLoaded = false;
|
||||
ensure_player_index(it->second);
|
||||
apply_port_preferences();
|
||||
return true;
|
||||
}
|
||||
|
||||
void remove_controller(Uint32 instance) noexcept {
|
||||
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
||||
SDL_CloseGamepad(it->second.m_controller);
|
||||
@@ -421,6 +483,13 @@ bool controller_has_rumble(Uint32 instance) noexcept {
|
||||
void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t high_freq_intensity,
|
||||
uint16_t duration_ms) noexcept {
|
||||
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
||||
// GC Pocket+ has been observed continuing to vibrate after a hard stop;
|
||||
// an ordinary stop cleared it. With GAMECUBE_RUMBLE_BRAKE enabled, SDL
|
||||
// encodes (0, 1) as adapter command 0, whereas (0, 0) sends command 2.
|
||||
// Apply the workaround here so shutdown uses the same stop as PAD calls.
|
||||
if (it->second.m_gameCubeUseOrdinaryStop && low_freq_intensity == 0 && high_freq_intensity == 0) {
|
||||
high_freq_intensity = 1;
|
||||
}
|
||||
SDL_RumbleGamepad(it->second.m_controller, low_freq_intensity, high_freq_intensity, duration_ms);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ extern Module Log;
|
||||
struct GameController {
|
||||
SDL_Gamepad* m_controller = nullptr;
|
||||
bool m_isGameCube = false;
|
||||
bool m_gameCubeUseOrdinaryStop = false;
|
||||
Sint32 m_index = -1;
|
||||
Sint32 m_playerIndex = -1;
|
||||
bool m_hasRumble = false;
|
||||
@@ -51,6 +52,7 @@ struct GameController {
|
||||
GameController* get_controller_for_player(uint32_t player) noexcept;
|
||||
Sint32 get_instance_for_player(uint32_t player) noexcept;
|
||||
SDL_JoystickID add_controller(SDL_JoystickID which) noexcept;
|
||||
bool refresh_controller(SDL_JoystickID instance) noexcept;
|
||||
void remove_controller(Uint32 instance) noexcept;
|
||||
Sint32 player_index(Uint32 instance) noexcept;
|
||||
void set_player_index(Uint32 instance, Sint32 index) noexcept;
|
||||
|
||||
@@ -137,7 +137,7 @@ static void prune_stale_rows() {
|
||||
static bool cache_init_core() {
|
||||
Log.debug("SQLite version {}", sqlite3_libversion());
|
||||
|
||||
const auto path = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.cachePath)} / "dawn_cache.db";
|
||||
const auto path = fs_path_from_string(g_config.cachePath) / "dawn_cache.db";
|
||||
std::string file = fs_path_to_string(path);
|
||||
Log.debug("Using dawn cache at {}", file);
|
||||
auto ret = sqlite3_open(file.c_str(), &db);
|
||||
@@ -165,8 +165,12 @@ static bool cache_init_core() {
|
||||
db = nullptr;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(path, ec);
|
||||
std::filesystem::remove(std::filesystem::path{file + "-wal"}, ec);
|
||||
std::filesystem::remove(std::filesystem::path{file + "-shm"}, ec);
|
||||
auto wal = path;
|
||||
wal += "-wal";
|
||||
std::filesystem::remove(wal, ec);
|
||||
auto shm = path;
|
||||
shm += "-shm";
|
||||
std::filesystem::remove(shm, ec);
|
||||
ret = sqlite3_open(file.c_str(), &db);
|
||||
if (ret != SQLITE_OK) {
|
||||
Log.error("Failed to recreate database: {}", sqlite3_errmsg(db));
|
||||
|
||||
@@ -294,6 +294,15 @@ void process_event(SDL_Event& event) {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_GAMEPAD_REMAPPED: {
|
||||
if (input::refresh_controller(event.gdevice.which)) {
|
||||
g_events.push_back(AuroraEvent{
|
||||
.type = AURORA_CONTROLLER_ADDED,
|
||||
.controller = event.gdevice.which,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_GAMEPAD_REMOVED: {
|
||||
input::remove_controller(event.gdevice.which);
|
||||
g_events.push_back(AuroraEvent{
|
||||
|
||||
+128
-31
@@ -1,15 +1,28 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(mkw_recompiled)
|
||||
|
||||
if(NOT WIN32 OR NOT MINGW OR NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
|
||||
NOT CMAKE_SIZEOF_VOID_P EQUAL 8 OR
|
||||
NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
message(FATAL_ERROR "WiiCompiled requires 64-bit LLVM-MinGW Clang on Windows")
|
||||
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
|
||||
endif()
|
||||
|
||||
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_PLATFORM_WINDOWS TRUE)
|
||||
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$")
|
||||
# The first native macOS target is Apple Silicon. Intel and universal
|
||||
# binaries remain future compatibility work; do not silently claim them.
|
||||
set(MKW_PLATFORM_MACOS TRUE)
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$")
|
||||
set(MKW_PLATFORM_LINUX TRUE)
|
||||
else()
|
||||
message(FATAL_ERROR
|
||||
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64")
|
||||
endif()
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
message(FATAL_ERROR "WiiCompiled only supports Release builds")
|
||||
endif()
|
||||
|
||||
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
|
||||
|
||||
# Preprocessor definitions that belong to this project's own code (the runtime,
|
||||
# the translated shards and the product glue) and to nothing else. They are
|
||||
# applied directory-scoped, immediately after the aurora add_subdirectory() call,
|
||||
@@ -46,6 +59,20 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
|
||||
target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
|
||||
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
|
||||
|
||||
# Linux guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
|
||||
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
|
||||
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
|
||||
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
|
||||
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
|
||||
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers
|
||||
# and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only.
|
||||
if(MKW_PLATFORM_LINUX)
|
||||
add_library(mkw_libco STATIC third_party/libco/libco.c)
|
||||
add_library(mkw::libco ALIAS mkw_libco)
|
||||
target_include_directories(mkw_libco PUBLIC third_party/libco)
|
||||
set_target_properties(mkw_libco PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
|
||||
# Runtime configuration is real TOML, parsed by toml11 rather than a project-
|
||||
# specific line parser. Keep it header-only and vendored so disconnected release
|
||||
# builds have exactly the same parser as developer builds.
|
||||
@@ -110,21 +137,35 @@ else()
|
||||
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
|
||||
endif()
|
||||
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE)
|
||||
elseif(MKW_PLATFORM_MACOS)
|
||||
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_VULKAN OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_METAL ON CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
|
||||
else()
|
||||
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE)
|
||||
|
||||
# Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are
|
||||
# missing (common on MinGW). Dawn only includes the header; no symbols are
|
||||
# referenced when PIX isn't present.
|
||||
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
|
||||
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
|
||||
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
|
||||
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h"
|
||||
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX is absent.\n")
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
|
||||
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
|
||||
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
|
||||
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h"
|
||||
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX isn't present.\n")
|
||||
endif()
|
||||
endif()
|
||||
# Deliberately NOT injected project-wide. Only a from-source Dawn build ever
|
||||
# includes DXProgrammableCapture.h, and this tree consumes Dawn as a prebuilt
|
||||
@@ -153,7 +194,9 @@ else()
|
||||
if(TARGET ${t})
|
||||
set_target_properties(${t} PROPERTIES UNITY_BUILD OFF)
|
||||
target_compile_options(${t} PRIVATE -O3 -ffast-math -w -pipe)
|
||||
target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR})
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR})
|
||||
endif()
|
||||
# Aurora's own sources include Windows headers and call std::min/max;
|
||||
# they relied on the old project-wide NOMINMAX that no longer leaks
|
||||
# into this subtree, so the define is applied per target here.
|
||||
@@ -198,6 +241,18 @@ endif()
|
||||
# a registration file is silently never compiled and never errors. The stale-glob
|
||||
# failure mode is worth far more than the milliseconds.
|
||||
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp")
|
||||
# HostContext's Apple Silicon backend is implemented in a small assembly
|
||||
# companion. It must be part of the product runtime as well as the
|
||||
# standalone context test; otherwise the final executable is missing
|
||||
# mkw_co_init/mkw_co_switch at link time.
|
||||
enable_language(ASM)
|
||||
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
else()
|
||||
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
|
||||
endif()
|
||||
set(MKW_PLATFORM_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/platform/host_platform.cpp")
|
||||
set(MKW_BASE_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/base_product.cpp")
|
||||
set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro_rewind_product.cpp")
|
||||
# The host ISA guard is the one translation unit that must not receive the
|
||||
@@ -205,25 +260,67 @@ set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro
|
||||
# mkw_runtime_common. See cmake/PublicProducts.cmake and the file's own header.
|
||||
set(MKW_CPU_BASELINE_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/host_cpu_baseline.cpp")
|
||||
list(REMOVE_ITEM SOURCES ${MKW_BASE_PRODUCT_SOURCE} ${MKW_RETRO_REWIND_PRODUCT_SOURCE}
|
||||
${MKW_CPU_BASELINE_SOURCE})
|
||||
${MKW_CPU_BASELINE_SOURCE} ${MKW_PLATFORM_SOURCE})
|
||||
|
||||
# This deliberately small library contains host services that are safe to
|
||||
# validate before guest memory and fiber work makes a full runtime build viable.
|
||||
add_library(mkw_platform STATIC "${MKW_PLATFORM_SOURCE}")
|
||||
target_include_directories(mkw_platform PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_platform PUBLIC cxx_std_17)
|
||||
set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
|
||||
|
||||
enable_testing()
|
||||
add_subdirectory(tests)
|
||||
|
||||
# The translator emits the complete, content-addressed source graph. Consuming
|
||||
# this one manifest keeps configure independent of the 28k generated function
|
||||
# files and of optional Retro Rewind artifacts such as code.map.
|
||||
set(MKW_TRANSLATED_SHARD_MANIFEST
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
|
||||
CACHE FILEPATH "Translator-owned aggregate shard manifest")
|
||||
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
message(FATAL_ERROR
|
||||
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
|
||||
"Run Translator.Cli emit-build-shards first; see translator/README.md.")
|
||||
endif()
|
||||
include("${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS})
|
||||
message(STATUS
|
||||
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; "
|
||||
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
|
||||
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
|
||||
if(MKW_BUILD_PRODUCTS)
|
||||
set(MKW_TRANSLATED_SHARD_MANIFEST
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
|
||||
CACHE FILEPATH "Translator-owned aggregate shard manifest")
|
||||
# The prebuilt export only needs the aurora/third-party closure configured above, so a
|
||||
# packaging machine without a translation stops here instead of failing.
|
||||
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
message(STATUS "No translator shard manifest; configuring the native prebuilt export only")
|
||||
return()
|
||||
endif()
|
||||
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
message(FATAL_ERROR
|
||||
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
|
||||
"Run Translator.Cli emit-build-shards first; see translator/README.md.")
|
||||
endif()
|
||||
include("${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS})
|
||||
message(STATUS
|
||||
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; "
|
||||
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
|
||||
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
|
||||
|
||||
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
|
||||
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
|
||||
else()
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# Compile-only audit of native runtime sources. It deliberately avoids
|
||||
# translated products until their host dependencies are portable.
|
||||
# These sources depend on generated/RuntimeConfig.h, which is emitted for
|
||||
# a particular game by the translator and is intentionally unavailable
|
||||
# in this platform-only configuration.
|
||||
set(MKW_MACOS_NATIVE_AUDIT_SOURCES ${SOURCES})
|
||||
list(REMOVE_ITEM MKW_MACOS_NATIVE_AUDIT_SOURCES
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/abi_bridge.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/hle/os/os_alarm.cpp")
|
||||
add_library(mkw_macos_native_compile OBJECT ${MKW_MACOS_NATIVE_AUDIT_SOURCES})
|
||||
target_include_directories(mkw_macos_native_compile PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include" "${CMAKE_CURRENT_LIST_DIR}/src"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/.." "${CMAKE_CURRENT_LIST_DIR}/../aurora-main/include")
|
||||
target_compile_features(mkw_macos_native_compile PRIVATE cxx_std_20)
|
||||
target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC)
|
||||
target_link_libraries(mkw_macos_native_compile PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
|
||||
mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
|
||||
message(STATUS "Translated product targets disabled (MKW_BUILD_PRODUCTS=OFF)")
|
||||
endif()
|
||||
|
||||
@@ -51,6 +51,16 @@ foreach(_dir IN LISTS _mkw_np_includes)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Dawn's own packaged config (DawnTargets.cmake) links dawn::webgpu_dawn against
|
||||
# Threads::Threads directly. A from-source aurora build resolves that as a side effect of
|
||||
# add_subdirectory(aurora-main) pulling in Dawn's own CMakeLists.txt; this mode never runs that
|
||||
# subdirectory at all, so nothing else would ever define it (verified directly: configuring without
|
||||
# this fails with "The link interface of target dawn::webgpu_dawn contains: Threads::Threads but
|
||||
# the target was not found"). find_package(Threads) is one of CMake's most basic finder modules and
|
||||
# is a no-op on Windows (its threading support is already part of the CRT), so this is safe on
|
||||
# every platform this package format targets, not just the one that first hit the failure.
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
# Dawn is a prebuilt package on both sides; resolve the same install tree the
|
||||
# package was built against so its imported target (and therefore
|
||||
# webgpu_dawn.dll / dxcompiler.dll / dxil.dll) is available exactly as a
|
||||
|
||||
@@ -25,6 +25,11 @@ if(EXISTS "${DATA_INIT_BLOB_ASM}")
|
||||
endif()
|
||||
list(REMOVE_DUPLICATES SOURCES)
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
find_library(MKW_IOKIT_FRAMEWORK IOKit REQUIRED)
|
||||
find_library(MKW_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
|
||||
endif()
|
||||
|
||||
function(mkw_apply_common_compile_options target)
|
||||
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
|
||||
endfunction()
|
||||
@@ -76,8 +81,14 @@ target_compile_definitions(mkw_runtime_common PRIVATE
|
||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
|
||||
elseif(MKW_PLATFORM_LINUX)
|
||||
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
|
||||
# media monitoring). Empty string on glibc >= 2.34 where dl* is in libc.
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
if(MKW_CPPWINRT_INCLUDE_DIR)
|
||||
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
|
||||
message(FATAL_ERROR
|
||||
@@ -121,16 +132,16 @@ set_target_properties(mkw_runtime_common PROPERTIES UNITY_BUILD ON UNITY_BUILD_M
|
||||
target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
mkw_apply_common_compile_options(mkw_runtime_common)
|
||||
|
||||
# Host ISA guard. Everything in MKW_ALL_BUILD_TARGETS below is compiled with
|
||||
# -march=x86-64-v3; this object library deliberately is not, which
|
||||
# is the whole point of keeping it out of mkw_runtime_common. It runs a CPUID
|
||||
# check from a C initializer so an unsupported machine gets a readable error
|
||||
# instead of an illegal-instruction crash. Excluded from the unity build and the
|
||||
# precompiled header because both are produced with the owning target's flags.
|
||||
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
|
||||
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
|
||||
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
|
||||
target_compile_options(mkw_cpu_baseline PRIVATE -w)
|
||||
# Host ISA guard. Windows and Linux x86_64 product targets use x86-64-v3, so
|
||||
# this object deliberately keeps the plain baseline ISA and checks the CPU
|
||||
# before any AVX2/FMA code can execute. AArch64 has no equivalent optional ISA
|
||||
# floor to probe: NEON/FMA are architectural requirements.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
|
||||
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
|
||||
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
|
||||
target_compile_options(mkw_cpu_baseline PRIVATE -w)
|
||||
endif()
|
||||
|
||||
if(NOT MKW_BASE_COMMON_SHARDS)
|
||||
message(FATAL_ERROR "Translator build graph contains no shared base shards")
|
||||
@@ -170,7 +181,9 @@ function(mkw_configure_product target)
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
|
||||
# Startup CPU check. Must stay a separate object library so it keeps the
|
||||
# plain baseline ISA while everything around it is built for x86-64-v3.
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>)
|
||||
endif()
|
||||
target_include_directories(${target} PRIVATE
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/include"
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src"
|
||||
@@ -186,10 +199,14 @@ function(mkw_configure_product target)
|
||||
# include the same fat translated headers; bound them by the same pool.
|
||||
mkw_bound_translated_compiles(${target})
|
||||
target_link_libraries(${target} PRIVATE
|
||||
mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
|
||||
target_link_libraries(${target} PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}")
|
||||
endif()
|
||||
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
|
||||
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
|
||||
aurora_copy_runtime_dlls(${target})
|
||||
@@ -204,26 +221,40 @@ function(mkw_configure_product target)
|
||||
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
|
||||
endif()
|
||||
|
||||
target_link_libraries(${target} PRIVATE
|
||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
|
||||
|
||||
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
foreach(runtime_dll libc++.dll libunwind.dll)
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}"
|
||||
OUTPUT_VARIABLE runtime_dll_path
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
if(NOT EXISTS "${runtime_dll_path}")
|
||||
get_filename_component(mkw_compiler_bin "${CMAKE_CXX_COMPILER}" DIRECTORY)
|
||||
set(runtime_dll_path "${mkw_compiler_bin}/${runtime_dll}")
|
||||
endif()
|
||||
if(NOT EXISTS "${runtime_dll_path}")
|
||||
message(FATAL_ERROR "llvm-mingw runtime DLL not found: ${runtime_dll}")
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${runtime_dll_path}" $<TARGET_FILE_DIR:${target}>)
|
||||
endforeach()
|
||||
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
elseif(MKW_PLATFORM_LINUX)
|
||||
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
|
||||
# files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own
|
||||
# target_link_libraries (object libraries don't carry usage requirements to a consumer
|
||||
# that isn't itself linked against as a target). fiber_manager.cpp's co_* calls live in
|
||||
# those objects, so the actual executable link needs mkw::libco directly, same as it
|
||||
# needs it independently of that first `if(WIN32)` branch above. ${CMAKE_DL_LIBS} is
|
||||
# here for the same reason: music_attenuation.cpp's dlopen(libdbus-1) lives in those
|
||||
# objects (empty string on glibc >= 2.34, where dl* is in libc).
|
||||
target_link_libraries(${target} PRIVATE mkw::libco ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
foreach(runtime_dll libc++.dll libunwind.dll)
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}"
|
||||
OUTPUT_VARIABLE runtime_dll_path
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
if(NOT EXISTS "${runtime_dll_path}")
|
||||
get_filename_component(mkw_compiler_bin "${CMAKE_CXX_COMPILER}" DIRECTORY)
|
||||
set(runtime_dll_path "${mkw_compiler_bin}/${runtime_dll}")
|
||||
endif()
|
||||
if(NOT EXISTS "${runtime_dll_path}")
|
||||
message(FATAL_ERROR "llvm-mingw runtime DLL not found: ${runtime_dll}")
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${runtime_dll_path}" $<TARGET_FILE_DIR:${target}>)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
set(MKW_WII_BOOTSTRAP_SOURCE_DIR "${MKW_RUNTIME_SOURCE_DIR}/assets/wii")
|
||||
if(NOT EXISTS "${MKW_WII_BOOTSTRAP_SOURCE_DIR}/shared2/wc24")
|
||||
@@ -282,11 +313,23 @@ else()
|
||||
message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)")
|
||||
endif()
|
||||
|
||||
# Windows and Linux x86_64 share the x86-64-v3 floor that the CPU baseline
|
||||
# object above checks. AArch64 builds are compiled locally for the host that
|
||||
# will run them, so both Linux and Apple Silicon use the compiler's native CPU
|
||||
# tuning rather than leaving target-specific performance on the table.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
|
||||
else()
|
||||
set(MKW_BASELINE_ARCH_FLAG "")
|
||||
endif()
|
||||
|
||||
set(MKW_ALL_BUILD_TARGETS
|
||||
mkw_runtime_common mkw_base_shared mkw_base_sensitive mkw_retro_sensitive
|
||||
mkw_retro_rewind_functions WiiCompiled RetroRewind)
|
||||
foreach(target IN LISTS MKW_ALL_BUILD_TARGETS)
|
||||
if(TARGET ${target})
|
||||
target_compile_options(${target} PRIVATE -march=x86-64-v3)
|
||||
if(TARGET ${target} AND MKW_BASELINE_ARCH_FLAG)
|
||||
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
@@ -1,38 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "runtime_config.h"
|
||||
#include "nand_path.h"
|
||||
#include "nand_settings.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace RuntimeConsoleIdentity {
|
||||
|
||||
struct Identity {
|
||||
std::string serial;
|
||||
std::string productCode;
|
||||
std::string area;
|
||||
std::string gameRegion;
|
||||
std::array<uint8_t, 6> mac;
|
||||
};
|
||||
|
||||
inline bool IsValidSerial(const std::string& serial) {
|
||||
return serial.size() == 9 &&
|
||||
serial != "000000000" &&
|
||||
std::all_of(serial.begin(), serial.end(),
|
||||
[](unsigned char value) { return std::isdigit(value) != 0; });
|
||||
}
|
||||
|
||||
inline Identity FromSerial(std::string serial) {
|
||||
// Keep Nintendo's Wii OUI. The suffix is derived from the persisted serial
|
||||
// Keep Nintendo's Wii OUI. The suffix is derived from the NAND serial
|
||||
// so every API exposes one coherent, stable virtual-console identity.
|
||||
uint32_t hash = 2166136261u;
|
||||
for (const unsigned char value : serial) {
|
||||
@@ -46,6 +36,7 @@ inline Identity FromSerial(std::string serial) {
|
||||
|
||||
return {
|
||||
std::move(serial),
|
||||
{}, {}, {},
|
||||
{
|
||||
0x00,
|
||||
0x09,
|
||||
@@ -57,82 +48,23 @@ inline Identity FromSerial(std::string serial) {
|
||||
};
|
||||
}
|
||||
|
||||
inline std::optional<std::string> ReadSerial(const std::filesystem::path& path) {
|
||||
std::ifstream input(path);
|
||||
std::string line;
|
||||
if (!input || !std::getline(input, line)) {
|
||||
return std::nullopt;
|
||||
inline Identity LoadFromNand() {
|
||||
const auto root = RuntimeNandPath::DiscoverNandRootPath();
|
||||
const auto settings = RuntimeNandSettings::Read(root);
|
||||
if (!settings || !RuntimeNandSettings::HasIdentity(*settings)) {
|
||||
RuntimeNandPath::FailNandRoot(
|
||||
"NAND setting.txt is missing or has invalid console identity fields (SERNO, CODE, AREA, GAME)",
|
||||
root / "title/00000001/00000002/data/setting.txt");
|
||||
}
|
||||
constexpr std::string_view prefix = "serial=";
|
||||
if (line.rfind(prefix, 0) != 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::string serial = line.substr(prefix.size());
|
||||
if (!IsValidSerial(serial)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return serial;
|
||||
}
|
||||
|
||||
inline bool WriteSerial(const std::filesystem::path& path, const std::string& serial) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::filesystem::path temporary = path.string() + ".tmp";
|
||||
{
|
||||
std::ofstream output(temporary, std::ios::trunc);
|
||||
if (!output) {
|
||||
return false;
|
||||
}
|
||||
output << "serial=" << serial << '\n';
|
||||
output.close();
|
||||
if (!output) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::rename(temporary, path, ec);
|
||||
if (!ec) {
|
||||
return true;
|
||||
}
|
||||
std::filesystem::remove(temporary, ec);
|
||||
return false;
|
||||
}
|
||||
|
||||
inline std::string GenerateSerial() {
|
||||
std::random_device entropy;
|
||||
std::seed_seq seed{
|
||||
entropy(),
|
||||
entropy(),
|
||||
entropy(),
|
||||
entropy(),
|
||||
};
|
||||
std::mt19937 generator(seed);
|
||||
std::uniform_int_distribution<uint32_t> distribution(100000000u, 999999999u);
|
||||
return std::to_string(distribution(generator));
|
||||
}
|
||||
|
||||
inline Identity LoadOrCreate(const std::filesystem::path& path) {
|
||||
if (const auto serial = ReadSerial(path)) {
|
||||
return FromSerial(*serial);
|
||||
}
|
||||
|
||||
const std::string generated = GenerateSerial();
|
||||
if (WriteSerial(path, generated)) {
|
||||
return FromSerial(generated);
|
||||
}
|
||||
|
||||
// Remain operational in a read-only environment. This fallback matches
|
||||
// Dolphin's deterministic serial while keeping the same valid identity shape.
|
||||
return FromSerial("123456789");
|
||||
Identity identity = FromSerial(settings->at("SERNO"));
|
||||
identity.productCode = settings->at("CODE");
|
||||
identity.area = settings->at("AREA");
|
||||
identity.gameRegion = settings->at("GAME");
|
||||
return identity;
|
||||
}
|
||||
|
||||
inline const Identity& Current() {
|
||||
static const Identity identity =
|
||||
LoadOrCreate(RuntimeConfigFile::ApplicationDataDirectory() / "ConsoleIdentity.txt");
|
||||
static const Identity identity = LoadFromNand();
|
||||
return identity;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
// The single vocabulary shared by everything that has to turn a Config.toml
|
||||
// controller name into a real button: the F10 settings bar, the macro engine,
|
||||
// and the startup mapping pass. Keeping one table here means a name that the
|
||||
// settings bar offers is always a name the config parser accepts, and vice
|
||||
// versa; the two used to drift because each side carried its own copy.
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
namespace ControllerNames {
|
||||
|
||||
// A GameCube button as the game sees it, with the Config.toml key that selects
|
||||
// it. Order matches RuntimeConfigFile::kControllerButtonKeys.
|
||||
struct GameCubeButtonItem {
|
||||
const char* configKey;
|
||||
const char* label;
|
||||
PADButton padButton;
|
||||
};
|
||||
|
||||
inline constexpr std::array<GameCubeButtonItem, PAD_BUTTON_COUNT> kGameCubeButtons = {{
|
||||
{"a", "A", PAD_BUTTON_A},
|
||||
{"b", "B", PAD_BUTTON_B},
|
||||
{"x", "X", PAD_BUTTON_X},
|
||||
{"y", "Y", PAD_BUTTON_Y},
|
||||
{"start", "Start", PAD_BUTTON_START},
|
||||
{"z", "Z", PAD_TRIGGER_Z},
|
||||
{"l", "L", PAD_TRIGGER_L},
|
||||
{"r", "R", PAD_TRIGGER_R},
|
||||
{"up", "D-pad Up", PAD_BUTTON_UP},
|
||||
{"down", "D-pad Down", PAD_BUTTON_DOWN},
|
||||
{"left", "D-pad Left", PAD_BUTTON_LEFT},
|
||||
{"right", "D-pad Right", PAD_BUTTON_RIGHT},
|
||||
}};
|
||||
|
||||
// A physical button on the host pad. Names are positional (south/east/...)
|
||||
// rather than Xbox-labelled so one config reads the same on any hardware.
|
||||
struct NativeButtonItem {
|
||||
const char* configName;
|
||||
const char* label;
|
||||
uint32_t nativeButton;
|
||||
};
|
||||
|
||||
inline constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNativeButtons = {{
|
||||
{"unmapped", "Unmapped / analog trigger", PAD_NATIVE_BUTTON_INVALID},
|
||||
{"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
|
||||
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
|
||||
{"north", "North (Y / Triangle)", SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{"back", "Back / Select / Create", SDL_GAMEPAD_BUTTON_BACK},
|
||||
{"guide", "Guide / Home / PS", SDL_GAMEPAD_BUTTON_GUIDE},
|
||||
{"start", "Start / Options", SDL_GAMEPAD_BUTTON_START},
|
||||
{"left_stick", "Left stick click (L3)", SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||
{"right_stick", "Right stick click (R3)", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
||||
{"left_shoulder", "Left bumper (LB / L1)", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||
{"right_shoulder", "Right bumper (RB / R1)", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||
{"dpad_up", "D-pad Up", SDL_GAMEPAD_BUTTON_DPAD_UP},
|
||||
{"dpad_down", "D-pad Down", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||
{"dpad_left", "D-pad Left", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
|
||||
{"dpad_right", "D-pad Right", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||
{"misc1", "Misc 1 / Share / Mic", SDL_GAMEPAD_BUTTON_MISC1},
|
||||
{"right_paddle1", "Right paddle 1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
|
||||
{"left_paddle1", "Left paddle 1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
|
||||
{"right_paddle2", "Right paddle 2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
|
||||
{"left_paddle2", "Left paddle 2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
|
||||
{"touchpad", "Touchpad click", SDL_GAMEPAD_BUTTON_TOUCHPAD},
|
||||
{"misc2", "Misc 2", SDL_GAMEPAD_BUTTON_MISC2},
|
||||
{"misc3", "Misc 3 / GC L click", SDL_GAMEPAD_BUTTON_MISC3},
|
||||
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
|
||||
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
|
||||
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
|
||||
}};
|
||||
|
||||
inline std::string TrimToken(std::string_view token) {
|
||||
const size_t begin = token.find_first_not_of(" \t");
|
||||
if (begin == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
const size_t end = token.find_last_not_of(" \t");
|
||||
return std::string(token.substr(begin, end - begin + 1));
|
||||
}
|
||||
|
||||
inline const NativeButtonItem* FindNativeButton(std::string_view configName) {
|
||||
const std::string name = TrimToken(configName);
|
||||
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
|
||||
[&](const NativeButtonItem& item) { return name == item.configName; });
|
||||
return it == kNativeButtons.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
// Falls back to the "unmapped" entry so callers always have a label to draw.
|
||||
inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
|
||||
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
|
||||
[&](const NativeButtonItem& item) { return nativeButton == item.nativeButton; });
|
||||
return it == kNativeButtons.end() ? kNativeButtons.front() : *it;
|
||||
}
|
||||
|
||||
inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) {
|
||||
const std::string key = TrimToken(configKey);
|
||||
const auto it = std::find_if(kGameCubeButtons.begin(), kGameCubeButtons.end(),
|
||||
[&](const GameCubeButtonItem& item) { return key == item.configKey; });
|
||||
return it == kGameCubeButtons.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
// "up" or "up,a" -> the OR of those GC button bits. Unknown names are skipped so
|
||||
// a typo costs one button instead of the whole macro.
|
||||
inline uint16_t GameCubeMaskFromKeys(std::string_view keys) {
|
||||
uint16_t mask = 0;
|
||||
size_t begin = 0;
|
||||
while (begin <= keys.size()) {
|
||||
const size_t comma = keys.find(',', begin);
|
||||
const std::string_view token =
|
||||
keys.substr(begin, comma == std::string_view::npos ? std::string_view::npos : comma - begin);
|
||||
if (const GameCubeButtonItem* item = FindGameCubeButton(token)) {
|
||||
mask |= static_cast<uint16_t>(item->padButton);
|
||||
}
|
||||
if (comma == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
begin = comma + 1;
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
inline std::string GameCubeKeysFromMask(uint16_t mask) {
|
||||
std::string keys;
|
||||
for (const auto& item : kGameCubeButtons) {
|
||||
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!keys.empty()) {
|
||||
keys += ',';
|
||||
}
|
||||
keys += item.configKey;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
inline std::string GameCubeLabelsFromMask(uint16_t mask) {
|
||||
std::string labels;
|
||||
for (const auto& item : kGameCubeButtons) {
|
||||
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!labels.empty()) {
|
||||
labels += " + ";
|
||||
}
|
||||
labels += item.label;
|
||||
}
|
||||
return labels.empty() ? std::string("None") : labels;
|
||||
}
|
||||
|
||||
} // namespace ControllerNames
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_events.h>
|
||||
|
||||
// Press-to-bind setup for joysticks SDL either doesn't recognize as gamepads or
|
||||
// recognizes with a mapping that lacks the analog stick (e.g. raphnet adapters).
|
||||
// The wizard produces a standard SDL gamepad mapping, applies it live, and
|
||||
// persists it to gamecontrollerdb.txt in the user data directory.
|
||||
namespace controller_mapping_wizard {
|
||||
|
||||
void LoadPersistedMappings();
|
||||
void HandleSdlEvent(const SDL_Event& event);
|
||||
|
||||
// Lists devices that need setup inside the controller settings menu.
|
||||
void DrawSetupList();
|
||||
// Draws the wizard window when active; call once per overlay frame.
|
||||
void Draw();
|
||||
|
||||
bool IsActive();
|
||||
|
||||
} // namespace controller_mapping_wizard
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// Host implementation of Dolphin's /dev/dolphin Discord contract. The guest
|
||||
// sends only strings and big-endian integer fields; the IPC wire protocol is
|
||||
// owned here so translated game code never needs host SDK headers.
|
||||
namespace DiscordPresence {
|
||||
|
||||
struct Activity {
|
||||
std::string details;
|
||||
std::string state;
|
||||
std::string largeImageKey;
|
||||
std::string largeImageText;
|
||||
std::string smallImageKey;
|
||||
std::string smallImageText;
|
||||
int64_t startTimestamp = 0;
|
||||
int64_t endTimestamp = 0;
|
||||
uint32_t partySize = 0;
|
||||
uint32_t partyMax = 0;
|
||||
};
|
||||
|
||||
void Initialize(const std::string& basicClientId, const std::string& basicTitle);
|
||||
void SetClient(const std::string& clientId);
|
||||
void SetActivity(Activity activity);
|
||||
void Reset();
|
||||
void Shutdown();
|
||||
|
||||
} // namespace DiscordPresence
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
@@ -22,7 +23,7 @@
|
||||
// Forward declarations
|
||||
struct CpuContext;
|
||||
|
||||
// GuestFiberManager: each guest OSThread maps to a Windows Fiber. A scheduler fiber picks
|
||||
// GuestFiberManager: each guest OSThread maps to a host context. A scheduler context picks
|
||||
// which guest fiber runs; a real timer thread queues VI retraces at the VI cadence. Guest
|
||||
// threads only switch at explicit yield points (OSSleepThread, OSYieldThread, ...), matching
|
||||
// Wii cooperative semantics exactly.
|
||||
@@ -39,7 +40,7 @@ enum class ThreadState : uint32_t {
|
||||
|
||||
// Information about a guest fiber
|
||||
struct GuestFiber {
|
||||
void* fiber = nullptr; // Windows fiber handle
|
||||
void* fiber = nullptr; // Host context handle
|
||||
uint32_t entryPoint = 0; // Thread entry function
|
||||
uint32_t entryArg = 0; // Argument to entry function
|
||||
CpuContext cpuContext{}; // Saved CPU context for this fiber
|
||||
@@ -103,11 +104,18 @@ private:
|
||||
#else
|
||||
static void FiberProc(void* param);
|
||||
#endif
|
||||
|
||||
// Switch from whichever fiber is currently active straight to the scheduler fiber, without
|
||||
// the SwitchToThread bookkeeping (CPU context save/restore, s_currentGuestThread). Used for
|
||||
// in-fiber yields that aren't a real guest thread switch: waiting out the EGG::Thread::start
|
||||
// deferral loop, and returning control on natural thread exit.
|
||||
static void SwitchToScheduler();
|
||||
|
||||
// Internal state
|
||||
static std::mutex s_mutex;
|
||||
static std::unordered_map<uint32_t, GuestFiber> s_fibers;
|
||||
static std::vector<void*> s_fibersPendingDelete;
|
||||
// The scheduler's own host context. Its opaque handle is supplied by the
|
||||
// active HostContext backend, so one field serves every supported host.
|
||||
static void* s_schedulerFiber;
|
||||
static uint32_t s_currentGuestThread;
|
||||
static bool s_initialized;
|
||||
@@ -123,4 +131,3 @@ private:
|
||||
extern std::atomic<uint32_t> g_viRetracePendingCount;
|
||||
|
||||
} // namespace Fiber
|
||||
|
||||
|
||||
@@ -12,10 +12,33 @@
|
||||
namespace GuestFlat {
|
||||
|
||||
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
|
||||
// of a global. 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the
|
||||
// usual image/heap placement.
|
||||
// of a global.
|
||||
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
|
||||
inline constexpr size_t kGuestPageSize = 0x1000;
|
||||
#if defined(__x86_64__)
|
||||
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
|
||||
// placement.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull;
|
||||
#elif defined(__aarch64__) && defined(__APPLE__)
|
||||
// Keep this well above the low address ranges that Darwin's ASLR may use for
|
||||
// a PIE executable and its shared cache. Apple Silicon's user VA is wider
|
||||
// than Linux's 39-bit minimum, so this 512 GiB region is available while the
|
||||
// Linux AArch64 target retains its 64 GiB placement below.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0080'0000'0000ull;
|
||||
#elif defined(__aarch64__)
|
||||
// 16 TiB (this arch's x86_64 sibling value) is unreachable on any AArch64
|
||||
// kernel configured for 39-bit virtual addresses (512 GiB ceiling) - common on
|
||||
// older/embedded targets (e.g. this project's own tested Jetson/L4T board, kernel
|
||||
// 4.9). There mmap() silently ignores a hint above the ceiling and hands back an
|
||||
// address near the top of the real range instead, which this module's caller
|
||||
// then rejects as "already occupied". 64 GiB is reachable on every AArch64 VA
|
||||
// width in real use (39-bit minimum and up) and was confirmed via direct mmap
|
||||
// probing to sit far below where the PIE image, heap, shared libraries and
|
||||
// stack actually land (all clustered above ~340 GiB on a 39-bit/512 GiB system).
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0010'0000'0000ull;
|
||||
#else
|
||||
#error "guest_flat_memory.h has no fixed flat guest base chosen for this architecture"
|
||||
#endif
|
||||
|
||||
#define MKW_FLAT_GUEST_BASE (reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase))
|
||||
|
||||
@@ -42,6 +65,25 @@ struct FaultCounters {
|
||||
// True once the reservation exists and translated code may use the flat path.
|
||||
bool IsActive();
|
||||
|
||||
// True when a host VM page covers more than one 4 KiB Wii page. In that
|
||||
// configuration, guest-view page protection cannot safely represent per-Wii-
|
||||
// page MMIO, deferred-read, or executable-write state, so general translated
|
||||
// accesses must use the checked Memory::* path.
|
||||
// Windows user mode and x86-64 always use a 4 KiB base page, so those builds
|
||||
// fold this to a compile-time false: it appears in every flat access and must
|
||||
// not become a hot-path load. Only AArch64, where the page size is a kernel
|
||||
// configuration (4/16/64 KiB), has to probe it at runtime.
|
||||
#if defined(_WIN32) || defined(__x86_64__)
|
||||
#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1
|
||||
#endif
|
||||
|
||||
#if defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
inline constexpr bool RequiresCheckedAccess() noexcept { return false; }
|
||||
#else
|
||||
extern bool g_requiresCheckedAccess;
|
||||
inline bool RequiresCheckedAccess() noexcept { return g_requiresCheckedAccess; }
|
||||
#endif
|
||||
|
||||
// Reserves the 4 GiB space (once per process) and maps every requested region
|
||||
// into both views. Throws std::runtime_error with a precise diagnosis when the
|
||||
// reservation, the section objects or a view cannot be created - a silent
|
||||
@@ -73,8 +115,12 @@ FaultCounters Counters();
|
||||
void LogFaultSummary() noexcept;
|
||||
|
||||
// Returns true when the access violation was a guest-space fault this module
|
||||
// resolved; the caller must then resume execution. `exceptionPointers` is a
|
||||
// Windows EXCEPTION_POINTERS*.
|
||||
bool HandleAccessViolation(void* exceptionPointers) noexcept;
|
||||
// resolved; the caller must then resume execution. `faultAddress` is the raw
|
||||
// host pointer the access violation trapped on (Windows: ExceptionInformation[1];
|
||||
// POSIX: siginfo_t::si_addr) and `isWrite` is whether it was a write access
|
||||
// (Windows: ExceptionInformation[0] != 0; POSIX: derived from the ucontext).
|
||||
// The platform-specific handler that calls this is expected to have already
|
||||
// done that extraction - this function only ever works with the parsed pair.
|
||||
bool HandleAccessViolation(void* faultAddress, bool isWrite) noexcept;
|
||||
|
||||
} // namespace GuestFlat
|
||||
|
||||
@@ -20,14 +20,14 @@
|
||||
namespace DvdFstContract {
|
||||
|
||||
struct RegisteredFile {
|
||||
std::string hostPath;
|
||||
std::filesystem::path hostPath;
|
||||
std::string dvdPath;
|
||||
uint32_t size = 0;
|
||||
uint32_t discOffsetWords = 0;
|
||||
};
|
||||
|
||||
struct IndexedEntry {
|
||||
std::string hostPath;
|
||||
std::filesystem::path hostPath;
|
||||
std::string dvdPath;
|
||||
uint32_t size = 0;
|
||||
uint32_t discOffsetWords = 0;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
// HostContext is the deliberately small boundary between the guest scheduler
|
||||
// and the host's cooperative-context facility. Windows uses native Fibers and
|
||||
// Linux uses libco; macOS AArch64 uses the local assembly backend because it
|
||||
// must preserve Darwin's platform-reserved x18 register, which libco's AArch64
|
||||
// backend does not save. Its handles are only valid on the thread that
|
||||
// initialized the scheduler.
|
||||
namespace HostContext {
|
||||
|
||||
using Handle = void*;
|
||||
using Entry = void (*)(void*);
|
||||
|
||||
bool InitializeScheduler(Handle* scheduler);
|
||||
void ShutdownScheduler(Handle scheduler);
|
||||
|
||||
Handle Create(std::size_t stackSize, Entry entry, void* argument);
|
||||
void Destroy(Handle context);
|
||||
bool IsCurrent(Handle context);
|
||||
void Switch(Handle target);
|
||||
|
||||
} // namespace HostContext
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user