815 lines
29 KiB
PowerShell
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
|