#requires -Version 7.0
param([string]$Destination, [switch]$SkipLaunchers, [switch]$VerifyDialogs, [string]$GameValidationRoot, [switch]$VerifyAbilityBanks, [switch]$VerifyExaltedEquipment)
$ErrorActionPreference = 'Stop'
if ($VerifyExaltedEquipment -and -not $VerifyAbilityBanks) { throw 'Exalted verification requires a fresh ability-bank validation fixture.' }
$taskRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
if (-not $Destination) { $Destination = Join-Path $taskRoot ('artifacts/package-' + [DateTime]::Now.ToString('yyyyMMdd-HHmmss')) }
$Destination = [IO.Path]::GetFullPath($Destination)
if (Test-Path -LiteralPath $Destination) { throw 'Choose a new output directory; existing packages are not overwritten.' }
New-Item -ItemType Directory -Path $Destination -Force | Out-Null
$buildRoot = Join-Path $taskRoot ('artifacts/publish-build/' + [Guid]::NewGuid().ToString('N'))
Push-Location $taskRoot
try {
& (Join-Path $PSScriptRoot 'Check-Git.ps1')
$applications = @(
@{ Name='Server'; Project='Server/XFEWarframe.Server'; Output='Server' },
@{ Name='Admin'; Project='Admin/XFEWarframe.Admin'; Output='Admin' },
@{ Name='Player'; Project='Client/XFEWarframe.Player'; Output='Client' },
@{ Name='Installer'; Project='Client/XFEWarframe.Installer'; Output='Client/Installer' }
)
foreach ($app in $applications) {
$project = Join-Path $app.Project ("XFEWarframe." + $app.Name + ".csproj")
dotnet publish $project -c Release -r win-x64 --self-contained true ("-p:XFEIntermediateRoot=" + (Join-Path $buildRoot "obj/$($app.Name)")) ("-p:OutputPath=" + (Join-Path $buildRoot $app.Name) + [IO.Path]::DirectorySeparatorChar) -o (Join-Path $Destination $app.Output) --nologo
if ($LASTEXITCODE -ne 0) { throw "$($app.Name) publish failed" }
}
& (Join-Path $PSScriptRoot 'Build-Bridge.ps1') -Destination (Join-Path $Destination 'Client/Bridge') -IfNeeded
Copy-Item -LiteralPath LICENSE,README.md,THIRD-PARTY-NOTICES.md -Destination $Destination
Copy-Item -LiteralPath docs -Destination $Destination -Recurse
& (Join-Path $PSScriptRoot 'Export-Licenses.ps1') -Destination (Join-Path $Destination 'licenses')
$validation = Join-Path $Destination 'validation'
New-Item -ItemType Directory -Path $validation -Force | Out-Null
foreach ($name in @('core-tests.trx','client-tests.trx')) {
$report = Join-Path $taskRoot "artifacts/test-results/$name"
if (Test-Path -LiteralPath $report) { Copy-Item -LiteralPath $report -Destination $validation }
}
foreach ($name in @('client-validation.json','client-compatibility-initial.json')) {
$report = Join-Path $taskRoot "artifacts/$name"
if (Test-Path -LiteralPath $report) { Copy-Item -LiteralPath $report -Destination $validation }
}
$sourceRoot = Join-Path $Destination 'source'
New-Item -ItemType Directory -Path $sourceRoot -Force | Out-Null
# Include corresponding source, never machine-specific profiles, secrets, live state, or build caches.
$sourceRelativeFiles = @(git -c core.quotepath=false ls-files --cached --others --exclude-standard -- Server Client Admin scripts docs)
if ($LASTEXITCODE -ne 0) { throw 'Cannot enumerate source files with the repository ignore rules.' }
$sourceFiles = $sourceRelativeFiles | ForEach-Object { $path = Join-Path $taskRoot $_; if (Test-Path -LiteralPath $path -PathType Leaf) { Get-Item -LiteralPath $path } }
foreach ($file in $sourceFiles) {
$target = Join-Path $sourceRoot ([IO.Path]::GetRelativePath($taskRoot, $file.FullName))
New-Item -ItemType Directory -Path (Split-Path $target) -Force | Out-Null
Copy-Item -LiteralPath $file.FullName -Destination $target
}
Copy-Item -LiteralPath XFEWarframe.slnx,XFEWarframe.slnLaunch,Directory.Build.props,global.json,LICENSE,README.md,THIRD-PARTY-NOTICES.md,.gitignore,.gitattributes,.editorconfig -Destination $sourceRoot
$launchers = @{
'Server/Start.cmd' = '"%~dp0XFEWarframe.Server.exe" --root="%~dp0runtime" %*'
'Admin/Start.cmd' = 'start "" "%~dp0XFEWarframe.Admin.exe" %*'
'Client/Start.cmd' = 'start "" "%~dp0XFEWarframe.Player.exe" %*'
'Client/Installer.cmd' = '"%~dp0Installer\XFEWarframe.Installer.exe" %*'
}
foreach ($entry in $launchers.GetEnumerator()) {
[IO.File]::WriteAllText((Join-Path $Destination $entry.Key), "@echo off" + [Environment]::NewLine + $entry.Value + [Environment]::NewLine, [Text.Encoding]::ASCII)
}
# Exercise the exact published executables and resource files before archiving.
& (Join-Path $PSScriptRoot 'Smoke-Test.ps1') -ServerPath (Join-Path $Destination 'Server/XFEWarframe.Server.exe') | Tee-Object -Variable taskHttpOutput
& (Join-Path $PSScriptRoot 'Smoke-Desktop.ps1') -Package $Destination | Tee-Object -Variable taskLayoutOutput
if ($VerifyDialogs) {
& (Join-Path $PSScriptRoot 'Smoke-Desktop.ps1') -Package $Destination -VerifyDialogs | Tee-Object -Variable taskDialogOutput
}
& (Join-Path $Destination 'Client/Installer/XFEWarframe.Installer.exe') selftest
if ($LASTEXITCODE -ne 0) { throw 'Published NativeAOT bridge self-test failed' }
@{ passed=$true; scope='NativeAOT, x64 relocation, callbacks and rollback; automatic Client/Bridge discovery' } |
ConvertTo-Json | Set-Content -LiteralPath (Join-Path $validation 'native-bridge-selftest.json') -Encoding utf8
$taskReportSets = @(
@{ Output=$taskHttpOutput; Marker='HTTP smoke results: '; Names=@('results.json'); Prefix='http-' },
@{ Output=$taskLayoutOutput; Marker='Desktop reports: '; Names=@('Admin.json','Player.json','PlayerReadOnly.json'); Prefix='desktop-layout-' }
)
if ($VerifyDialogs) {
$taskReportSets += @{ Output=$taskDialogOutput; Marker='Desktop reports: '; Names=@('Admin.json','Player.json','PlayerReadOnly.json'); Prefix='desktop-' }
} else {
$taskReportSets += @{ Output=$taskLayoutOutput; Marker='Desktop reports: '; Names=@('Admin.json','Player.json','PlayerReadOnly.json'); Prefix='desktop-' }
}
# Copy this run's reports, never another parallel validation's most recent directory.
foreach ($spec in $taskReportSets) {
$taskReportLine = @($spec.Output | Where-Object { "$_".StartsWith($spec.Marker, [StringComparison]::Ordinal) })
if ($taskReportLine.Count -ne 1) { throw "Missing or ambiguous validation report: $($spec.Prefix)" }
$taskReportDirectory = "$($taskReportLine[0])".Substring($spec.Marker.Length)
foreach ($name in $spec.Names) {
Copy-Item -LiteralPath (Join-Path $taskReportDirectory $name) -Destination (Join-Path $validation ($spec.Prefix + $name))
}
}
if ($VerifyAbilityBanks) {
& (Join-Path $PSScriptRoot 'Start-MultiAbilityTest.ps1') -Package $Destination -AbilityBanks -NoWait
$GameValidationRoot = Join-Path $taskRoot 'artifacts/ability-bank-game-test'
}
if ($VerifyExaltedEquipment) {
& (Join-Path $PSScriptRoot 'Start-ExaltedEquipmentTest.ps1') -Package $Destination -SessionFile (Join-Path $GameValidationRoot 'session.json') -ReportDirectory (Join-Path $GameValidationRoot 'exalted-validation')
}
if ($GameValidationRoot) {
$taskBridgeHash = (Get-FileHash -LiteralPath (Join-Path $Destination 'Client/Bridge/XFEWarframe.Bridge.dll') -Algorithm SHA256).Hash
foreach ($name in @('login','engine')) {
$report = Join-Path $GameValidationRoot ($name + '-validation/result.json')
$taskGameResult = Get-Content -LiteralPath $report -Raw | ConvertFrom-Json
if ($taskGameResult.passed -ne $true -or $taskGameResult.bridgeSha256 -ne $taskBridgeHash) {
throw "The $name game validation did not pass with this exact bridge binary."
}
Copy-Item -LiteralPath $report -Destination (Join-Path $validation ('game-' + $name + '.json'))
}
$taskWheelReport = Join-Path $GameValidationRoot 'wheel-validation/result.json'
if (Test-Path -LiteralPath $taskWheelReport) {
$taskWheelResult = Get-Content -LiteralPath $taskWheelReport -Raw | ConvertFrom-Json
if ($taskWheelResult.passed -ne $true -or $taskWheelResult.bridgeSha256 -ne $taskBridgeHash) {
throw 'The wheel validation did not pass with this exact bridge binary.'
}
Copy-Item -LiteralPath $taskWheelReport -Destination (Join-Path $validation 'game-wheels.json')
}
foreach ($name in @('king','audio','branding-login','branding-game') + $(if ($VerifyExaltedEquipment) { @('exalted') } else { @() })) {
$report = Join-Path $GameValidationRoot ($name + '-validation/result.json')
if (-not (Test-Path -LiteralPath $report)) {
if ($VerifyAbilityBanks) { throw "Missing $name game validation." }
continue
}
$taskGameResult = Get-Content -LiteralPath $report -Raw | ConvertFrom-Json
if ($taskGameResult.passed -ne $true -or $taskGameResult.bridgeSha256 -ne $taskBridgeHash) {
throw "The $name game validation did not pass with this exact bridge binary."
}
Copy-Item -LiteralPath $report -Destination (Join-Path $validation ('game-' + $name + '.json'))
}
}
$common = @('source','docs','licenses','LICENSE','README.md','THIRD-PARTY-NOTICES.md','validation') | ForEach-Object { Join-Path $Destination $_ }
foreach ($role in @('Server','Client','Admin')) {
Compress-Archive -LiteralPath (@((Join-Path $Destination $role)) + $common) -DestinationPath (Join-Path $Destination "XFEWarframe-$role-win-x64.zip") -CompressionLevel Fastest
}
Get-ChildItem -LiteralPath $Destination -Filter '*.zip' | Get-FileHash -Algorithm SHA256 |
Select-Object @{n='File';e={Split-Path $_.Path -Leaf}},Hash | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Destination 'SHA256.json') -Encoding utf8
} finally { Pop-Location }
Write-Output "Published Server, Client and Admin to $Destination"
if ($SkipLaunchers) { return }
$taskPackageRelative = [IO.Path]::GetRelativePath((Join-Path $taskRoot 'artifacts'), $Destination)
foreach ($entry in @(
@{Name='Server';Command=('"%~dp0' + $taskPackageRelative + '\Server\XFEWarframe.Server.exe" --root="%~dp0..\Server\runtime" %*')},
@{Name='Desktop';Command=('start "" "%~dp0' + $taskPackageRelative + '\Admin\XFEWarframe.Admin.exe" --root="%~dp0..\Server\runtime" %*')},
@{Name='Player';Command=('start "" "%~dp0' + $taskPackageRelative + '\Client\XFEWarframe.Player.exe" %*')}
)) {
[IO.File]::WriteAllText((Join-Path $taskRoot ('artifacts/Start-Updated-' + $entry.Name + '.cmd')), "@echo off`r`nchcp 65001 >nul`r`n" + $entry.Command + "`r`n", [Text.UTF8Encoding]::new($false))
}
#requires -Version 7.0
param([string]$Destination, [switch]$SkipLaunchers, [switch]$VerifyDialogs, [string]$GameValidationRoot, [switch]$VerifyAbilityBanks, [switch]$VerifyExaltedEquipment)
$ErrorActionPreference = 'Stop'
if ($VerifyExaltedEquipment -and -not $VerifyAbilityBanks) { throw 'Exalted verification requires a fresh ability-bank validation fixture.' }
$taskRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
if (-not $Destination) { $Destination = Join-Path $taskRoot ('artifacts/package-' + [DateTime]::Now.ToString('yyyyMMdd-HHmmss')) }
$Destination = [IO.Path]::GetFullPath($Destination)
if (Test-Path -LiteralPath $Destination) { throw 'Choose a new output directory; existing packages are not overwritten.' }
New-Item -ItemType Directory -Path $Destination -Force | Out-Null
$buildRoot = Join-Path $taskRoot ('artifacts/publish-build/' + [Guid]::NewGuid().ToString('N'))
Push-Location $taskRoot
try {
& (Join-Path $PSScriptRoot 'Check-Git.ps1')
$applications = @(
@{ Name='Server'; Project='Server/XFEWarframe.Server'; Output='Server' },
@{ Name='Admin'; Project='Admin/XFEWarframe.Admin'; Output='Admin' },
@{ Name='Player'; Project='Client/XFEWarframe.Player'; Output='Client' },
@{ Name='Installer'; Project='Client/XFEWarframe.Installer'; Output='Client/Installer' }
)
foreach ($app in $applications) {
$project = Join-Path $app.Project ("XFEWarframe." + $app.Name + ".csproj")
dotnet publish $project -c Release -r win-x64 --self-contained true ("-p:XFEIntermediateRoot=" + (Join-Path $buildRoot "obj/$($app.Name)")) ("-p:OutputPath=" + (Join-Path $buildRoot $app.Name) + [IO.Path]::DirectorySeparatorChar) -o (Join-Path $Destination $app.Output) --nologo
if ($LASTEXITCODE -ne 0) { throw "$($app.Name) publish failed" }
}
& (Join-Path $PSScriptRoot 'Build-Bridge.ps1') -Destination (Join-Path $Destination 'Client/Bridge') -IfNeeded
Copy-Item -LiteralPath LICENSE,README.md,THIRD-PARTY-NOTICES.md -Destination $Destination
Copy-Item -LiteralPath docs -Destination $Destination -Recurse
& (Join-Path $PSScriptRoot 'Export-Licenses.ps1') -Destination (Join-Path $Destination 'licenses')
$validation = Join-Path $Destination 'validation'
New-Item -ItemType Directory -Path $validation -Force | Out-Null
foreach ($name in @('core-tests.trx','client-tests.trx')) {
$report = Join-Path $taskRoot "artifacts/test-results/$name"
if (Test-Path -LiteralPath $report) { Copy-Item -LiteralPath $report -Destination $validation }
}
foreach ($name in @('client-validation.json','client-compatibility-initial.json')) {
$report = Join-Path $taskRoot "artifacts/$name"
if (Test-Path -LiteralPath $report) { Copy-Item -LiteralPath $report -Destination $validation }
}
$sourceRoot = Join-Path $Destination 'source'
New-Item -ItemType Directory -Path $sourceRoot -Force | Out-Null
# Include corresponding source, never machine-specific profiles, secrets, live state, or build caches.
$sourceRelativeFiles = @(git -c core.quotepath=false ls-files --cached --others --exclude-standard -- Server Client Admin scripts docs)
if ($LASTEXITCODE -ne 0) { throw 'Cannot enumerate source files with the repository ignore rules.' }
$sourceFiles = $sourceRelativeFiles | ForEach-Object { $path = Join-Path $taskRoot $_; if (Test-Path -LiteralPath $path -PathType Leaf) { Get-Item -LiteralPath $path } }
foreach ($file in $sourceFiles) {
$target = Join-Path $sourceRoot ([IO.Path]::GetRelativePath($taskRoot, $file.FullName))
New-Item -ItemType Directory -Path (Split-Path $target) -Force | Out-Null
Copy-Item -LiteralPath $file.FullName -Destination $target
}
Copy-Item -LiteralPath XFEWarframe.slnx,XFEWarframe.slnLaunch,Directory.Build.props,global.json,LICENSE,README.md,THIRD-PARTY-NOTICES.md,.gitignore,.gitattributes,.editorconfig -Destination $sourceRoot
$launchers = @{
'Server/Start.cmd' = '"%~dp0XFEWarframe.Server.exe" --root="%~dp0runtime" %*'
'Admin/Start.cmd' = 'start "" "%~dp0XFEWarframe.Admin.exe" %*'
'Client/Start.cmd' = 'start "" "%~dp0XFEWarframe.Player.exe" %*'
'Client/Installer.cmd' = '"%~dp0Installer\XFEWarframe.Installer.exe" %*'
}
foreach ($entry in $launchers.GetEnumerator()) {
[IO.File]::WriteAllText((Join-Path $Destination $entry.Key), "@echo off" + [Environment]::NewLine + $entry.Value + [Environment]::NewLine, [Text.Encoding]::ASCII)
}
# Exercise the exact published executables and resource files before archiving.
& (Join-Path $PSScriptRoot 'Smoke-Test.ps1') -ServerPath (Join-Path $Destination 'Server/XFEWarframe.Server.exe') | Tee-Object -Variable taskHttpOutput
& (Join-Path $PSScriptRoot 'Smoke-Desktop.ps1') -Package $Destination | Tee-Object -Variable taskLayoutOutput
if ($VerifyDialogs) {
& (Join-Path $PSScriptRoot 'Smoke-Desktop.ps1') -Package $Destination -VerifyDialogs | Tee-Object -Variable taskDialogOutput
}
& (Join-Path $Destination 'Client/Installer/XFEWarframe.Installer.exe') selftest
if ($LASTEXITCODE -ne 0) { throw 'Published NativeAOT bridge self-test failed' }
@{ passed=$true; scope='NativeAOT, x64 relocation, callbacks and rollback; automatic Client/Bridge discovery' } |
ConvertTo-Json | Set-Content -LiteralPath (Join-Path $validation 'native-bridge-selftest.json') -Encoding utf8
$taskReportSets = @(
@{ Output=$taskHttpOutput; Marker='HTTP smoke results: '; Names=@('results.json'); Prefix='http-' },
@{ Output=$taskLayoutOutput; Marker='Desktop reports: '; Names=@('Admin.json','Player.json','PlayerReadOnly.json'); Prefix='desktop-layout-' }
)
if ($VerifyDialogs) {
$taskReportSets += @{ Output=$taskDialogOutput; Marker='Desktop reports: '; Names=@('Admin.json','Player.json','PlayerReadOnly.json'); Prefix='desktop-' }
} else {
$taskReportSets += @{ Output=$taskLayoutOutput; Marker='Desktop reports: '; Names=@('Admin.json','Player.json','PlayerReadOnly.json'); Prefix='desktop-' }
}
# Copy this run's reports, never another parallel validation's most recent directory.
foreach ($spec in $taskReportSets) {
$taskReportLine = @($spec.Output | Where-Object { "$_".StartsWith($spec.Marker, [StringComparison]::Ordinal) })
if ($taskReportLine.Count -ne 1) { throw "Missing or ambiguous validation report: $($spec.Prefix)" }
$taskReportDirectory = "$($taskReportLine[0])".Substring($spec.Marker.Length)
foreach ($name in $spec.Names) {
Copy-Item -LiteralPath (Join-Path $taskReportDirectory $name) -Destination (Join-Path $validation ($spec.Prefix + $name))
}
}
if ($VerifyAbilityBanks) {
& (Join-Path $PSScriptRoot 'Start-MultiAbilityTest.ps1') -Package $Destination -AbilityBanks -NoWait
$GameValidationRoot = Join-Path $taskRoot 'artifacts/ability-bank-game-test'
}
if ($VerifyExaltedEquipment) {
& (Join-Path $PSScriptRoot 'Start-ExaltedEquipmentTest.ps1') -Package $Destination -SessionFile (Join-Path $GameValidationRoot 'session.json') -ReportDirectory (Join-Path $GameValidationRoot 'exalted-validation')
}
if ($GameValidationRoot) {
$taskBridgeHash = (Get-FileHash -LiteralPath (Join-Path $Destination 'Client/Bridge/XFEWarframe.Bridge.dll') -Algorithm SHA256).Hash
foreach ($name in @('login','engine')) {
$report = Join-Path $GameValidationRoot ($name + '-validation/result.json')
$taskGameResult = Get-Content -LiteralPath $report -Raw | ConvertFrom-Json
if ($taskGameResult.passed -ne $true -or $taskGameResult.bridgeSha256 -ne $taskBridgeHash) {
throw "The $name game validation did not pass with this exact bridge binary."
}
Copy-Item -LiteralPath $report -Destination (Join-Path $validation ('game-' + $name + '.json'))
}
$taskWheelReport = Join-Path $GameValidationRoot 'wheel-validation/result.json'
if (Test-Path -LiteralPath $taskWheelReport) {
$taskWheelResult = Get-Content -LiteralPath $taskWheelReport -Raw | ConvertFrom-Json
if ($taskWheelResult.passed -ne $true -or $taskWheelResult.bridgeSha256 -ne $taskBridgeHash) {
throw 'The wheel validation did not pass with this exact bridge binary.'
}
Copy-Item -LiteralPath $taskWheelReport -Destination (Join-Path $validation 'game-wheels.json')
}
foreach ($name in @('king','audio','branding-login','branding-game') + $(if ($VerifyExaltedEquipment) { @('exalted') } else { @() })) {
$report = Join-Path $GameValidationRoot ($name + '-validation/result.json')
if (-not (Test-Path -LiteralPath $report)) {
if ($VerifyAbilityBanks) { throw "Missing $name game validation." }
continue
}
$taskGameResult = Get-Content -LiteralPath $report -Raw | ConvertFrom-Json
if ($taskGameResult.passed -ne $true -or $taskGameResult.bridgeSha256 -ne $taskBridgeHash) {
throw "The $name game validation did not pass with this exact bridge binary."
}
Copy-Item -LiteralPath $report -Destination (Join-Path $validation ('game-' + $name + '.json'))
}
}
$common = @('source','docs','licenses','LICENSE','README.md','THIRD-PARTY-NOTICES.md','validation') | ForEach-Object { Join-Path $Destination $_ }
foreach ($role in @('Server','Client','Admin')) {
Compress-Archive -LiteralPath (@((Join-Path $Destination $role)) + $common) -DestinationPath (Join-Path $Destination "XFEWarframe-$role-win-x64.zip") -CompressionLevel Fastest
}
Get-ChildItem -LiteralPath $Destination -Filter '*.zip' | Get-FileHash -Algorithm SHA256 |
Select-Object @{n='File';e={Split-Path $_.Path -Leaf}},Hash | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Destination 'SHA256.json') -Encoding utf8
} finally { Pop-Location }
Write-Output "Published Server, Client and Admin to $Destination"
if ($SkipLaunchers) { return }
$taskPackageRelative = [IO.Path]::GetRelativePath((Join-Path $taskRoot 'artifacts'), $Destination)
foreach ($entry in @(
@{Name='Server';Command=('"%~dp0' + $taskPackageRelative + '\Server\XFEWarframe.Server.exe" --root="%~dp0..\Server\runtime" %*')},
@{Name='Desktop';Command=('start "" "%~dp0' + $taskPackageRelative + '\Admin\XFEWarframe.Admin.exe" --root="%~dp0..\Server\runtime" %*')},
@{Name='Player';Command=('start "" "%~dp0' + $taskPackageRelative + '\Client\XFEWarframe.Player.exe" %*')}
)) {
[IO.File]::WriteAllText((Join-Path $taskRoot ('artifacts/Start-Updated-' + $entry.Name + '.cmd')), "@echo off`r`nchcp 65001 >nul`r`n" + $entry.Command + "`r`n", [Text.UTF8Encoding]::new($false))
}