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