Files
wiicompiled/Launcher/Prepare-NativePrebuilt.ps1
T
patchzyy ec226e8348 init
2026-08-23 17:10:50 +02:00

413 lines
21 KiB
PowerShell

# Builds the redistributable precompiled aurora + third-party package: aurora (~43% of local build
# CPU time) and vendored Crypto++ are identical for every user, so this configures runtime/ like
# LocalBuild.ps1 (both dot-source NativeBuildFlags.ps1), builds just that closure, and harvests the
# archives plus a generated CMake description into launcher/artifacts/dependencies/native_prebuilt.
[CmdletBinding(PositionalBinding = $false)]
param(
[string]$PortableToolsDirectory = 'launcher/artifacts/portable-tools',
[string]$DependencySourceDirectory = 'launcher/artifacts/dependencies',
[string]$OutputDirectory,
[string]$StageDirectory = 'build/native-prebuilt-stage',
[switch]$KeepStage,
# Maintainer iteration aid: keep an already configured staging build so a
# re-harvest does not recompile aurora from scratch.
[switch]$ReuseStage,
[int]$Parallel = 0
)
$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))
}
function Normalize([string]$Path) { return ([IO.Path]::GetFullPath($Path)).Replace('\', '/') }
function StartsWithPath([string]$Candidate, [string]$Root) {
$rootSlash = $Root.TrimEnd('/') + '/'
return $Candidate.StartsWith($rootSlash, [StringComparison]::OrdinalIgnoreCase)
}
function RelativeTo([string]$Candidate, [string]$Root) {
$rootSlash = $Root.TrimEnd('/') + '/'
return $Candidate.Substring($rootSlash.Length)
}
$portableTools = Full $PortableToolsDirectory
$dependencies = Full $DependencySourceDirectory
$stage = Full $StageDirectory
if ([string]::IsNullOrWhiteSpace($OutputDirectory)) {
$OutputDirectory = Join-Path $dependencies 'native_prebuilt'
}
$package = Full $OutputDirectory
$toolchainBin = Join-Path $portableTools 'llvm-mingw\bin'
$cmake = Join-Path $portableTools 'CMake\bin\cmake.exe'
$ninja = Join-Path $portableTools 'Ninja\ninja.exe'
$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'
$clangBinary = Join-Path $toolchainBin 'clang-22.exe'
$runtimeSource = Join-Path $repoRoot 'runtime'
$auroraSource = Join-Path $repoRoot 'aurora-main'
Assert-File $cmake 'Portable CMake'
Assert-File $ninja 'Portable Ninja'
Assert-File $cc 'Portable C compiler'
Assert-File $cxx 'Portable C++ compiler'
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 }
# The package must never contain a stale mixture of two builds.
if (Test-Path -LiteralPath $package) { Remove-Item -LiteralPath $package -Recurse -Force }
[IO.Directory]::CreateDirectory($package) | Out-Null
[IO.Directory]::CreateDirectory((Join-Path $package 'lib')) | Out-Null
[IO.Directory]::CreateDirectory((Join-Path $package 'bin')) | Out-Null
[IO.Directory]::CreateDirectory((Join-Path $package 'include')) | Out-Null
# The staging build has to be configured without the package present, otherwise
# the runtime would consume the very package this script is producing.
if (-not $ReuseStage -and (Test-Path -LiteralPath $stage)) { Remove-Item -LiteralPath $stage -Recurse -Force }
$exportDirectory = Join-Path $stage 'export'
[IO.Directory]::CreateDirectory($exportDirectory) | Out-Null
$oldPath = $env:PATH
try {
$env:PATH = Get-MkwToolchainPath $portableTools
$configure = Get-MkwNativeConfigureArguments -SourceDirectory $runtimeSource -BuildDirectory $stage `
-Ninja $ninja -CCompiler $cc -CxxCompiler $cxx -ResourceCompiler $windres `
-DependenciesDirectory $dependencies `
-AdditionalArguments @("-DMKW_NATIVE_PREBUILT_EXPORT_DIR=$(ConvertTo-MkwCMakePath $exportDirectory)")
Invoke-Checked $cmake $configure 'Configuring the aurora/third-party staging build' -LogPrefix 'MKWNP'
# 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.
Invoke-Checked $cmake @('--build', $stage, '--target', 'mkw_np_probe', '--parallel', "$Parallel") `
'Building the aurora/third-party closure' -LogPrefix 'MKWNP'
} finally {
$env:PATH = $oldPath
}
# ---------------------------------------------------------------------------
# Read the description CMake wrote, plus the resolved command lines Ninja holds.
# ---------------------------------------------------------------------------
$metaPath = Join-Path $exportDirectory 'meta.txt'
Assert-File $metaPath 'Native prebuilt export metadata'
$meta = @{}
foreach ($line in Get-Content -LiteralPath $metaPath) {
if ($line -match '^([a-z0-9_]+)=(.*)$') { $meta[$Matches[1]] = $Matches[2] }
}
$buildNinja = Join-Path $stage 'build.ninja'
Assert-File $buildNinja 'Generated build.ninja'
$ninjaLines = [IO.File]::ReadAllLines($buildNinja)
function Get-NinjaStatementVariables([string[]]$Lines, [string]$RulePrefix) {
$variables = $null
for ($i = 0; $i -lt $Lines.Length; $i++) {
$line = $Lines[$i]
if (-not $line.StartsWith('build ')) { continue }
$colon = $line.IndexOf(': ')
if ($colon -lt 0) { continue }
$rule = $line.Substring($colon + 2)
$space = $rule.IndexOf(' ')
if ($space -ge 0) { $rule = $rule.Substring(0, $space) }
if ($rule -ne $RulePrefix) { continue }
$variables = [ordered]@{}
for ($j = $i + 1; $j -lt $Lines.Length; $j++) {
$entry = $Lines[$j]
if ($entry -notmatch '^ ([A-Z_]+) = ?(.*)$') { break }
$variables[$Matches[1]] = $Matches[2]
}
break
}
if ($null -eq $variables) { throw "No Ninja statement used rule $RulePrefix." }
return $variables
}
function Get-NinjaValue($Variables, [string]$Name) {
if ($Variables.Contains($Name)) { return [string]$Variables[$Name] }
return ''
}
function Split-NinjaList([string]$Value) {
if ([string]::IsNullOrWhiteSpace($Value)) { return , [string[]]@() }
return , [string[]]@($Value -split '\s+' | Where-Object { $_ })
}
function Subtract-Ordered([string[]]$All, [string[]]$Remove) {
if ($null -eq $All) { return [string[]]@() }
$result = [Collections.Generic.List[string]]::new()
$pending = [Collections.Generic.List[string]]::new()
if ($null -ne $Remove) { foreach ($item in $Remove) { $pending.Add($item) } }
foreach ($item in $All) {
$index = $pending.IndexOf($item)
if ($index -ge 0) { $pending.RemoveAt($index); continue }
$result.Add($item)
}
return $result.ToArray()
}
$probeCompile = Get-NinjaStatementVariables $ninjaLines 'CXX_COMPILER__mkw_np_probe_unscanned_Release'
$controlCompile = Get-NinjaStatementVariables $ninjaLines 'CXX_COMPILER__mkw_np_probe_control_unscanned_Release'
$probeLink = Get-NinjaStatementVariables $ninjaLines 'CXX_EXECUTABLE_LINKER__mkw_np_probe_Release'
$controlLink = Get-NinjaStatementVariables $ninjaLines 'CXX_EXECUTABLE_LINKER__mkw_np_probe_control_Release'
$includeArguments = Subtract-Ordered (Split-NinjaList (Get-NinjaValue $probeCompile 'INCLUDES')) `
(Split-NinjaList (Get-NinjaValue $controlCompile 'INCLUDES'))
$defineArguments = Subtract-Ordered (Split-NinjaList (Get-NinjaValue $probeCompile 'DEFINES')) `
(Split-NinjaList (Get-NinjaValue $controlCompile 'DEFINES'))
$compileOptions = Subtract-Ordered (Split-NinjaList (Get-NinjaValue $probeCompile 'FLAGS')) `
(Split-NinjaList (Get-NinjaValue $controlCompile 'FLAGS'))
$linkItems = Subtract-Ordered (Split-NinjaList (Get-NinjaValue $probeLink 'LINK_LIBRARIES')) `
(Split-NinjaList (Get-NinjaValue $controlLink 'LINK_LIBRARIES'))
if ($includeArguments.Count -eq 0) { throw 'The aurora compile interface is empty; the probe did not link aurora.' }
if ($linkItems.Count -eq 0) { throw 'The aurora link interface is empty; the probe did not link aurora.' }
# ---------------------------------------------------------------------------
# Harvest the built libraries.
# ---------------------------------------------------------------------------
$binaryRoot = Normalize $meta['binary_dir']
$auroraRoot = Normalize $meta['aurora_dir']
$runtimeRoot = Normalize $meta['runtime_dir']
$dependencyRoot = Normalize $dependencies
$targets = @{}
foreach ($line in Get-Content -LiteralPath (Join-Path $exportDirectory 'targets.txt')) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }
$parts = $line -split '\|'
if ($parts.Count -ne 4) { throw "Malformed exported target line: $line" }
$targets[$parts[0]] = [pscustomobject]@{
Name = $parts[0]; Type = $parts[1]
File = Normalize $parts[2]; LinkerFile = Normalize $parts[3]
}
}
$dawnLinkerFile = ''
$dawnRuntimeFile = ''
foreach ($line in Get-Content -LiteralPath (Join-Path $exportDirectory 'dawn.txt')) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }
$parts = $line -split '\|'
$dawnRuntimeFile = Normalize $parts[1]
$dawnLinkerFile = Normalize $parts[2]
}
# linker-file path -> package reference, built only from libraries that were
# actually produced by this build.
$linkerFileToReference = @{}
$sharedImports = [Collections.Generic.List[object]]::new()
$copiedFiles = [Collections.Generic.List[string]]::new()
function Copy-Harvested([string]$SourcePath, [string]$SubDirectory) {
$name = [IO.Path]::GetFileName($SourcePath)
$destination = Join-Path (Join-Path $package $SubDirectory) $name
if (Test-Path -LiteralPath $destination) {
throw "Two harvested files collide on the name ${name}: $SourcePath"
}
Copy-Item -LiteralPath $SourcePath -Destination $destination
$copiedFiles.Add("$SubDirectory/$name")
return "$SubDirectory/$name"
}
foreach ($name in ($targets.Keys | Sort-Object)) {
$target = $targets[$name]
if (-not (Test-Path -LiteralPath $target.LinkerFile -PathType Leaf)) {
# Not part of the closure mkw_np_probe pulled in; deliberately not shipped.
continue
}
$relativeLinker = Copy-Harvested $target.LinkerFile 'lib'
if ($target.Type -eq 'SHARED_LIBRARY') {
if (-not (Test-Path -LiteralPath $target.File -PathType Leaf)) {
throw "Shared library $name has no runtime file: $($target.File)"
}
$relativeRuntime = Copy-Harvested $target.File 'bin'
$sharedImports.Add([pscustomobject]@{
Target = "mkw_np::$name"; Runtime = $relativeRuntime; Implib = $relativeLinker })
$linkerFileToReference[$target.LinkerFile] = "mkw_np::$name"
} else {
$linkerFileToReference[$target.LinkerFile] = "`@PKG`@/$relativeLinker"
}
}
if ($sharedImports.Count -eq 0) { throw 'No shared libraries were harvested; the SDL/zlib/libpng DLLs would be missing.' }
if ($dawnLinkerFile) { $linkerFileToReference[$dawnLinkerFile] = 'dawn::webgpu_dawn' }
# ---------------------------------------------------------------------------
# Rewrite the compile/link interface into workspace-relative tokens.
# ---------------------------------------------------------------------------
$generatedIncludeIndex = 0
function ConvertTo-Token([string]$AbsolutePath, [string]$Kind) {
$path = Normalize $AbsolutePath
if (StartsWithPath $path $auroraRoot) { return "`@AURORA`@/$(RelativeTo $path $auroraRoot)" }
if (StartsWithPath $path $runtimeRoot) { return "`@RUNTIME`@/$(RelativeTo $path $runtimeRoot)" }
if (StartsWithPath $path $dependencyRoot) { return "`@DEPS`@/$(RelativeTo $path $dependencyRoot)" }
if (StartsWithPath $path $binaryRoot) {
if ($Kind -ne 'include') { throw "Unhandled build-tree artifact: $path" }
# A generated header directory (SDL_revision.h and friends). The source
# trees ship, but these do not exist until something configures them, so
# they travel inside the package.
$script:generatedIncludeIndex++
$name = 'generated{0:d2}' -f $script:generatedIncludeIndex
$destination = Join-Path (Join-Path $package 'include') $name
[IO.Directory]::CreateDirectory($destination) | Out-Null
Copy-Item -Path (Join-Path $path '*') -Destination $destination -Recurse -Force
foreach ($file in Get-ChildItem -LiteralPath $destination -Recurse -File) {
$copiedFiles.Add(('include/{0}/{1}' -f $name,
$file.FullName.Substring($destination.Length + 1).Replace('\', '/')))
}
return "`@PKG`@/include/$name"
}
throw "Path escapes every shippable root ($Kind): $path"
}
# Ninja splits "-isystem <path>" into two list entries (the vendored Crypto++
# headers are declared SYSTEM, so they arrive that way), while plain aurora
# includes stay glued to -I. Only the directory itself travels in the package;
# the consumer re-emits it through INTERFACE_INCLUDE_DIRECTORIES and CMake
# chooses the flag form per usage context.
$packageIncludeDirectories = @()
for ($i = 0; $i -lt $includeArguments.Count; $i++) {
$entry = $includeArguments[$i]
if ($entry.StartsWith('-I')) {
$packageIncludeDirectories += (ConvertTo-Token $entry.Substring(2) 'include')
} elseif ($entry -eq '-isystem' -and ($i + 1) -lt $includeArguments.Count) {
$i++
$packageIncludeDirectories += (ConvertTo-Token $includeArguments[$i] 'include')
} elseif ($entry.StartsWith('-isystem')) {
$packageIncludeDirectories += (ConvertTo-Token $entry.Substring('-isystem'.Length) 'include')
} else {
throw "Unexpected include argument: $entry"
}
}
$packageDefinitions = @($defineArguments | ForEach-Object {
if (-not $_.StartsWith('-D')) { throw "Unexpected define argument: $_" }
# Ninja carries the Windows command-line escaping; CMake re-applies it.
$_.Substring(2).Replace('\"', '"')
})
$packageLinkItems = @($linkItems | ForEach-Object {
$item = $_
if ($item.StartsWith('-')) { return $item }
$absolute = if ([IO.Path]::IsPathRooted($item)) { Normalize $item } else { Normalize (Join-Path $stage $item) }
if ($linkerFileToReference.ContainsKey($absolute)) { return $linkerFileToReference[$absolute] }
if (StartsWithPath $absolute $dependencyRoot) { return "`@DEPS`@/$(RelativeTo $absolute $dependencyRoot)" }
throw "Link item is neither a system library nor a harvested archive: $item"
})
# 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.
$packageCompileOptions = @($compileOptions | ForEach-Object { "`$<`$<COMPILE_LANGUAGE:CXX>:$_>" })
# ---------------------------------------------------------------------------
# Emit the consumer-facing CMake description.
# ---------------------------------------------------------------------------
function Format-CMakeBlock([string]$Name, [string[]]$Items) {
if ($null -eq $Items -or $Items.Count -eq 0) { return "set($Name `"`")`n" }
$body = ($Items | ForEach-Object { ' "' + $_.Replace('\', '/').Replace('"', '\"') + '"' }) -join "`n"
return "set($Name`n$body)`n"
}
$generated = [Collections.Generic.List[string]]::new()
$generated.Add('# Generated by launcher/Prepare-NativePrebuilt.ps1 - do not edit.')
$generated.Add('#')
$generated.Add('# Describes the precompiled aurora + third-party archives that replace a')
$generated.Add('# from-source aurora-main build. Every path is a token resolved against the')
$generated.Add('# consuming workspace, because the source trees still ship next to this')
$generated.Add('# package and the runtime compiles against their headers.')
$generated.Add('')
$generated.Add('if(NOT MKW_NP_PACKAGE_DIR OR NOT MKW_NP_AURORA_DIR OR NOT MKW_NP_RUNTIME_DIR OR NOT MKW_NP_DEPS_DIR)')
$generated.Add(' message(FATAL_ERROR "native_prebuilt.cmake must be included by runtime/cmake/NativePrebuilt.cmake")')
$generated.Add('endif()')
$generated.Add('')
$generated.Add((Format-CMakeBlock 'MKW_NP_INCLUDE_DIRECTORIES' $packageIncludeDirectories).TrimEnd())
$generated.Add((Format-CMakeBlock 'MKW_NP_COMPILE_DEFINITIONS' $packageDefinitions).TrimEnd())
$generated.Add((Format-CMakeBlock 'MKW_NP_COMPILE_OPTIONS' $packageCompileOptions).TrimEnd())
$generated.Add((Format-CMakeBlock 'MKW_NP_LINK_LIBRARIES' $packageLinkItems).TrimEnd())
$generated.Add((Format-CMakeBlock 'MKW_NP_AURORA_TARGETS' @('aurora::gx','aurora::pad','aurora::si','aurora::vi','aurora::mtx')).TrimEnd())
$generated.Add('')
$generated.Add("set(MKW_NP_DAWN_CONFIG_DIR `"$(ConvertTo-Token $meta['dawn_config_dir'] 'dawn')`")")
$generated.Add("set(MKW_NP_DAWN_VERSION `"$($meta['aurora_dawn_version'])`")")
$generated.Add('')
$generated.Add('# Shared third-party libraries keep imported SHARED targets so that')
$generated.Add('# $<TARGET_RUNTIME_DLLS> still copies them next to the game executable.')
foreach ($import in $sharedImports) {
$generated.Add("add_library($($import.Target) SHARED IMPORTED GLOBAL)")
$generated.Add("set_target_properties($($import.Target) PROPERTIES")
$generated.Add(" IMPORTED_LOCATION `"`${MKW_NP_PACKAGE_DIR}/$($import.Runtime)`"")
$generated.Add(" IMPORTED_IMPLIB `"`${MKW_NP_PACKAGE_DIR}/$($import.Implib)`")")
}
$generatedText = ($generated -join "`r`n") + "`r`n"
$generatedPath = Join-Path $package 'native_prebuilt.cmake'
[IO.File]::WriteAllText($generatedPath, $generatedText, (New-Object Text.UTF8Encoding($false)))
# --- Provenance ---
# The pinned Dawn tree is what this harvest compiled against; without its digest the
# consumer-side ABI check would silently pass on a mismatched Dawn.
$dawnRuntimeHash = Get-MkwDawnRuntimeSha256 $dependencies
if (-not $dawnRuntimeHash) {
throw "The pinned Dawn runtime is missing: $(Join-Path $dependencies 'dawn_prebuilt\bin\webgpu_dawn.dll')"
}
$auroraSourceFingerprint = Get-MkwAuroraSourceFingerprint $auroraSource
if (-not $auroraSourceFingerprint) {
throw "The aurora source tree could not be fingerprinted: $auroraSource"
}
# The harvested Crypto++ archive is consumed against this tree's headers, so it
# is recorded for the same reason as the aurora fingerprint above.
$thirdPartyDirectory = Join-Path $runtimeSource 'third_party'
$thirdPartySourceFingerprint = Get-MkwThirdPartySourceFingerprint $thirdPartyDirectory
if (-not $thirdPartySourceFingerprint) {
throw "The vendored third-party tree could not be fingerprinted: $thirdPartyDirectory"
}
$contents = @(Get-ChildItem -LiteralPath $package -Recurse -File | ForEach-Object {
[pscustomobject][ordered]@{
Path = $_.FullName.Substring($package.Length + 1).Replace('\', '/')
Bytes = $_.Length
Sha256 = Get-MkwFileSha256 $_.FullName
}
} | Sort-Object Path)
$provenance = [ordered]@{
SchemaVersion = 1
BuiltUtc = [DateTime]::UtcNow.ToString('O')
# A precompiled archive is only interchangeable with locally compiled objects
# when the compiler and the flag set are bit-for-bit the ones the user's
# machine will use. LocalBuild.ps1 re-checks both before consuming this.
CompilerSha256 = Get-MkwFileSha256 $clangBinary
CompilerVersion = $meta['cxx_compiler_version']
FlagFingerprint = Get-MkwNativeFlagFingerprint
DawnVersion = $meta['aurora_dawn_version']
# The version tag is not a content identity: the upstream prebuilt asset is
# mutable, so record the digest of the Dawn runtime these archives were
# actually compiled and linked against. Consumers re-check it.
DawnRuntimeSha256 = $dawnRuntimeHash
# libaurora_*.a is compiled from this tree, while the runtime that calls into
# it is compiled on the user's machine. Record what was harvested so both the
# release build and the local build can refuse a package aurora has outgrown.
AuroraSourceFingerprint = $auroraSourceFingerprint
ThirdPartySourceFingerprint = $thirdPartySourceFingerprint
Sdl3Target = $meta['aurora_sdl3_target']
HarvestedLibraryCount = @($linkerFileToReference.Keys).Count
Contents = $contents
}
$provenance | ConvertTo-Json -Depth 6 |
Set-Content -LiteralPath (Join-Path $package 'provenance.json') -Encoding UTF8
if (-not $KeepStage) { Remove-Item -LiteralPath $stage -Recurse -Force }
$totalBytes = ($contents | Measure-Object -Property Bytes -Sum).Sum
Write-Host ''
Write-Host "Native prebuilt package: $package"
Write-Host (" files: {0}; size: {1:n1} MiB" -f $contents.Count, ($totalBytes / 1MB))
Write-Host (" archives: {0}; shared imports: {1}" -f (@(Get-ChildItem (Join-Path $package 'lib') -File).Count), $sharedImports.Count)
Write-Host (" flag fingerprint: {0}" -f $provenance.FlagFingerprint)