hypertwist/scripts/Invoke-HyperTwistXrPackage.ps1

466 lines
15 KiB
PowerShell

param(
[string]$ProjectRoot = 'C:\HyperTwist',
[string]$ArchiveDirectory = 'C:\HyperTwist\packaged\xr',
[ValidateSet('Development', 'Shipping')]
[string]$Configuration = 'Development',
[string]$CookMap = '/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining',
[string[]]$AdditionalCookMaps = @('/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining'),
[string[]]$SmokeMaps = @(),
[double]$ObservationSeconds = 8.0,
[double]$TimeoutSeconds = 30.0,
[ValidateSet('nullrhi', 'windowed')]
[string]$RuntimeLaunchMode = 'nullrhi',
[string[]]$AdditionalCookerOptions = @(
'-DisablePlugins=MovieRenderPipeline',
'-SkipCookingEditorContent'
),
[string]$ValidationReportPath = '',
[switch]$CleanArchive,
[switch]$SkipBuild,
[switch]$PrePackageEditorBuildPerformed,
[ValidateSet('not-run', 'passed')]
[string]$PrePackageEditorBuildResult = 'not-run',
[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-HyperTwistXrPackage.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
$ValidationRootPath = Join-Path $ArchiveDirectory 'validation'
$SmokeReportDirectory = Join-Path $ValidationRootPath 'smoke'
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 12
$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-JsonPropertyValue {
param(
[Parameter(Mandatory = $true)]
[object]$Object,
[Parameter(Mandatory = $true)]
[string[]]$Names,
[object]$Default = $null
)
foreach ($Name in $Names)
{
$Property = $Object.PSObject.Properties[$Name]
if ($null -ne $Property)
{
return $Property.Value
}
}
return $Default
}
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 XR packaged-validation cook map '$GameMapPath' was not found at '$ExpectedMapPath'."
}
}
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'."
}
foreach ($TargetMap in ($CookMaps + $ResolvedSmokeMaps | Select-Object -Unique))
{
Assert-CookMapExists -RootPath $ProjectRoot -GameMapPath $TargetMap
}
if ($SkipBuild)
{
Assert-SkipBuildInputFreshness `
-RootPath $ProjectRoot `
-Configuration $Configuration `
-GameTargetReceiptPath $GameTargetReceiptPath
}
if ($PrePackageEditorBuildPerformed -and $PrePackageEditorBuildResult -ne 'passed')
{
throw "PrePackageEditorBuildPerformed was set, but PrePackageEditorBuildResult was '$PrePackageEditorBuildResult' instead of 'passed'."
}
if (-not $PrePackageEditorBuildPerformed -and $PrePackageEditorBuildResult -eq 'passed')
{
throw "PrePackageEditorBuildResult was 'passed', but PrePackageEditorBuildPerformed was not set."
}
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 'xr-package-validation-report.json'
}
$RunUatArguments = @(
'BuildCookRun',
"-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)
{
$RunUatArguments += "-AdditionalCookerOptions=$($ResolvedAdditionalCookerOptions -join ' ')"
}
$ValidationReport = [ordered]@{
reportVersion = 'ht-xr-package-validation/v1'
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
projectRoot = $ProjectRoot
archiveDirectory = $ArchiveDirectory
configuration = $Configuration
cookMaps = @($CookMaps)
smokeMaps = @($ResolvedSmokeMaps)
observationSeconds = $ObservationSeconds
timeoutSeconds = $TimeoutSeconds
runtimeLaunchMode = $RuntimeLaunchMode
validationLaunchSurfaceId = 'xr/openxr-desktop-training-packaged-validation-launch-surface'
validationGameModeClassPath = '/Script/UnrealHyperTwist.HyperTwistXrTrainingGameMode'
additionalCookerOptions = @($ResolvedAdditionalCookerOptions)
cleanArchive = [bool]$CleanArchive
skipBuild = [bool]$SkipBuild
prePackageEditorBuild = [ordered]@{
lane = 'Windows Unreal editor build'
performed = [bool]$PrePackageEditorBuildPerformed
result = $PrePackageEditorBuildResult
}
skipLaunch = [bool]$SkipLaunch
result = 'failed'
packagedExecutablePath = $null
smokeReports = @()
packagedHeadMountedDisplaySessionObserved = $false
packagedControllerInputObserved = $false
packagedProofStatusSet = @()
remainingGateIds = @()
error = $null
}
try
{
Write-Host "Packaging HyperTwist XR 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
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 XR map '$SmokeMap'..."
& $LaunchScriptPath `
-PackageRoot $ArchiveDirectory `
-MapAssetPath $SmokeMap `
-ObservationSeconds $ObservationSeconds `
-TimeoutSeconds $TimeoutSeconds `
-RuntimeLaunchMode $RuntimeLaunchMode `
-ReportPath $SmokeReportPath
if ($LASTEXITCODE -ne 0)
{
throw "Packaged XR smoke launch failed for '$SmokeMap' with exit code $LASTEXITCODE."
}
if (-not (Test-Path $SmokeReportPath))
{
throw "Packaged XR smoke report '$SmokeReportPath' was not written for '$SmokeMap'."
}
$SmokeReport = Read-JsonFile -Path $SmokeReportPath
if ($null -eq $SmokeReport)
{
throw "Packaged XR smoke report '$SmokeReportPath' could not be parsed."
}
if ($SmokeReport.result -ne 'passed')
{
throw "Packaged XR smoke report '$SmokeReportPath' recorded result '$($SmokeReport.result)' for '$SmokeMap'."
}
if ($SmokeReport.mapAssetPath -ne $SmokeMap)
{
throw "Packaged XR smoke report '$SmokeReportPath' surfaced map '$($SmokeReport.mapAssetPath)' instead of '$SmokeMap'."
}
if ($null -eq $SmokeReport.runtimeReport)
{
throw "Packaged XR smoke report '$SmokeReportPath' did not retain the nested runtime report."
}
$RuntimeReport = $SmokeReport.runtimeReport
$ValidationReport.packagedHeadMountedDisplaySessionObserved =
$ValidationReport.packagedHeadMountedDisplaySessionObserved -or [bool](Get-JsonPropertyValue `
-Object $RuntimeReport `
-Names @('bObservedHeadMountedDisplaySession', 'ObservedHeadMountedDisplaySession') `
-Default $false)
$ValidationReport.packagedControllerInputObserved =
$ValidationReport.packagedControllerInputObserved -or [bool](Get-JsonPropertyValue `
-Object $RuntimeReport `
-Names @('bObservedControllerInputActivity', 'ObservedControllerInputActivity') `
-Default $false)
$ValidationReport.smokeReports += $SmokeReport
$ValidationReport.packagedProofStatusSet += [string]$RuntimeReport.PackagedProofStatus
foreach ($RemainingGateId in @((Get-JsonPropertyValue `
-Object $RuntimeReport `
-Names @('RemainingGateIds', 'remainingGateIds') `
-Default @())))
{
if (-not [string]::IsNullOrWhiteSpace($RemainingGateId))
{
$ValidationReport.remainingGateIds += $RemainingGateId
}
}
}
}
$ValidationReport.packagedProofStatusSet =
@($ValidationReport.packagedProofStatusSet | Select-Object -Unique)
$ValidationReport.remainingGateIds =
@($ValidationReport.remainingGateIds | Select-Object -Unique)
$ValidationReport.result = 'passed'
}
catch
{
$ValidationReport.error = $_.Exception.Message
Write-Utf8JsonFile -Path $ValidationReportPath -Value $ValidationReport
throw
}
Write-Utf8JsonFile -Path $ValidationReportPath -Value $ValidationReport