hypertwist/scripts/Invoke-HyperTwistClassicCubePackage.ps1
2026-07-23 17:23:24 +00:00

815 lines
29 KiB
PowerShell

param(
[string]$ProjectRoot = 'C:\HyperTwist',
[string]$ArchiveDirectory = 'C:\HyperTwist\packaged\classic-cube',
[ValidateSet('Development', 'Shipping')]
[string]$Configuration = 'Development',
[string]$CookMap = '/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining',
[string[]]$AdditionalCookMaps = @('/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining'),
[string[]]$SmokeMaps = @(),
[string[]]$AdditionalCookerOptions = @(
'-DisablePlugins=MovieRenderPipeline',
'-SkipCookingEditorContent'
),
[bool]$HeadlessSmoke = $true,
[string]$ValidationReportPath = '',
[string]$SolverTablePath = '',
[Int64]$ExpectedSolverTableSizeBytes = 676207080,
[string]$ExpectedSolverTableSha256 = 'dc6de3d909a37afe2ba7f1f978543a13eae215bd20d954d66813524a8ca20034',
[string]$BrowserShellSourceDirectory = '',
[string[]]$BootstrapLaunchArguments = @(
'-notraceserver',
'-traceautostart=0'
),
[switch]$CleanArchive,
[switch]$SkipBuild,
[switch]$SkipBrowserShellBuild,
[switch]$SkipLaunch
)
$ErrorActionPreference = 'Stop'
$RunUatPath = 'C:\Program Files\Epic Games\UE_5.7\Engine\Build\BatchFiles\RunUAT.bat'
$UProjectPath = Join-Path $ProjectRoot 'UnrealHyperTwist\UnrealHyperTwist.uproject'
$LaunchScriptPath = Join-Path $ProjectRoot 'scripts\Launch-HyperTwistClassicCubePackage.ps1'
$BootstrapLaunchArgumentsScriptPath = Join-Path $ProjectRoot 'scripts\Set-HyperTwistBootstrapLaunchArguments.ps1'
$GameTargetReceiptPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Binaries\Win64\UnrealHyperTwist.target'
$UnrealBuildToolSavedPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Saved\UnrealBuildTool'
$CookMaps = @($CookMap) + $AdditionalCookMaps | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
$ResolvedSmokeMaps = @(
if ($SmokeMaps.Count -gt 0)
{
$SmokeMaps
}
else
{
$CookMaps
}
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
$ResolvedAdditionalCookerOptions = @($AdditionalCookerOptions) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
$ExpectedClassicCubeMaterialPaths = @(
(Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\M_HT_ClassicCubeFaceMaster.uasset'),
(Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Up.uasset'),
(Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Down.uasset'),
(Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Front.uasset'),
(Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Back.uasset'),
(Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Left.uasset'),
(Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Right.uasset'),
(Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Internal.uasset')
)
$ValidationRootPath = Join-Path $ArchiveDirectory 'validation'
$SmokeReportDirectory = Join-Path $ValidationRootPath 'smoke'
$BootstrapLaunchArgumentsReportPath = Join-Path $ValidationRootPath 'bootstrap-launch-arguments-report.json'
if ([string]::IsNullOrWhiteSpace($SolverTablePath))
{
$SolverTablePath = Join-Path $ProjectRoot 'UnrealHyperTwist\Saved\twophase-ht.tbl'
}
if ([string]::IsNullOrWhiteSpace($BrowserShellSourceDirectory))
{
$BrowserShellSourceDirectory = Join-Path $ProjectRoot 'Content\Browser'
}
function Write-Utf8JsonFile {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[object]$Value
)
$ParentPath = Split-Path -Parent $Path
if (-not [string]::IsNullOrWhiteSpace($ParentPath))
{
New-Item -ItemType Directory -Force -Path $ParentPath | Out-Null
}
$Json = $Value | ConvertTo-Json -Depth 10
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($Path, $Json, $Utf8NoBom)
}
function Convert-PathToken {
param(
[string]$Value
)
if ([string]::IsNullOrWhiteSpace($Value))
{
return 'unknown'
}
return ($Value -replace '[\\/:*?"<>| ]', '_')
}
function Read-JsonFile {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
}
function Get-BrowserShellEvidence {
param(
[Parameter(Mandatory = $true)]
[string]$RootPath
)
$RequiredRelativePaths = @(
'index.html',
'src\browser-runtime-bootstrap.js',
'src\browser-spatial-runtime-fallback.js',
'dist\browser-spatial-runtime.js'
)
$RequiredFiles = @()
foreach ($RelativePath in $RequiredRelativePaths)
{
$AbsolutePath = Join-Path $RootPath $RelativePath
if (-not (Test-Path -LiteralPath $AbsolutePath -PathType Leaf))
{
throw "Required browser-shell artifact '$RelativePath' was not found beneath '$RootPath'."
}
$Item = Get-Item -LiteralPath $AbsolutePath
$RequiredFiles += [pscustomobject][ordered]@{
relativePath = $RelativePath.Replace('\', '/')
sizeBytes = [Int64]$Item.Length
sha256 = (Get-FileHash -LiteralPath $AbsolutePath -Algorithm SHA256).Hash.ToLowerInvariant()
}
}
$AllFiles = @(
Get-ChildItem -LiteralPath $RootPath -Recurse -File |
Where-Object {
$_.FullName -notmatch '[\\/]node_modules[\\/]' `
-and $_.Extension -ne '.map'
}
)
return [pscustomobject][ordered]@{
rootPath = (Get-Item -LiteralPath $RootPath).FullName
deployableFileCount = $AllFiles.Count
deployableSizeBytes = [Int64](($AllFiles | Measure-Object -Property Length -Sum).Sum)
requiredFiles = @($RequiredFiles)
}
}
function Invoke-BrowserShellBuild {
param(
[Parameter(Mandatory = $true)]
[string]$SourceDirectory,
[Parameter(Mandatory = $true)]
[bool]$SkipBuild
)
$PackageManifestPath = Join-Path $SourceDirectory 'package.json'
if (-not (Test-Path -LiteralPath $PackageManifestPath -PathType Leaf))
{
throw "Browser-shell package manifest was not found at '$PackageManifestPath'."
}
if (-not $SkipBuild)
{
$NpmCommand = Get-Command 'npm.cmd' -ErrorAction SilentlyContinue
if ($null -eq $NpmCommand)
{
throw 'npm.cmd is required to verify and build the packaged HyperTwist browser shell.'
}
Write-Host "Verifying HyperTwist browser shell beneath '$SourceDirectory'..."
& $NpmCommand.Source --prefix $SourceDirectory run verify:shell
if ($LASTEXITCODE -ne 0)
{
throw "Browser-shell verification failed with exit code $LASTEXITCODE."
}
Write-Host "Restoring the locked HyperTwist browser-shell toolchain beneath '$SourceDirectory'..."
& $NpmCommand.Source --prefix $SourceDirectory ci --no-audit --no-fund
if ($LASTEXITCODE -ne 0)
{
throw "Browser-shell locked dependency restore failed with exit code $LASTEXITCODE."
}
Write-Host "Building HyperTwist browser shell beneath '$SourceDirectory'..."
& $NpmCommand.Source --prefix $SourceDirectory run build
if ($LASTEXITCODE -ne 0)
{
throw "Browser-shell production build failed with exit code $LASTEXITCODE."
}
}
return Get-BrowserShellEvidence -RootPath $SourceDirectory
}
function Copy-BrowserShellToPackage {
param(
[Parameter(Mandatory = $true)]
[string]$SourceDirectory,
[Parameter(Mandatory = $true)]
[string]$PackagedExecutablePath
)
$ExecutableDirectory = Split-Path -Parent $PackagedExecutablePath
$DestinationDirectory = Join-Path $ExecutableDirectory 'Content\Browser'
if (Test-Path -LiteralPath $DestinationDirectory)
{
Remove-Item -LiteralPath $DestinationDirectory -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $DestinationDirectory | Out-Null
New-Item -ItemType Directory -Force -Path (Join-Path $DestinationDirectory 'src') | Out-Null
Copy-Item `
-LiteralPath (Join-Path $SourceDirectory 'index.html') `
-Destination (Join-Path $DestinationDirectory 'index.html') `
-Force
Copy-Item `
-LiteralPath (Join-Path $SourceDirectory 'src\browser-runtime-bootstrap.js') `
-Destination (Join-Path $DestinationDirectory 'src\browser-runtime-bootstrap.js') `
-Force
Copy-Item `
-LiteralPath (Join-Path $SourceDirectory 'src\browser-spatial-runtime-fallback.js') `
-Destination (Join-Path $DestinationDirectory 'src\browser-spatial-runtime-fallback.js') `
-Force
$DistSourceDirectory = Join-Path $SourceDirectory 'dist'
Get-ChildItem -LiteralPath $DistSourceDirectory -Recurse -File |
Where-Object { $_.Extension -ne '.map' } |
ForEach-Object {
$RelativePath = $_.FullName.Substring($DistSourceDirectory.Length).TrimStart('\', '/')
$DestinationPath = Join-Path (Join-Path $DestinationDirectory 'dist') $RelativePath
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $DestinationPath) | Out-Null
Copy-Item -LiteralPath $_.FullName -Destination $DestinationPath -Force
}
return Get-BrowserShellEvidence -RootPath $DestinationDirectory
}
function Get-BootstrapRuntimeArgumentEvidence {
param(
[string]$RuntimeLogPath,
[Parameter(Mandatory = $true)]
[string]$RuntimeDiagnosticsPath,
[string[]]$ObservedCommandLines = @(),
[object[]]$TraceControlListeningEndpoints = @(),
[Parameter(Mandatory = $true)]
[string[]]$ExpectedArguments
)
$RuntimeLogExists = -not [string]::IsNullOrWhiteSpace($RuntimeLogPath) `
-and (Test-Path -LiteralPath $RuntimeLogPath -PathType Leaf)
$RuntimeDiagnosticsExists = -not [string]::IsNullOrWhiteSpace($RuntimeDiagnosticsPath) `
-and (Test-Path -LiteralPath $RuntimeDiagnosticsPath -PathType Leaf)
if (-not $RuntimeDiagnosticsExists)
{
return [pscustomobject][ordered]@{
runtimeLogPath = $RuntimeLogPath
runtimeLogExists = $false
runtimeDiagnosticsPath = $RuntimeDiagnosticsPath
runtimeDiagnosticsExists = $false
expectedArguments = @($ExpectedArguments)
matchedArguments = @()
missingArguments = @($ExpectedArguments)
traceControlListenerLines = @()
traceControlListeningEndpoints = @($TraceControlListeningEndpoints)
result = 'failed'
error = 'The direct HyperTwist diagnostics file was not found.'
}
}
$RuntimeLogLines = [string[]]@(
if ($RuntimeLogExists)
{
Get-Content -LiteralPath $RuntimeLogPath |
ForEach-Object { $_.ToString() }
}
)
$RuntimeDiagnosticsLines = [string[]]@(
if ($RuntimeDiagnosticsExists)
{
Get-Content -LiteralPath $RuntimeDiagnosticsPath |
ForEach-Object { $_.ToString() }
}
)
$RuntimeEvidenceLines = [string[]]@(
@($RuntimeLogLines) +
@($RuntimeDiagnosticsLines) +
@($ObservedCommandLines)
)
$RuntimeEvidenceText = [string]::Join([Environment]::NewLine, $RuntimeEvidenceLines)
$MatchedArguments = @(
$ExpectedArguments |
Where-Object {
$RuntimeEvidenceText.IndexOf(
$_,
[System.StringComparison]::OrdinalIgnoreCase
) -ge 0
}
)
$MissingArguments = @(
$ExpectedArguments |
Where-Object { $MatchedArguments -notcontains $_ }
)
$TraceControlListenerLines = [string[]]@(
@($RuntimeLogLines + $RuntimeDiagnosticsLines) |
Where-Object {
$_.IndexOf(
'Control listening on port',
[System.StringComparison]::OrdinalIgnoreCase
) -ge 0
}
)
$Passed = $MissingArguments.Count -eq 0 `
-and $TraceControlListenerLines.Count -eq 0 `
-and $TraceControlListeningEndpoints.Count -eq 0
return [pscustomobject][ordered]@{
runtimeLogPath = $RuntimeLogPath
runtimeLogExists = $RuntimeLogExists
runtimeDiagnosticsPath = $RuntimeDiagnosticsPath
runtimeDiagnosticsExists = $RuntimeDiagnosticsExists
observedCommandLines = @($ObservedCommandLines)
expectedArguments = @($ExpectedArguments)
matchedArguments = @($MatchedArguments)
missingArguments = @($MissingArguments)
traceControlListenerLines = @($TraceControlListenerLines)
traceControlListeningEndpoints = @($TraceControlListeningEndpoints)
result = if ($Passed) { 'passed' } else { 'failed' }
error = if ($Passed)
{
$null
}
elseif ($MissingArguments.Count -gt 0)
{
"Runtime evidence omitted bootstrap arguments: $($MissingArguments -join ', ')"
}
else
{
'Runtime opened an Unreal trace-control listener.'
}
}
}
function Get-ValidatedSolverTableEvidence {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[Int64]$ExpectedSizeBytes,
[Parameter(Mandatory = $true)]
[string]$ExpectedSha256
)
if (-not (Test-Path -LiteralPath $Path -PathType Leaf))
{
throw "Required two-phase solver table was not found at '$Path'."
}
$Item = Get-Item -LiteralPath $Path
if ($Item.Length -ne $ExpectedSizeBytes)
{
throw "Two-phase solver table '$Path' was $($Item.Length) bytes; expected exactly $ExpectedSizeBytes bytes."
}
$ActualSha256 = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
$NormalizedExpectedSha256 = $ExpectedSha256.Trim().ToLowerInvariant()
if ($ActualSha256 -ne $NormalizedExpectedSha256)
{
throw "Two-phase solver table '$Path' had SHA-256 '$ActualSha256'; expected '$NormalizedExpectedSha256'."
}
return [pscustomobject][ordered]@{
path = $Item.FullName
sizeBytes = [Int64]$Item.Length
sha256 = $ActualSha256
}
}
function Copy-ValidatedSolverTableToPackage {
param(
[Parameter(Mandatory = $true)]
[string]$SourcePath,
[Parameter(Mandatory = $true)]
[string]$PackagedExecutablePath,
[Parameter(Mandatory = $true)]
[Int64]$ExpectedSizeBytes,
[Parameter(Mandatory = $true)]
[string]$ExpectedSha256
)
$ExecutableDirectory = Split-Path -Parent $PackagedExecutablePath
$DestinationPath = Join-Path $ExecutableDirectory 'UnrealHyperTwist\Saved\twophase-ht.tbl'
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $DestinationPath) | Out-Null
Copy-Item -LiteralPath $SourcePath -Destination $DestinationPath -Force
return Get-ValidatedSolverTableEvidence `
-Path $DestinationPath `
-ExpectedSizeBytes $ExpectedSizeBytes `
-ExpectedSha256 $ExpectedSha256
}
function Resolve-PackagedExecutablePath {
param(
[string]$PackageRoot
)
$CandidateExecutablePaths = @(
(Join-Path $PackageRoot 'Windows\UnrealHyperTwist.exe'),
(Join-Path $PackageRoot 'WindowsNoEditor\UnrealHyperTwist.exe'),
(Join-Path $PackageRoot 'UnrealHyperTwist.exe')
)
return $CandidateExecutablePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
}
function Convert-GameMapPathToContentPath {
param(
[string]$RootPath,
[string]$GameMapPath
)
if (-not $GameMapPath.StartsWith('/Game/'))
{
throw "Cook map '$GameMapPath' is not a supported /Game asset path."
}
$RelativeMapPath = $GameMapPath.Substring('/Game/'.Length).Replace('/', '\')
return Join-Path $RootPath ("UnrealHyperTwist\Content\{0}.umap" -f $RelativeMapPath)
}
function Assert-CookMapExists {
param(
[string]$RootPath,
[string]$GameMapPath
)
$ExpectedMapPath = Convert-GameMapPathToContentPath -RootPath $RootPath -GameMapPath $GameMapPath
if (-not (Test-Path $ExpectedMapPath))
{
throw "Expected cook map '$GameMapPath' was not found at '$ExpectedMapPath'. Run 'scripts\Invoke-HyperTwistClassicCubeMapAuthoring.ps1' first."
}
}
function Resolve-ExistingPath {
param(
[string[]]$CandidatePaths
)
return $CandidatePaths | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
}
function Get-MostRecentFileItem {
param(
[string[]]$Paths
)
$MostRecentItem = $null
foreach ($Path in $Paths)
{
if (-not (Test-Path -LiteralPath $Path))
{
continue
}
$PathItem = Get-Item -LiteralPath $Path
$CandidateItem = $null
if ($PathItem.PSIsContainer)
{
$CandidateItem = Get-ChildItem -LiteralPath $Path -Recurse -File -ErrorAction SilentlyContinue |
Sort-Object LastWriteTimeUtc -Descending |
Select-Object -First 1
}
else
{
$CandidateItem = $PathItem
}
if ($null -eq $CandidateItem)
{
continue
}
if ($null -eq $MostRecentItem -or $CandidateItem.LastWriteTimeUtc -gt $MostRecentItem.LastWriteTimeUtc)
{
$MostRecentItem = $CandidateItem
}
}
return $MostRecentItem
}
function Assert-SkipBuildInputFreshness {
param(
[string]$RootPath,
[string]$Configuration,
[string]$GameTargetReceiptPath
)
if (-not (Test-Path -LiteralPath $GameTargetReceiptPath))
{
throw "SkipBuild was requested, but the packaged-game target receipt was not found at '$GameTargetReceiptPath'. Re-run without -SkipBuild so RunUAT can build the Win64 game target."
}
$GameBinaryPath = Resolve-ExistingPath -CandidatePaths @(
(Join-Path $RootPath 'UnrealHyperTwist\Binaries\Win64\UnrealHyperTwist.exe'),
(Join-Path $RootPath ("UnrealHyperTwist\Binaries\Win64\UnrealHyperTwist-Win64-{0}.exe" -f $Configuration))
)
if ($null -eq $GameBinaryPath)
{
throw "SkipBuild was requested, but no local packaged-game binary was found beneath '$(Join-Path $RootPath 'UnrealHyperTwist\Binaries\Win64')'. Re-run without -SkipBuild so RunUAT can build the Win64 game target."
}
$NewestBuildInputItem = Get-MostRecentFileItem -Paths @(
(Join-Path $RootPath 'UnrealHyperTwist\UnrealHyperTwist.uproject'),
(Join-Path $RootPath 'UnrealHyperTwist\Source'),
(Join-Path $RootPath 'UnrealHyperTwist\Config'),
(Join-Path $RootPath 'UnrealHyperTwist\Plugins')
)
if ($null -eq $NewestBuildInputItem)
{
return
}
$ReceiptItem = Get-Item -LiteralPath $GameTargetReceiptPath
$GameBinaryItem = Get-Item -LiteralPath $GameBinaryPath
if ($NewestBuildInputItem.LastWriteTimeUtc -gt $ReceiptItem.LastWriteTimeUtc `
-or $NewestBuildInputItem.LastWriteTimeUtc -gt $GameBinaryItem.LastWriteTimeUtc)
{
throw (
"SkipBuild was requested, but the packaged-game build artifacts are stale. " +
"Newest build input '{0}' ({1:o}) is newer than receipt '{2}' ({3:o}) or game binary '{4}' ({5:o}). " +
"Re-run without -SkipBuild so RunUAT can rebuild the Win64 game target; a passed editor build alone is not sufficient packaged-runtime proof."
) -f `
$NewestBuildInputItem.FullName,
$NewestBuildInputItem.LastWriteTimeUtc,
$ReceiptItem.FullName,
$ReceiptItem.LastWriteTimeUtc,
$GameBinaryItem.FullName,
$GameBinaryItem.LastWriteTimeUtc
}
}
if (-not (Test-Path $RunUatPath))
{
throw "RunUAT was not found at '$RunUatPath'."
}
if (-not (Test-Path $UProjectPath))
{
throw "UnrealHyperTwist project file was not found at '$UProjectPath'."
}
if (-not (Test-Path -LiteralPath $BootstrapLaunchArgumentsScriptPath -PathType Leaf))
{
throw "Bootstrap launch-argument helper was not found at '$BootstrapLaunchArgumentsScriptPath'."
}
foreach ($TargetCookMap in $CookMaps)
{
Assert-CookMapExists -RootPath $ProjectRoot -GameMapPath $TargetCookMap
}
foreach ($TargetSmokeMap in $ResolvedSmokeMaps)
{
Assert-CookMapExists -RootPath $ProjectRoot -GameMapPath $TargetSmokeMap
}
foreach ($ExpectedMaterialPath in $ExpectedClassicCubeMaterialPaths)
{
if (-not (Test-Path $ExpectedMaterialPath))
{
throw "Expected classic-cube material asset was not found at '$ExpectedMaterialPath'. Run 'scripts\\Invoke-HyperTwistClassicCubeMapAuthoring.ps1' first."
}
}
if ($SkipBuild)
{
Assert-SkipBuildInputFreshness `
-RootPath $ProjectRoot `
-Configuration $Configuration `
-GameTargetReceiptPath $GameTargetReceiptPath
}
if ($CleanArchive -and (Test-Path $ArchiveDirectory))
{
Remove-Item -LiteralPath $ArchiveDirectory -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $UnrealBuildToolSavedPath | Out-Null
New-Item -ItemType Directory -Force -Path $ArchiveDirectory | Out-Null
New-Item -ItemType Directory -Force -Path $ValidationRootPath | Out-Null
New-Item -ItemType Directory -Force -Path $SmokeReportDirectory | Out-Null
if ([string]::IsNullOrWhiteSpace($ValidationReportPath))
{
$ValidationReportPath = Join-Path $ValidationRootPath 'classic-cube-package-validation-report.json'
}
$RunUatArguments = @(
'BuildCookRun',
'-WaitForUATMutex',
"-project=$UProjectPath",
'-noP4',
'-platform=Win64',
"-clientconfig=$Configuration",
'-cook',
'-stage',
'-package',
'-pak',
'-archive',
"-archivedirectory=$ArchiveDirectory",
"-map=$($CookMaps -join '+')",
'-unattended',
'-utf8output'
)
if (-not $SkipBuild)
{
$RunUatArguments += '-build'
}
if ($ResolvedAdditionalCookerOptions.Count -gt 0)
{
# Keep the runtime package lane insulated from editor-only plugin/content bleed.
$RunUatArguments += "-AdditionalCookerOptions=$($ResolvedAdditionalCookerOptions -join ' ')"
}
$ValidationReport = [ordered]@{
reportVersion = 'ht-classic-cube-package-validation/v2'
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
projectRoot = $ProjectRoot
archiveDirectory = $ArchiveDirectory
configuration = $Configuration
cookMaps = @($CookMaps)
smokeMaps = @($ResolvedSmokeMaps)
additionalCookerOptions = @($ResolvedAdditionalCookerOptions)
headlessSmoke = $HeadlessSmoke
cleanArchive = [bool]$CleanArchive
skipBuild = [bool]$SkipBuild
skipLaunch = [bool]$SkipLaunch
result = 'failed'
packagedExecutablePath = $null
bootstrapLaunchArguments = [ordered]@{
result = 'pending'
traceControlProtection = if ($Configuration -eq 'Shipping')
{
'shipping-compile-time-disabled'
}
else
{
'runtime-listener-rejection-only'
}
expectedArguments = @($BootstrapLaunchArguments)
patchReportPath = $BootstrapLaunchArgumentsReportPath
patchReport = $null
runtimeProofs = @()
}
solverTable = [ordered]@{
result = 'pending'
sourcePath = $SolverTablePath
expectedSizeBytes = $ExpectedSolverTableSizeBytes
expectedSha256 = $ExpectedSolverTableSha256.ToLowerInvariant()
sourceEvidence = $null
packagedEvidence = $null
}
browserShell = [ordered]@{
result = 'pending'
sourceDirectory = $BrowserShellSourceDirectory
skipBuild = [bool]$SkipBrowserShellBuild
sourceEvidence = $null
packagedEvidence = $null
}
smokeReports = @()
error = $null
}
try
{
$ValidationReport.solverTable.sourceEvidence = Get-ValidatedSolverTableEvidence `
-Path $SolverTablePath `
-ExpectedSizeBytes $ExpectedSolverTableSizeBytes `
-ExpectedSha256 $ExpectedSolverTableSha256
$ValidationReport.browserShell.sourceEvidence = Invoke-BrowserShellBuild `
-SourceDirectory $BrowserShellSourceDirectory `
-SkipBuild ([bool]$SkipBrowserShellBuild)
Write-Host "Packaging HyperTwist classic-cube validation lane to '$ArchiveDirectory'..."
& $RunUatPath @RunUatArguments
if ($LASTEXITCODE -ne 0)
{
throw "RunUAT packaging failed with exit code $LASTEXITCODE."
}
$PackagedExecutablePath = Resolve-PackagedExecutablePath -PackageRoot $ArchiveDirectory
if ($null -eq $PackagedExecutablePath)
{
throw "No packaged UnrealHyperTwist executable was found beneath '$ArchiveDirectory' after packaging."
}
$ValidationReport.packagedExecutablePath = $PackagedExecutablePath
& $BootstrapLaunchArgumentsScriptPath `
-ExecutablePath $PackagedExecutablePath `
-AdditionalArguments $BootstrapLaunchArguments `
-ReportPath $BootstrapLaunchArgumentsReportPath | Out-Null
$BootstrapPatchReport = Read-JsonFile -Path $BootstrapLaunchArgumentsReportPath
if ($null -eq $BootstrapPatchReport -or $BootstrapPatchReport.result -ne 'passed')
{
throw "Packaged bootstrap launch-argument patch did not record a passed result."
}
$ValidationReport.bootstrapLaunchArguments.patchReport = $BootstrapPatchReport
$ValidationReport.solverTable.packagedEvidence = Copy-ValidatedSolverTableToPackage `
-SourcePath $SolverTablePath `
-PackagedExecutablePath $PackagedExecutablePath `
-ExpectedSizeBytes $ExpectedSolverTableSizeBytes `
-ExpectedSha256 $ExpectedSolverTableSha256
$ValidationReport.solverTable.result = 'passed'
$ValidationReport.browserShell.packagedEvidence = Copy-BrowserShellToPackage `
-SourceDirectory $BrowserShellSourceDirectory `
-PackagedExecutablePath $PackagedExecutablePath
$ValidationReport.browserShell.result = 'passed'
if (-not $SkipLaunch)
{
if (-not (Test-Path $LaunchScriptPath))
{
throw "Launch script was not found at '$LaunchScriptPath'."
}
foreach ($SmokeMap in $ResolvedSmokeMaps)
{
$SmokeReportPath = Join-Path $SmokeReportDirectory (
'{0}.json' -f (Convert-PathToken -Value $SmokeMap)
)
Write-Host "Smoke validating packaged classic-cube map '$SmokeMap'..."
& $LaunchScriptPath `
-PackageRoot $ArchiveDirectory `
-MapUrl $SmokeMap `
-ReportPath $SmokeReportPath `
-UseNullRHI:$HeadlessSmoke `
-NoSound:$HeadlessSmoke
if ($LASTEXITCODE -ne 0)
{
throw "Packaged classic-cube smoke launch failed for '$SmokeMap' with exit code $LASTEXITCODE."
}
if (-not (Test-Path $SmokeReportPath))
{
throw "Packaged classic-cube smoke launch for '$SmokeMap' completed without writing the expected report '$SmokeReportPath'."
}
$SmokeReport = Read-JsonFile -Path $SmokeReportPath
if ($null -eq $SmokeReport)
{
throw "Packaged classic-cube smoke report '$SmokeReportPath' could not be parsed."
}
if ($SmokeReport.result -ne 'passed')
{
throw "Packaged classic-cube smoke report '$SmokeReportPath' did not record a passed result."
}
if ($SmokeReport.mapUrl -ne $SmokeMap)
{
throw "Packaged classic-cube smoke report '$SmokeReportPath' targeted '$($SmokeReport.mapUrl)' instead of '$SmokeMap'."
}
$RuntimeArgumentEvidence = Get-BootstrapRuntimeArgumentEvidence `
-RuntimeLogPath $SmokeReport.runtimeLogPath `
-RuntimeDiagnosticsPath $SmokeReport.runtimeDiagnosticsPath `
-ObservedCommandLines @($SmokeReport.processCommandLines) `
-TraceControlListeningEndpoints @($SmokeReport.traceControlListeningEndpoints) `
-ExpectedArguments $BootstrapLaunchArguments
$ValidationReport.bootstrapLaunchArguments.runtimeProofs += @(
$RuntimeArgumentEvidence
)
if ($RuntimeArgumentEvidence.result -ne 'passed')
{
throw $RuntimeArgumentEvidence.error
}
$ValidationReport.smokeReports += @($SmokeReport)
}
$ValidationReport.bootstrapLaunchArguments.result = 'passed'
}
else
{
$ValidationReport.bootstrapLaunchArguments.result = 'patch-passed-runtime-not-run'
}
$ValidationReport.result = 'passed'
}
catch
{
$ValidationReport.error = $_.Exception.Message
Write-Utf8JsonFile -Path $ValidationReportPath -Value $ValidationReport
throw
}
Write-Utf8JsonFile -Path $ValidationReportPath -Value $ValidationReport