[CmdletBinding()] param( [ValidateRange(1, 16)][int]$MaxParallelActions = 16, [ValidateRange(60, 900)][int]$TimeoutSeconds = 900 ) # Stages only inside Saved/BuildHost, builds the isolated host, then audits the exact Core object # files used by the UE module link. Le projet propriétaire et son éditeur ne sont jamais lancés. Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $PluginRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) $HostRoot = Join-Path $PluginRoot 'Saved\BuildHost\HostProject' $HostProject = Join-Path $HostRoot 'HostProject.uproject' $StagePlugin = Join-Path $HostRoot 'Plugins\VoxelForge' $StageStandalone = Join-Path $StagePlugin 'Standalone' $LogRoot = Join-Path $PluginRoot 'Saved\BuildHost' $EngineRoot = 'E:\Program Files\Epic Games\UE_5.7' $UbtDll = Join-Path $EngineRoot 'Engine\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.dll' $CoreManifest = Join-Path $PluginRoot 'Standalone\Core\core_sources.txt' $BuildLog = Join-Path $LogRoot 'UBT-CoreIntegration.log' $BuildStdout = Join-Path $LogRoot 'UBT-CoreIntegration.stdout.log' $BuildStderr = Join-Path $LogRoot 'UBT-CoreIntegration.stderr.log' $AuditLibrary = Join-Path $LogRoot 'VoxelForgeCore_ue_link_inputs.lib' $AuditResult = Join-Path $LogRoot 'VoxelForgeCore_ue_link_safety.log' function Assert-UnderPath([string]$Candidate, [string]$Root) { $candidateFull = [IO.Path]::GetFullPath($Candidate).TrimEnd('\', '/') $rootFull = [IO.Path]::GetFullPath($Root).TrimEnd('\', '/') if (-not ($candidateFull.Equals($rootFull, [StringComparison]::OrdinalIgnoreCase) -or $candidateFull.StartsWith($rootFull + '\', [StringComparison]::OrdinalIgnoreCase))) { throw "Refusing a BuildHost path outside its root: $candidateFull" } } if (-not ('VoxelForgeRound4BuildMemoryProbe' -as [type])) { Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; public static class VoxelForgeRound4BuildMemoryProbe { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] private struct MEMORYSTATUSEX { public uint dwLength; public uint dwMemoryLoad; public ulong ullTotalPhys; public ulong ullAvailPhys; public ulong ullTotalPageFile; public ulong ullAvailPageFile; public ulong ullTotalVirtual; public ulong ullAvailVirtual; public ulong ullAvailExtendedVirtual; } [DllImport("kernel32.dll", SetLastError = true)] private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer); public static ulong AvailablePhysicalBytes() { var status = new MEMORYSTATUSEX(); status.dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX)); if (!GlobalMemoryStatusEx(ref status)) throw new InvalidOperationException("GlobalMemoryStatusEx failed."); return status.ullAvailPhys; } } '@ } function Wait-ForLaunchMemory { while ([VoxelForgeRound4BuildMemoryProbe]::AvailablePhysicalBytes() -lt 4GB) { Write-Warning 'BuildHost UBT launch is waiting for at least 4 GiB of free physical memory.' Start-Sleep -Seconds 20 } } function Copy-DirectoryContents([string]$Source, [string]$Destination) { if (-not (Test-Path -LiteralPath $Source -PathType Container)) { throw "Canonical Core source directory is missing: $Source" } [IO.Directory]::CreateDirectory($Destination) | Out-Null foreach ($item in @(Get-ChildItem -LiteralPath $Source -Force)) { $target = Join-Path $Destination $item.Name if ($item.PSIsContainer) { Copy-DirectoryContents $item.FullName $target } else { Copy-Item -LiteralPath $item.FullName -Destination $target -Force } } } function Find-VsTool([string]$ToolName) { $toolRoot = 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC' foreach ($version in @(Get-ChildItem -LiteralPath $toolRoot -Directory | Sort-Object Name -Descending)) { $candidate = Join-Path $version.FullName "bin\Hostx64\x64\$ToolName" if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate } } throw "$ToolName was not found in the configured VS 2022 toolchain." } foreach ($required in @($HostProject, $UbtDll, $CoreManifest)) { if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { throw "Required file is missing: $required" } } Assert-UnderPath $StagePlugin $HostRoot Assert-UnderPath $StageStandalone $StagePlugin Assert-UnderPath $LogRoot $PluginRoot Write-Host 'Staging plugin source and canonical Core inputs into BuildHost.' $stageSource = Join-Path $StagePlugin 'Source' Assert-UnderPath $stageSource $StagePlugin if (Test-Path -LiteralPath $stageSource -PathType Container) { $sourceItem = Get-Item -LiteralPath $stageSource -Force if (($sourceItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "Refusing to replace a linked BuildHost source path: $stageSource" } $nestedLinks = @(Get-ChildItem -LiteralPath $stageSource -Force -Recurse | Where-Object { ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 }) if ($nestedLinks.Count -gt 0) { throw "Refusing to replace BuildHost source containing a link: $($nestedLinks[0].FullName)" } Remove-Item -LiteralPath $stageSource -Recurse -Force } Copy-DirectoryContents (Join-Path $PluginRoot 'Source') $stageSource Copy-DirectoryContents (Join-Path $PluginRoot 'Standalone\Core\include') (Join-Path $StageStandalone 'Core\include') Copy-DirectoryContents (Join-Path $PluginRoot 'Standalone\Core\src') (Join-Path $StageStandalone 'Core\src') Copy-DirectoryContents (Join-Path $PluginRoot 'Standalone\Compat') (Join-Path $StageStandalone 'Compat') Copy-DirectoryContents (Join-Path $PluginRoot 'Standalone\Source') (Join-Path $StageStandalone 'Source') Copy-Item -LiteralPath $CoreManifest -Destination (Join-Path $StageStandalone 'Core\core_sources.txt') -Force Copy-Item -LiteralPath (Join-Path $PluginRoot 'VoxelForge.uplugin') ` -Destination (Join-Path $StagePlugin 'VoxelForge.uplugin') -Force # The verified owner-asset copies are already staged under BuildHost. Read only those copies here; # never resolve, open, copy, or compare against the owner's project Content directory. $hostAssetRoot = Join-Path $HostRoot 'Content\VoxelForge' $expectedAssetHashes = @{ 'DA_Settings' = '8ABE8243104980260FECA0A6F4CF4927768E8E568221A6F6A2C2694F6D15D454' 'DA_Strate1' = 'A9CA6CDDD102250A893E379350B1D0F4AAF04C4F7FCA70E0DB46052A067D2CFB' 'DA_Strate2' = '8F9A12A3181635149E6F79F192E0DDAF6B6577442EE324071A7749DCC78B9A0C' 'DA_Strate3' = '5470D538C3C5E88237463E71BCA9E2EB170DFE3A0543C0F33298D9FB010FD2F9' 'DA_Strate4' = 'A98E33E1CD42F374D6CB70F5071E8EBBA74D91ED59D2FD3201782DAB84D28F5C' } foreach ($assetName in $expectedAssetHashes.Keys) { $stagedAsset = Join-Path $hostAssetRoot "$assetName.uasset" if (-not (Test-Path -LiteralPath $stagedAsset -PathType Leaf)) { throw "Verified BuildHost asset copy is missing: $stagedAsset" } $stagedHash = (Get-FileHash -LiteralPath $stagedAsset -Algorithm SHA256).Hash if ($stagedHash -ne $expectedAssetHashes[$assetName]) { throw "BuildHost owner asset copy differs from the recorded owner-asset hash: $assetName" } } $dotnet = (Get-Command dotnet.exe -ErrorAction Stop).Source $argumentList = @( ('"{0}"' -f $UbtDll), 'UnrealEditor', 'Win64', 'Development', ('-Project="{0}"' -f $HostProject), '-WaitMutex', '-NoHotReloadFromIDE', '-NoUBA', '-NoXGE', '-NoFASTBuild', '-NoSNDBS', "-MaxParallelActions=$MaxParallelActions", ('-Log="{0}"' -f $BuildLog) ) $crashRoots = @((Join-Path $HostRoot 'Saved\Crashes'), (Join-Path $StagePlugin 'Saved\Crashes')) $crashBefore = @{} foreach ($crashRoot in $crashRoots) { if (Test-Path -LiteralPath $crashRoot -PathType Container) { foreach ($crash in @(Get-ChildItem -LiteralPath $crashRoot -Directory -Filter 'UECC-*' -Recurse -Force)) { $crashBefore[$crash.FullName.ToLowerInvariant()] = $crash.LastWriteTimeUtc.Ticks } } } $buildSucceeded = $false for ($attempt = 1; $attempt -le 2 -and -not $buildSucceeded; ++$attempt) { $attemptStdout = if ($attempt -eq 1) { $BuildStdout } else { Join-Path $LogRoot 'UBT-CoreIntegration.retry.stdout.log' } $attemptStderr = if ($attempt -eq 1) { $BuildStderr } else { Join-Path $LogRoot 'UBT-CoreIntegration.retry.stderr.log' } Write-Host "BuildHost UBT attempt $attempt of 2; max parallel actions=$MaxParallelActions; Normal priority." Wait-ForLaunchMemory $foreignEditors = @(Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -like 'UnrealEditor*' }) if ($foreignEditors.Count -gt 0) { $ids = ($foreignEditors | ForEach-Object { "$($_.ProcessName):$($_.Id)" }) -join ', ' throw "UnrealEditor is already running; leaving it untouched: $ids" } $process = Start-Process -FilePath $dotnet -ArgumentList $argumentList -WorkingDirectory $PluginRoot ` -RedirectStandardOutput $attemptStdout -RedirectStandardError $attemptStderr ` -WindowStyle Hidden -PassThru $process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal $started = [datetime]::UtcNow while (-not $process.HasExited) { Start-Sleep -Seconds 5 $process.Refresh() if (([datetime]::UtcNow - $started).TotalSeconds -gt $TimeoutSeconds) { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue throw "BuildHost UBT exceeded the $TimeoutSeconds-second time box; stopped PID $($process.Id)." } if (-not $process.HasExited -and $process.PrivateMemorySize64 -gt 8GB) { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue throw "BuildHost UBT exceeded the 8 GiB private-memory limit; stopped PID $($process.Id)." } } $process.WaitForExit() $process.Refresh() $buildExitCode = $process.ExitCode if ([string]::IsNullOrEmpty([string]$buildExitCode) -and (Test-Path -LiteralPath $BuildLog -PathType Leaf) -and ((Get-Content -LiteralPath $BuildLog -Raw) -match '(?m)^Result: Succeeded\s*$')) { # Some Windows Start-Process handles lose the exit-code field after UBT's child exits; # accept only UBT's explicit successful target result in that case. $buildExitCode = 0 } if ($buildExitCode -eq 0) { $buildSucceeded = $true Write-Host "BuildHost UBT attempt $attempt passed." } elseif ($buildExitCode -eq -1073741502 -and $attempt -eq 1) { Write-Warning 'BuildHost UBT exited with 0xC0000142; retrying once as requested.' } else { throw "BuildHost UBT attempt $attempt failed with exit code '$buildExitCode'. See $BuildLog." } } if (-not $buildSucceeded) { throw 'BuildHost UBT did not complete successfully.' } $manifestRows = @(Get-Content -LiteralPath $CoreManifest | ForEach-Object { $_.Trim().Replace('\', '/') } | Where-Object { $_ -and -not $_.StartsWith('#') }) $expectedObjects = @($manifestRows | ForEach-Object { 'VFCore_' + ($_ -replace '[^A-Za-z0-9]+', '_') + '.cpp.obj' }) $objectRoot = Join-Path $StagePlugin 'Intermediate\Build' $coreObjects = @(Get-ChildItem -LiteralPath $objectRoot -Recurse -File -Filter 'VFCore_*.cpp.obj' | Where-Object { $_.FullName -match '\\VoxelForgeCore\\' }) $actualObjects = @($coreObjects | ForEach-Object { $_.Name } | Sort-Object -Unique) $missingObjects = @($expectedObjects | Where-Object { $actualObjects -notcontains $_ }) $unexpectedObjects = @($actualObjects | Where-Object { $expectedObjects -notcontains $_ }) if ($missingObjects.Count -gt 0 -or $unexpectedObjects.Count -gt 0 -or $coreObjects.Count -ne $manifestRows.Count) { throw "Core link input inventory mismatch: expected $($manifestRows.Count) shims, found $($coreObjects.Count); missing=[$($missingObjects -join ',')], extra=[$($unexpectedObjects -join ',')]." } $libTool = Find-VsTool 'lib.exe' if (Test-Path -LiteralPath $AuditLibrary -PathType Leaf) { Remove-Item -LiteralPath $AuditLibrary -Force } & $libTool '/nologo' ("/OUT:{0}" -f $AuditLibrary) @($coreObjects.FullName) if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $AuditLibrary -PathType Leaf)) { throw "Could not archive the exact UBT Core link inputs for symbol inspection (exit $LASTEXITCODE)." } & (Join-Path $PluginRoot 'Standalone\Core\CheckCoreSymbols.ps1') ` -LibraryPath $AuditLibrary -ResultPath $AuditResult $newCrashes = [Collections.Generic.List[string]]::new() foreach ($crashRoot in $crashRoots) { if (Test-Path -LiteralPath $crashRoot -PathType Container) { foreach ($crash in @(Get-ChildItem -LiteralPath $crashRoot -Directory -Filter 'UECC-*' -Recurse -Force)) { if (-not $crashBefore.ContainsKey($crash.FullName.ToLowerInvariant())) { [void]$newCrashes.Add($crash.FullName) } } } } if ($newCrashes.Count -gt 0) { throw "New UECC crash folders appeared during the BuildHost run: $($newCrashes -join '; ')" } Write-Host "Verified the five recorded asset hashes in the existing BuildHost Content copies." Write-Host "Core manifest shims linked and audited: $($coreObjects.Count)" Write-Host "Link-safety result: $AuditResult" Write-Host "Build log: $BuildLog"