summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorFrédéric Desbiens <[email protected]>2026-05-26 17:17:40 -0400
committerGitHub <[email protected]>2026-05-26 17:17:40 -0400
commit2c16114a450056103bacee0a3254a57dad7e1abd (patch)
tree5d6b68e52bcc550e3b08adeabc38f54918804d1d /scripts
parente031a3d506fdbc589fc6032eba618918e0c6719a (diff)
Added win64 ports of ThreadX and ThreadX SMP (#529)
Windows x64 port and regression suite This PR adds the Windows x64 (Win64) simulation port for both the standalone and SMP variants of ThreadX, along with the full CMake build and test infrastructure needed to run the regression suite on Windows. New ports Win64 standalone (ports/win64/vs_2022): self-contained Windows simulation port using Win32 threading primitives as virtual cores. Includes CMake integration, build/test scripts, and MSVC project files. Win64 SMP (ports/win64_smp/vs_2022): multi-core Windows simulation port. Supports up to 4 virtual cores backed by Windows host threads. Scheduler and timer improvements The initial port used coarse polling and synchronous SuspendThread/ResumeThread pairs throughout the scheduler hot path. Several rounds of optimization reduced the SMP regression suite runtime from ~150 s to ~78 s (-48%), with no regressions: - Replaced scheduler polling with an event-driven wake path; switched the simulated timer to one-shot rearming to eliminate catch-up ticks. - Skip SuspendThread when _tx_thread_preempt_disable != 0 (new suspension type 3) -- the primary optimization, yielding up to 7.9x speedup on preemption-heavy tests. - Skip SuspendThread when a thread is spinning on the Win32 critical section (suspension type 4), and fix a stale-TLS bug in _tx_win32_critical_section_obtain that could stamp mutex_access on the wrong virtual core. - Added a 2 ms scheduler event timeout (matching the Linux SMP port) to prevent stalls on any missed SetEvent. - Enabled high-resolution waitable timers (SetWaitableTimerEx) for accurate 100 Hz tick cadence. - Increased TX_WIN32_CONTENTION_PAUSE_COUNT from 64 to 256 to reduce SwitchToThread overhead under heavy CS contention. Build and test infrastructure - Hardened the Windows build wrapper (scripts/build_tx.ps1): invoke Ninja directly for Ninja build trees, fix timeout detection, add a default build timeout, and limit fallback replay to real timeout cases. - Added -Clean support to Windows test scripts to remove stale CTest state before each run. - Skip Visual Studio DevShell re-entry when the active MSVC environment already matches the requested architecture. - Fixed scripts/build_tx.sh (Linux) regression source generation: replaced brittle exact-string insertion with line-based matching so the interrupt dispatcher hook is inserted reliably for both simulator ports. Test suite updates - Introduced test/tx/regression/threadx_test_port.h with portable macros (TX_TEST_POINTER_WORD, TX_TEST_STORE_POINTER) for storing pointers in test arrays on 64-bit targets where ULONG remains 32-bit. - Adjusted pool-capacity and pointer-storage patterns in regression tests to use ALIGN_TYPE-sized slots, making the suite correct on 64-bit hosts. - Restored stricter event flag, sleep, and timer expectations now that port-level fixes make prior Windows accommodations unnecessary. - Tightened SMP watchdog and clean-build timeout defaults. Version metadata Updated Win32, Win64, and Win64 SMP port version strings to 6.5.1.202602. Co-authored-by: Copilot <[email protected]> Co-authored-by: Codex (gpt 5.5) <[email protected]>
Diffstat (limited to 'scripts')
-rw-r--r--scripts/build_smp.ps154
-rw-r--r--scripts/build_tx.ps158
-rw-r--r--scripts/test_smp.ps1120
-rw-r--r--scripts/test_tx.ps1121
-rw-r--r--scripts/tx_windows_common.ps1941
5 files changed, 1294 insertions, 0 deletions
diff --git a/scripts/build_smp.ps1 b/scripts/build_smp.ps1
new file mode 100644
index 00000000..c66a2beb
--- /dev/null
+++ b/scripts/build_smp.ps1
@@ -0,0 +1,54 @@
+[CmdletBinding()]
+param(
+ [AllowNull()]
+ [object]$Configuration = 'all',
+
+ [int]$Parallel = [Math]::Max(1, [Environment]::ProcessorCount),
+
+ [int]$BuildTimeoutSeconds = 180,
+
+ [string]$BuildDir,
+
+ [switch]$Clean
+)
+
+$ErrorActionPreference = 'Stop'
+. (Join-Path $PSScriptRoot 'tx_windows_common.ps1')
+
+$repoRoot = Split-Path -Parent $PSScriptRoot
+
+if (-not $BuildDir) {
+ $BuildDir = Join-Path $repoRoot 'build\tests\win64_smp'
+}
+
+$selectedConfigurations = Resolve-RegressionConfigurations -RequestedConfigurations $Configuration
+Write-Host "Selected configurations: $($selectedConfigurations -join ', ')"
+
+Enter-VisualStudioDevShell -VsArch 'amd64'
+
+foreach ($currentConfiguration in $selectedConfigurations) {
+ $currentBuildDirName = Get-RegressionBuildDirectoryName -ConfigurationName $currentConfiguration
+ $currentBuildDir = Join-Path $BuildDir $currentBuildDirName
+
+ if ($Clean) {
+ Remove-BuildDirectory -Path $currentBuildDir -RepoRoot $repoRoot
+ }
+
+ Remove-NinjaLock -Path $currentBuildDir
+
+ Write-Host "Configuring win64_smp / $currentConfiguration"
+ Invoke-NativeCommand -FilePath 'cmake' -Arguments @(
+ '-S', (Join-Path $repoRoot 'test\smp\cmake'),
+ '-B', $currentBuildDir,
+ '-G', 'Ninja',
+ '-DCMAKE_C_COMPILER_FORCED=TRUE',
+ '-DCMAKE_C_COMPILER_WORKS=TRUE',
+ '-DCMAKE_C_ABI_COMPILED=TRUE',
+ "-DCMAKE_BUILD_TYPE=$currentConfiguration",
+ '-DTHREADX_ARCH=win64',
+ '-DTHREADX_TOOLCHAIN=vs_2022'
+ )
+
+ Write-Host "Building win64_smp / $currentConfiguration"
+ Invoke-CMakeBuild -BuildDir $currentBuildDir -Parallel $Parallel -TimeoutSeconds $BuildTimeoutSeconds
+}
diff --git a/scripts/build_tx.ps1 b/scripts/build_tx.ps1
new file mode 100644
index 00000000..eeaa2222
--- /dev/null
+++ b/scripts/build_tx.ps1
@@ -0,0 +1,58 @@
+[CmdletBinding()]
+param(
+ [ValidateSet('win64', 'win32')]
+ [string]$Arch = 'win64',
+
+ [AllowNull()]
+ [object]$Configuration = 'all',
+
+ [int]$Parallel = [Math]::Max(1, [Environment]::ProcessorCount),
+
+ [int]$BuildTimeoutSeconds = 60,
+
+ [string]$BuildDir,
+
+ [switch]$Clean
+)
+
+$ErrorActionPreference = 'Stop'
+. (Join-Path $PSScriptRoot 'tx_windows_common.ps1')
+
+$repoRoot = Split-Path -Parent $PSScriptRoot
+$settings = Get-PortSettings -SelectedArch $Arch
+
+if (-not $BuildDir) {
+ $BuildDir = Join-Path $repoRoot "build\tests\$Arch"
+}
+
+$selectedConfigurations = Resolve-RegressionConfigurations -RequestedConfigurations $Configuration
+Write-Host "Selected configurations: $($selectedConfigurations -join ', ')"
+
+Enter-VisualStudioDevShell -VsArch $settings.VsArch
+
+foreach ($currentConfiguration in $selectedConfigurations) {
+ $currentBuildDirName = Get-RegressionBuildDirectoryName -ConfigurationName $currentConfiguration
+ $currentBuildDir = Join-Path $BuildDir $currentBuildDirName
+
+ if ($Clean) {
+ Remove-BuildDirectory -Path $currentBuildDir -RepoRoot $repoRoot
+ }
+
+ Remove-NinjaLock -Path $currentBuildDir
+
+ Write-Host "Configuring $Arch / $currentConfiguration"
+ Invoke-NativeCommand -FilePath 'cmake' -Arguments @(
+ '-S', (Join-Path $repoRoot 'test\tx\cmake'),
+ '-B', $currentBuildDir,
+ '-G', 'Ninja',
+ '-DCMAKE_C_COMPILER_FORCED=TRUE',
+ '-DCMAKE_C_COMPILER_WORKS=TRUE',
+ '-DCMAKE_C_ABI_COMPILED=TRUE',
+ "-DCMAKE_BUILD_TYPE=$currentConfiguration",
+ "-DTHREADX_ARCH=$($settings.ThreadXArch)",
+ "-DTHREADX_TOOLCHAIN=$($settings.ThreadXToolchain)"
+ )
+
+ Write-Host "Building $Arch / $currentConfiguration"
+ Invoke-CMakeBuild -BuildDir $currentBuildDir -Parallel $Parallel -TimeoutSeconds $BuildTimeoutSeconds
+}
diff --git a/scripts/test_smp.ps1 b/scripts/test_smp.ps1
new file mode 100644
index 00000000..e71b1081
--- /dev/null
+++ b/scripts/test_smp.ps1
@@ -0,0 +1,120 @@
+[CmdletBinding()]
+param(
+ [AllowNull()]
+ [object]$Configuration = 'all',
+
+ [int]$Parallel = 1,
+
+ [int]$RepeatFailCount = 2,
+
+ [int]$TestTimeoutSeconds = 45,
+
+ [switch]$CollectFailureDiagnostics = $true,
+
+ [string]$TestRegex,
+
+ [switch]$RerunFailedOnly,
+
+ [string]$BuildDir,
+
+ [switch]$Clean
+)
+
+$ErrorActionPreference = 'Stop'
+. (Join-Path $PSScriptRoot 'tx_windows_common.ps1')
+
+$repoRoot = Split-Path -Parent $PSScriptRoot
+
+if (-not $BuildDir) {
+ $BuildDir = Join-Path $repoRoot 'build\tests\win64_smp'
+}
+
+$selectedConfigurations = Resolve-RegressionConfigurations -RequestedConfigurations $Configuration
+Write-Host "Selected configurations: $($selectedConfigurations -join ', ')"
+
+Enter-VisualStudioDevShell -VsArch 'amd64'
+
+if ($Parallel -ne 1) {
+ Write-Warning 'Windows SMP simulator regression tests are timing-sensitive. Forcing -Parallel 1.'
+ $Parallel = 1
+}
+
+if ($TestRegex -and -not $PSBoundParameters.ContainsKey('TestTimeoutSeconds')) {
+ $TestTimeoutSeconds = 60
+ Write-Host "Targeted run detected; using per-test timeout of $TestTimeoutSeconds seconds."
+}
+
+$failedConfigurations = @()
+
+foreach ($currentConfiguration in $selectedConfigurations) {
+ $currentBuildDirName = Get-RegressionBuildDirectoryName -ConfigurationName $currentConfiguration
+ $currentBuildDir = Join-Path $BuildDir $currentBuildDirName
+ $currentTestingTemporaryDir = Join-Path $currentBuildDir 'Testing\Temporary'
+
+ try {
+ if ($Clean) {
+ $currentTestingDir = Join-Path $currentBuildDir 'Testing'
+ Remove-CtestTestingDirectory -Path $currentTestingDir
+ }
+
+ if (-not (Test-Path -LiteralPath $currentBuildDir)) {
+ throw "Build directory does not exist for win64_smp / ${currentConfiguration}: $currentBuildDir"
+ }
+
+ Remove-NinjaLock -Path $currentBuildDir
+ if (Test-Path -LiteralPath $currentTestingTemporaryDir) {
+ Remove-Item -LiteralPath (Join-Path $currentTestingTemporaryDir 'LastTest.log') -Force -ErrorAction SilentlyContinue
+ Remove-Item -LiteralPath (Join-Path $currentTestingTemporaryDir 'LastTestsFailed.log') -Force -ErrorAction SilentlyContinue
+ }
+
+ Write-Host "Testing win64_smp / $currentConfiguration"
+ $ctestArguments = @(
+ '--test-dir', $currentBuildDir,
+ '--output-on-failure',
+ '--timeout', $TestTimeoutSeconds.ToString(),
+ '-j', $Parallel.ToString()
+ )
+
+ if ($RepeatFailCount -gt 1) {
+ $ctestArguments += @('--repeat', "until-pass:$RepeatFailCount")
+ }
+
+ if ($TestRegex) {
+ $ctestArguments += @('-R', $TestRegex)
+ }
+
+ if ($RerunFailedOnly) {
+ $ctestArguments += '--rerun-failed'
+ }
+
+ Invoke-NativeCommand -FilePath 'ctest' -Arguments $ctestArguments
+ }
+ catch {
+ if ($CollectFailureDiagnostics -and (Test-Path -LiteralPath $currentBuildDir)) {
+ try {
+ Invoke-CtestFailureDiagnostics -BuildDir $currentBuildDir -TestingTemporaryDir $currentTestingTemporaryDir `
+ -TimeoutSeconds $TestTimeoutSeconds
+ }
+ catch {
+ Write-Warning "Failure diagnostics collection failed for ${currentConfiguration}: $($_.Exception.Message)"
+ }
+ }
+
+ $failedConfigurations += @{
+ Configuration = $currentConfiguration
+ Message = $_.Exception.Message
+ }
+
+ Write-Warning "Configuration failed: $currentConfiguration"
+ }
+}
+
+if ($failedConfigurations.Count -gt 0) {
+ Write-Host ''
+ Write-Host 'Configuration failure summary:'
+ foreach ($failedConfiguration in $failedConfigurations) {
+ Write-Host "- $($failedConfiguration.Configuration): $($failedConfiguration.Message)"
+ }
+
+ throw "One or more configurations failed: $($failedConfigurations.Configuration -join ', ')"
+}
diff --git a/scripts/test_tx.ps1 b/scripts/test_tx.ps1
new file mode 100644
index 00000000..a5607ed7
--- /dev/null
+++ b/scripts/test_tx.ps1
@@ -0,0 +1,121 @@
+[CmdletBinding()]
+param(
+ [ValidateSet('win64', 'win32')]
+ [string]$Arch = 'win64',
+
+ [AllowNull()]
+ [object]$Configuration = 'all',
+
+ [int]$Parallel = [Math]::Max(1, [Environment]::ProcessorCount),
+
+ [int]$RepeatFailCount = 1,
+
+ [int]$TestTimeoutSeconds = 20,
+
+ [switch]$CollectFailureDiagnostics = $true,
+
+ [string]$TestRegex,
+
+ [switch]$RerunFailedOnly,
+
+ [string]$BuildDir,
+
+ [switch]$Clean
+)
+
+$ErrorActionPreference = 'Stop'
+. (Join-Path $PSScriptRoot 'tx_windows_common.ps1')
+
+$repoRoot = Split-Path -Parent $PSScriptRoot
+$settings = Get-PortSettings -SelectedArch $Arch
+
+if (-not $BuildDir) {
+ $BuildDir = Join-Path $repoRoot "build\tests\$Arch"
+}
+
+$selectedConfigurations = Resolve-RegressionConfigurations -RequestedConfigurations $Configuration
+Write-Host "Selected configurations: $($selectedConfigurations -join ', ')"
+
+Enter-VisualStudioDevShell -VsArch $settings.VsArch
+
+if (($settings.ThreadXArch -eq 'win32') -or ($settings.ThreadXArch -eq 'win64')) {
+ if ($Parallel -ne 1) {
+ Write-Warning "Windows simulator regression tests are timing-sensitive under concurrent ctest execution. Forcing -Parallel 1."
+ $Parallel = 1
+ }
+}
+
+$failedConfigurations = @()
+
+foreach ($currentConfiguration in $selectedConfigurations) {
+ $currentBuildDirName = Get-RegressionBuildDirectoryName -ConfigurationName $currentConfiguration
+ $currentBuildDir = Join-Path $BuildDir $currentBuildDirName
+ $currentTestingTemporaryDir = Join-Path $currentBuildDir 'Testing\Temporary'
+
+ try {
+ if ($Clean) {
+ $currentTestingDir = Join-Path $currentBuildDir 'Testing'
+ Remove-CtestTestingDirectory -Path $currentTestingDir
+ }
+
+ if (-not (Test-Path -LiteralPath $currentBuildDir)) {
+ throw "Build directory does not exist for $Arch / ${currentConfiguration}: $currentBuildDir"
+ }
+
+ Remove-NinjaLock -Path $currentBuildDir
+ if (Test-Path -LiteralPath $currentTestingTemporaryDir) {
+ Remove-Item -LiteralPath (Join-Path $currentTestingTemporaryDir 'LastTest.log') -Force -ErrorAction SilentlyContinue
+ Remove-Item -LiteralPath (Join-Path $currentTestingTemporaryDir 'LastTestsFailed.log') -Force -ErrorAction SilentlyContinue
+ }
+
+ Write-Host "Testing $Arch / $currentConfiguration"
+ $ctestArguments = @(
+ '--test-dir', $currentBuildDir,
+ '--output-on-failure',
+ '--timeout', $TestTimeoutSeconds.ToString(),
+ '-j', $Parallel.ToString()
+ )
+
+ if ($RepeatFailCount -gt 1) {
+ $ctestArguments += @('--repeat', "until-pass:$RepeatFailCount")
+ }
+
+ if ($TestRegex) {
+ $ctestArguments += @('-R', $TestRegex)
+ }
+
+ if ($RerunFailedOnly) {
+ $ctestArguments += '--rerun-failed'
+ }
+
+ Invoke-NativeCommand -FilePath 'ctest' -Arguments $ctestArguments
+ }
+ catch {
+ if ($CollectFailureDiagnostics -and (Test-Path -LiteralPath $currentBuildDir)) {
+ try {
+ Invoke-CtestFailureDiagnostics -BuildDir $currentBuildDir -TestingTemporaryDir $currentTestingTemporaryDir `
+ -TimeoutSeconds $TestTimeoutSeconds
+ }
+ catch {
+ Write-Warning "Failure diagnostics collection failed for ${currentConfiguration}: $($_.Exception.Message)"
+ }
+ }
+
+ $failedConfigurations += @{
+ Configuration = $currentConfiguration
+ Message = $_.Exception.Message
+ }
+
+ Write-Warning "Configuration failed: $currentConfiguration"
+ }
+}
+
+if ($failedConfigurations.Count -gt 0) {
+ Write-Host ''
+ Write-Host 'Configuration failure summary:'
+ foreach ($failedConfiguration in $failedConfigurations) {
+ Write-Host "- $($failedConfiguration.Configuration): $($failedConfiguration.Message)"
+ }
+
+ throw "One or more configurations failed: $($failedConfigurations.Configuration -join ', ')"
+}
diff --git a/scripts/tx_windows_common.ps1 b/scripts/tx_windows_common.ps1
new file mode 100644
index 00000000..7b712d4a
--- /dev/null
+++ b/scripts/tx_windows_common.ps1
@@ -0,0 +1,941 @@
+[CmdletBinding()]
+param()
+
+$ErrorActionPreference = 'Stop'
+
+function Invoke-NativeCommand {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$FilePath,
+
+ [Parameter()]
+ [string[]]$Arguments = @()
+ )
+
+ & $FilePath @Arguments
+ if ($LASTEXITCODE -ne 0) {
+ throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $($Arguments -join ' ')"
+ }
+}
+
+function Get-CommandPathIfAvailable {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$CommandName
+ )
+
+ $command = Get-Command $CommandName -ErrorAction SilentlyContinue
+ if ($null -eq $command) {
+ return $null
+ }
+
+ return $command.Source
+}
+
+function Get-PortSettings {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$SelectedArch
+ )
+
+ switch ($SelectedArch) {
+ 'win32' {
+ return @{
+ ThreadXArch = 'win32'
+ ThreadXToolchain = 'vs_2019'
+ VsArch = 'x86'
+ }
+ }
+ 'win64' {
+ return @{
+ ThreadXArch = 'win64'
+ ThreadXToolchain = 'vs_2022'
+ VsArch = 'amd64'
+ }
+ }
+ default {
+ throw "Unsupported architecture: $SelectedArch"
+ }
+ }
+}
+
+function Get-RegressionConfigurations {
+ return @(
+ 'default_build_coverage',
+ 'disable_notify_callbacks_build',
+ 'stack_checking_build',
+ 'stack_checking_rand_fill_build',
+ 'trace_build'
+ )
+}
+
+function Resolve-RegressionConfigurations {
+ param(
+ [Parameter(Mandatory = $false)]
+ [AllowNull()]
+ [AllowEmptyCollection()]
+ [object]$RequestedConfigurations = 'all'
+ )
+
+ $allConfigurations = Get-RegressionConfigurations
+ $resolvedConfigurations = @()
+
+ if ($null -eq $RequestedConfigurations) {
+ $resolvedConfigurations = @('all')
+ }
+ elseif ($RequestedConfigurations -is [System.Array]) {
+ foreach ($requestedConfiguration in $RequestedConfigurations) {
+ if ($null -ne $requestedConfiguration) {
+ $resolvedConfigurations += [string]$requestedConfiguration
+ }
+ }
+ }
+ else {
+ $resolvedConfigurations = @([string]$RequestedConfigurations)
+ }
+
+ $normalizedConfigurations = @()
+ foreach ($requestedConfiguration in $resolvedConfigurations) {
+ foreach ($configurationPart in ($requestedConfiguration -split ',')) {
+ $trimmedConfiguration = $configurationPart.Trim()
+ if ($trimmedConfiguration.Length -gt 0) {
+ $normalizedConfigurations += $trimmedConfiguration
+ }
+ }
+ }
+
+ if (($normalizedConfigurations.Count -eq 0) -or ($normalizedConfigurations -contains 'all')) {
+ return $allConfigurations
+ }
+
+ foreach ($normalizedConfiguration in $normalizedConfigurations) {
+ if ($allConfigurations -notcontains $normalizedConfiguration) {
+ throw "Unsupported configuration: $normalizedConfiguration"
+ }
+ }
+
+ return $normalizedConfigurations
+}
+
+function Get-RegressionBuildDirectoryName {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$ConfigurationName
+ )
+
+ switch ($ConfigurationName) {
+ 'default_build_coverage' {
+ return 'dbc'
+ }
+ 'disable_notify_callbacks_build' {
+ return 'dnc'
+ }
+ 'stack_checking_build' {
+ return 'sc'
+ }
+ 'stack_checking_rand_fill_build' {
+ return 'scrf'
+ }
+ 'trace_build' {
+ return 'tr'
+ }
+ default {
+ throw "Unsupported configuration: $ConfigurationName"
+ }
+ }
+}
+
+function Enter-VisualStudioDevShell {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$VsArch
+ )
+
+ $targetArch = switch ($VsArch) {
+ 'amd64' { 'x64' }
+ 'x86' { 'x86' }
+ default { $VsArch }
+ }
+
+ if ((Get-Command cl -ErrorAction SilentlyContinue) -and ($env:VSCMD_ARG_TGT_ARCH -eq $targetArch)) {
+ return
+ }
+
+ $vsWherePath = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
+ if (-not (Test-Path -LiteralPath $vsWherePath)) {
+ throw "Unable to locate vswhere.exe at $vsWherePath"
+ }
+
+ $installationPath = & $vsWherePath -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
+ if (-not $installationPath) {
+ throw 'Unable to locate a Visual Studio 2022 installation with MSVC build tools.'
+ }
+
+ $launchScript = Join-Path $installationPath 'Common7\Tools\Launch-VsDevShell.ps1'
+ if (-not (Test-Path -LiteralPath $launchScript)) {
+ throw "Unable to locate Launch-VsDevShell.ps1 at $launchScript"
+ }
+
+ $env:VSCMD_SKIP_SENDTELEMETRY = '1'
+ & $launchScript -VsInstallationPath $installationPath -Arch $VsArch -HostArch amd64 -SkipAutomaticLocation | Out-Null
+
+ if (-not (Get-Command cl -ErrorAction SilentlyContinue)) {
+ throw 'MSVC compiler environment was not activated successfully.'
+ }
+}
+
+function Remove-BuildDirectory {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Path,
+
+ [Parameter(Mandatory = $true)]
+ [string]$RepoRoot
+ )
+
+ $fullRepoRoot = [System.IO.Path]::GetFullPath($RepoRoot)
+ $fullPath = [System.IO.Path]::GetFullPath($Path)
+
+ if (-not $fullPath.StartsWith($fullRepoRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
+ throw "Refusing to remove a directory outside the repository: $fullPath"
+ }
+
+ if (Test-Path -LiteralPath $fullPath) {
+ try {
+ Remove-Item -LiteralPath $fullPath -Recurse -Force -ErrorAction Stop
+ return
+ } catch {
+ Write-Warning "Failed to remove build directory '$fullPath': $($_.Exception.Message)"
+ }
+
+ Get-ChildItem -LiteralPath $fullPath -Force -Recurse -ErrorAction SilentlyContinue | ForEach-Object {
+ try {
+ if (($_.Attributes -band [System.IO.FileAttributes]::ReadOnly) -ne 0) {
+ $_.Attributes = ($_.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly))
+ }
+ } catch {
+ }
+ }
+
+ try {
+ Remove-Item -LiteralPath $fullPath -Recurse -Force -ErrorAction Stop
+ } catch {
+ Write-Warning "Proceeding with partially cleaned build directory '$fullPath': $($_.Exception.Message)"
+ }
+ }
+}
+
+function Remove-CtestTestingDirectory {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Path
+ )
+
+ if (-not (Test-Path -LiteralPath $Path)) {
+ return
+ }
+
+ try {
+ Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop
+ return
+ } catch {
+ Write-Warning "Failed to remove CTest directory '$Path': $($_.Exception.Message)"
+ }
+
+ Get-ChildItem -LiteralPath $Path -Force -Recurse -ErrorAction SilentlyContinue | ForEach-Object {
+ try {
+ if (($_.Attributes -band [System.IO.FileAttributes]::ReadOnly) -ne 0) {
+ $_.Attributes = ($_.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly))
+ }
+ } catch {
+ }
+ }
+
+ try {
+ Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop
+ } catch {
+ Write-Warning "Proceeding with partially cleaned CTest directory '$Path': $($_.Exception.Message)"
+ }
+}
+
+function Remove-NinjaLock {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Path
+ )
+
+ $ninjaLockPath = Join-Path $Path '.ninja_lock'
+ if (Test-Path -LiteralPath $ninjaLockPath) {
+ Remove-Item -LiteralPath $ninjaLockPath -Force
+ }
+}
+
+function Get-WindowsDebuggerPath {
+ $debuggerPath = Get-CommandPathIfAvailable -CommandName 'cdb.exe'
+ if ($debuggerPath) {
+ return $debuggerPath
+ }
+
+ $candidatePaths = @(
+ (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Debuggers\x64\cdb.exe'),
+ (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Debuggers\x86\cdb.exe')
+ )
+
+ foreach ($candidatePath in $candidatePaths) {
+ if (Test-Path -LiteralPath $candidatePath) {
+ return $candidatePath
+ }
+ }
+
+ return $null
+}
+
+function Get-SanitizedFileName {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Name
+ )
+
+ $safeName = [regex]::Replace($Name, '[<>:"/\\|?*]', '_')
+ $safeName = $safeName -replace '\s+', '_'
+ return $safeName
+}
+
+function Initialize-MinidumpSupport {
+ if ($null -ne ('ThreadX.WindowsMiniDump' -as [type])) {
+ return
+ }
+
+ Add-Type -TypeDefinition @'
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace ThreadX
+{
+ public static class WindowsMiniDump
+ {
+ [DllImport("Dbghelp.dll", SetLastError = true)]
+ private static extern bool MiniDumpWriteDump(
+ IntPtr hProcess,
+ uint processId,
+ IntPtr hFile,
+ uint dumpType,
+ IntPtr exceptionParam,
+ IntPtr userStreamParam,
+ IntPtr callbackParam);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern IntPtr OpenProcess(uint desiredAccess, bool inheritHandle, int processId);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool CloseHandle(IntPtr handle);
+
+ private const uint ProcessQueryInformation = 0x0400U;
+ private const uint ProcessVmRead = 0x0010U;
+ private const uint ProcessDupHandle = 0x0040U;
+
+ public static bool WriteDump(int processId, string dumpPath, uint dumpType, out int errorCode)
+ {
+ IntPtr processHandle = OpenProcess(ProcessQueryInformation | ProcessVmRead | ProcessDupHandle, false, processId);
+ if (processHandle == IntPtr.Zero)
+ {
+ errorCode = Marshal.GetLastWin32Error();
+ return false;
+ }
+
+ try
+ {
+ using (FileStream dumpStream = new FileStream(dumpPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read))
+ {
+ bool success = MiniDumpWriteDump(
+ processHandle,
+ unchecked((uint)processId),
+ dumpStream.SafeFileHandle.DangerousGetHandle(),
+ dumpType,
+ IntPtr.Zero,
+ IntPtr.Zero,
+ IntPtr.Zero);
+
+ errorCode = success ? 0 : Marshal.GetLastWin32Error();
+ return success;
+ }
+ }
+ finally
+ {
+ CloseHandle(processHandle);
+ }
+ }
+ }
+}
+'@
+}
+
+function Wait-FileReadable {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Path,
+
+ [Parameter()]
+ [int]$TimeoutSeconds = 10
+ )
+
+ $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
+ while ((Get-Date) -lt $deadline) {
+ if (-not (Test-Path -LiteralPath $Path)) {
+ Start-Sleep -Milliseconds 200
+ continue
+ }
+
+ try {
+ $fileStream = [System.IO.File]::Open($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
+ $fileStream.Dispose()
+ return $true
+ }
+ catch {
+ Start-Sleep -Milliseconds 200
+ }
+ }
+
+ return $false
+}
+
+function Get-CtestTestMetadata {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$BuildDir
+ )
+
+ $ctestOutput = & ctest --test-dir $BuildDir --show-only=json-v1
+ if ($LASTEXITCODE -ne 0) {
+ throw "Unable to enumerate ctest metadata in $BuildDir"
+ }
+
+ return (($ctestOutput -join [Environment]::NewLine) | ConvertFrom-Json).tests
+}
+
+function Get-CtestFailedTestNames {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$TestingTemporaryDir
+ )
+
+ $lastFailedPath = Join-Path $TestingTemporaryDir 'LastTestsFailed.log'
+ if (-not (Test-Path -LiteralPath $lastFailedPath)) {
+ return @()
+ }
+
+ $failedTestNames = @()
+ foreach ($logLine in (Get-Content -LiteralPath $lastFailedPath)) {
+ if ([string]::IsNullOrWhiteSpace($logLine)) {
+ continue
+ }
+
+ if ($logLine -match '^\s*\d+:(?<name>.+)\s*$') {
+ $failedTestNames += $Matches['name'].Trim()
+ }
+ else {
+ $failedTestNames += $logLine.Trim()
+ }
+ }
+
+ return $failedTestNames
+}
+
+function Invoke-ProcessDumpCapture {
+ param(
+ [Parameter(Mandatory = $true)]
+ [int]$ProcessId,
+
+ [Parameter(Mandatory = $true)]
+ [string]$DumpPath,
+
+ [Parameter()]
+ [int]$TimeoutSeconds = 15
+ )
+
+ $outputDirectory = Split-Path -Parent $DumpPath
+ if (-not (Test-Path -LiteralPath $outputDirectory)) {
+ New-Item -ItemType Directory -Path $outputDirectory | Out-Null
+ }
+
+ Remove-Item -LiteralPath $DumpPath -Force -ErrorAction SilentlyContinue
+
+ Initialize-MinidumpSupport
+ $dumpType = [uint32]0x00001006
+ $errorCode = 0
+ $dumpCaptured = [ThreadX.WindowsMiniDump]::WriteDump($ProcessId, $DumpPath, $dumpType, [ref]$errorCode)
+
+ if (-not $dumpCaptured) {
+ Write-Warning "MiniDumpWriteDump failed for PID ${ProcessId} with Win32 error $errorCode"
+ return $false
+ }
+
+ return (Test-Path -LiteralPath $DumpPath)
+}
+
+function Invoke-DumpStackAnalysis {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$DumpPath,
+
+ [Parameter(Mandatory = $true)]
+ [string]$OutputBasePath,
+
+ [Parameter()]
+ [string]$SymbolPath,
+
+ [Parameter()]
+ [int]$TimeoutSeconds = 15
+ )
+
+ if (-not (Test-Path -LiteralPath $DumpPath)) {
+ Write-Warning "Skipping dump analysis because the dump file was not created: $DumpPath"
+ return $false
+ }
+
+ if (-not (Wait-FileReadable -Path $DumpPath)) {
+ Write-Warning "Skipping dump analysis because the dump file is not readable yet: $DumpPath"
+ return $false
+ }
+
+ $debuggerPath = Get-WindowsDebuggerPath
+ if (-not $debuggerPath) {
+ Write-Warning 'Skipping dump analysis because cdb.exe is not available.'
+ return $false
+ }
+
+ $outputDirectory = Split-Path -Parent $OutputBasePath
+ if (-not (Test-Path -LiteralPath $outputDirectory)) {
+ New-Item -ItemType Directory -Path $outputDirectory | Out-Null
+ }
+
+ $stdoutPath = "${OutputBasePath}.stdout.txt"
+ $stderrPath = "${OutputBasePath}.stderr.txt"
+ $commandFilePath = "${OutputBasePath}.commands.txt"
+ Set-Content -LiteralPath $commandFilePath -Value @(
+ '!runaway 7'
+ '~* kb 200'
+ 'q'
+ ) -Encoding Ascii
+ $cdbArguments = @(
+ '-lines',
+ '-z', $DumpPath
+ )
+
+ if ($SymbolPath) {
+ $cdbArguments += @('-y', $SymbolPath)
+ }
+
+ $cdbArguments += @('-cf', $commandFilePath)
+ $cdbProcess = Start-Process -FilePath $debuggerPath -ArgumentList $cdbArguments -PassThru -NoNewWindow `
+ -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath
+
+ try {
+ $cdbProcess | Wait-Process -Timeout $TimeoutSeconds -ErrorAction Stop
+ }
+ catch {
+ if (-not $cdbProcess.HasExited) {
+ $null = Start-Process -FilePath 'taskkill.exe' -ArgumentList @('/PID', $cdbProcess.Id.ToString(), '/T', '/F') `
+ -WindowStyle Hidden -Wait -PassThru
+ }
+ }
+
+ return $true
+}
+
+function Invoke-ProcessWithTimeout {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$FilePath,
+
+ [Parameter()]
+ [string[]]$Arguments = @(),
+
+ [Parameter()]
+ [int]$TimeoutSeconds = 0,
+
+ [Parameter()]
+ [string]$WorkingDirectory,
+
+ [Parameter()]
+ [string]$RedirectStandardOutputPath,
+
+ [Parameter()]
+ [string]$RedirectStandardErrorPath,
+
+ [Parameter()]
+ [scriptblock]$OnTimeout,
+
+ [Parameter()]
+ [scriptblock]$PostTimeout
+ )
+
+ $argumentList = @()
+ foreach ($argument in $Arguments) {
+ if ($argument -match '\s|"') {
+ $argumentList += '"' + ($argument -replace '"', '\"') + '"'
+ }
+ else {
+ $argumentList += $argument
+ }
+ }
+
+ $startProcessParameters = @{
+ FilePath = $FilePath
+ NoNewWindow = $true
+ PassThru = $true
+ }
+
+ if ($argumentList.Count -gt 0) {
+ $startProcessParameters['ArgumentList'] = $argumentList
+ }
+
+ if ($WorkingDirectory) {
+ $startProcessParameters['WorkingDirectory'] = $WorkingDirectory
+ }
+
+ if ($RedirectStandardOutputPath) {
+ $startProcessParameters['RedirectStandardOutput'] = $RedirectStandardOutputPath
+ }
+
+ if ($RedirectStandardErrorPath) {
+ $startProcessParameters['RedirectStandardError'] = $RedirectStandardErrorPath
+ }
+
+ $process = Start-Process @startProcessParameters
+ if ($TimeoutSeconds -le 0) {
+ $process | Wait-Process
+ $completed = $true
+ }
+ else {
+ try {
+ $process | Wait-Process -Timeout $TimeoutSeconds -ErrorAction Stop
+ $completed = $true
+ }
+ catch {
+ $completed = $false
+ }
+ }
+
+ if (-not $completed) {
+ if ($null -ne $OnTimeout) {
+ & $OnTimeout $process
+ }
+
+ $null = Start-Process -FilePath 'taskkill.exe' -ArgumentList @('/PID', $process.Id.ToString(), '/T', '/F') -WindowStyle Hidden -Wait -PassThru
+
+ if ($null -ne $PostTimeout) {
+ & $PostTimeout $process
+ }
+
+ return @{
+ Completed = $false
+ ExitCode = $null
+ ProcessId = $process.Id
+ }
+ }
+
+ $process.Refresh()
+ return @{
+ Completed = $true
+ ExitCode = $process.ExitCode
+ ProcessId = $process.Id
+ }
+}
+
+function Invoke-CtestFailureDiagnostics {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$BuildDir,
+
+ [Parameter(Mandatory = $true)]
+ [string]$TestingTemporaryDir,
+
+ [Parameter(Mandatory = $true)]
+ [int]$TimeoutSeconds
+ )
+
+ $failedTestNames = Get-CtestFailedTestNames -TestingTemporaryDir $TestingTemporaryDir
+ if ($failedTestNames.Count -eq 0) {
+ Write-Warning "No failed tests were recorded in $TestingTemporaryDir"
+ return
+ }
+
+ $testMetadataList = Get-CtestTestMetadata -BuildDir $BuildDir
+ $testMetadataMap = @{}
+ foreach ($testMetadata in $testMetadataList) {
+ $testMetadataMap[$testMetadata.name] = $testMetadata
+ }
+
+ $diagnosticsRoot = Join-Path $TestingTemporaryDir 'FailureDiagnostics'
+ if (-not (Test-Path -LiteralPath $diagnosticsRoot)) {
+ New-Item -ItemType Directory -Path $diagnosticsRoot | Out-Null
+ }
+
+ foreach ($failedTestName in $failedTestNames) {
+ if (-not $testMetadataMap.ContainsKey($failedTestName)) {
+ Write-Warning "Unable to locate ctest metadata for failed test: $failedTestName"
+ continue
+ }
+
+ $testMetadata = $testMetadataMap[$failedTestName]
+ if (($null -eq $testMetadata.command) -or ($testMetadata.command.Count -eq 0)) {
+ Write-Warning "No executable command was recorded for failed test: $failedTestName"
+ continue
+ }
+
+ $testArguments = @()
+ if ($testMetadata.command.Count -gt 1) {
+ $testArguments = @($testMetadata.command[1..($testMetadata.command.Count - 1)])
+ }
+
+ $safeTestName = Get-SanitizedFileName -Name $failedTestName
+ $stdoutPath = Join-Path $diagnosticsRoot "${safeTestName}.stdout.txt"
+ $stderrPath = Join-Path $diagnosticsRoot "${safeTestName}.stderr.txt"
+ $debugOutputBasePath = Join-Path $diagnosticsRoot "${safeTestName}.cdb"
+ $workingDirectory = $null
+ $symbolDirectory = Split-Path -Parent $testMetadata.command[0]
+
+ if ($null -ne $testMetadata.properties) {
+ foreach ($testProperty in $testMetadata.properties) {
+ if ($testProperty.name -eq 'WORKING_DIRECTORY') {
+ $workingDirectory = $testProperty.value
+ break
+ }
+ }
+ }
+
+ Write-Warning "Collecting failure diagnostics for $failedTestName"
+ $dumpPath = '{0}.{1}.dmp' -f $debugOutputBasePath, ([DateTime]::UtcNow.ToString('yyyyMMddHHmmssfff'))
+ $testResult = Invoke-ProcessWithTimeout -FilePath $testMetadata.command[0] -Arguments $testArguments `
+ -TimeoutSeconds $TimeoutSeconds -WorkingDirectory $workingDirectory -RedirectStandardOutputPath $stdoutPath `
+ -RedirectStandardErrorPath $stderrPath -OnTimeout {
+ param($timedOutProcess)
+ Invoke-ProcessDumpCapture -ProcessId $timedOutProcess.Id -DumpPath $dumpPath | Out-Null
+ } -PostTimeout {
+ param($timedOutProcess)
+ if (Test-Path -LiteralPath $dumpPath) {
+ Invoke-DumpStackAnalysis -DumpPath $dumpPath -OutputBasePath $debugOutputBasePath -SymbolPath $symbolDirectory | Out-Null
+ }
+ }
+
+ if (-not $testResult.Completed) {
+ Write-Warning "Timeout diagnostics were captured for $failedTestName under $diagnosticsRoot"
+ continue
+ }
+
+ Write-Warning "Replay finished for $failedTestName with exit code $($testResult.ExitCode). Output was saved under $diagnosticsRoot"
+ }
+}
+
+function Test-IsNinjaBuildDirectory {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$BuildDir
+ )
+
+ return (Test-Path -LiteralPath (Join-Path $BuildDir 'build.ninja'))
+}
+
+function Get-NinjaBuildStatements {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$BuildDir
+ )
+
+ $buildNinjaPath = Join-Path $BuildDir 'build.ninja'
+ if (-not (Test-Path -LiteralPath $buildNinjaPath)) {
+ throw "Unable to locate build.ninja in $BuildDir"
+ }
+
+ return Get-Content -LiteralPath $buildNinjaPath
+}
+
+function New-NinjaRspFile {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$BuildDir,
+
+ [Parameter(Mandatory = $true)]
+ [string]$RspRelativePath
+ )
+
+ $buildStatements = Get-NinjaBuildStatements -BuildDir $BuildDir
+ $rspLine = ' RSP_FILE = ' + $RspRelativePath
+ $rspIndex = -1
+
+ for ($index = 0; $index -lt $buildStatements.Count; $index++) {
+ if ($buildStatements[$index] -eq $rspLine) {
+ $rspIndex = $index
+ break
+ }
+ }
+
+ if ($rspIndex -lt 0) {
+ throw "Unable to locate RSP_FILE entry for $RspRelativePath in build.ninja."
+ }
+
+ $buildIndex = -1
+ for ($index = $rspIndex; $index -ge 0; $index--) {
+ if ($buildStatements[$index].StartsWith('build ')) {
+ $buildIndex = $index
+ break
+ }
+ }
+
+ if ($buildIndex -lt 0) {
+ throw "Unable to locate the build statement that owns $RspRelativePath."
+ }
+
+ $buildLine = $buildStatements[$buildIndex]
+ if ($buildLine -notmatch '^build\s+\S+:\s+\S+\s+(.+)$') {
+ throw "Unable to parse build statement for $RspRelativePath."
+ }
+
+ $rspContents = ($Matches[1] -split '\s+') -join [Environment]::NewLine
+ $rspPath = Join-Path $BuildDir $RspRelativePath
+ $rspParent = Split-Path -Parent $rspPath
+
+ if (-not (Test-Path -LiteralPath $rspParent)) {
+ New-Item -ItemType Directory -Path $rspParent | Out-Null
+ }
+
+ Set-Content -LiteralPath $rspPath -Value $rspContents
+}
+
+function Ensure-NinjaRspFiles {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$BuildDir,
+
+ [Parameter(Mandatory = $true)]
+ [string]$CommandLine
+ )
+
+ $rspMatches = [regex]::Matches($CommandLine, '@(?<path>[^\s"]+\.rsp)')
+ foreach ($rspMatch in $rspMatches) {
+ $rspRelativePath = $rspMatch.Groups['path'].Value
+ $rspPath = Join-Path $BuildDir $rspRelativePath
+ if (-not (Test-Path -LiteralPath $rspPath)) {
+ New-NinjaRspFile -BuildDir $BuildDir -RspRelativePath $rspRelativePath
+ }
+ }
+}
+
+function Get-PendingNinjaCommands {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$BuildDir
+ )
+
+ $commandLines = & ninja -C $BuildDir -t commands
+ if ($LASTEXITCODE -ne 0) {
+ throw "Unable to enumerate pending Ninja commands in $BuildDir"
+ }
+
+ return $commandLines | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
+}
+
+function Invoke-NinjaFallbackBuild {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$BuildDir
+ )
+
+ $pendingCommands = Get-PendingNinjaCommands -BuildDir $BuildDir
+ if ($pendingCommands.Count -eq 0) {
+ return
+ }
+
+ Push-Location $BuildDir
+ try {
+ foreach ($pendingCommand in $pendingCommands) {
+ Ensure-NinjaRspFiles -BuildDir $BuildDir -CommandLine $pendingCommand
+
+ $commandToRun = $pendingCommand -replace '\s/showIncludes(?=\s|$)', ''
+
+ if ($commandToRun -match '^[^ ]*cmd(?:\.exe)?\s+/C\s+"(?<inner>.*)"\s*$') {
+ & cmd.exe /C $Matches['inner']
+ }
+ else {
+ & cmd.exe /C $commandToRun
+ }
+
+ if ($LASTEXITCODE -ne 0) {
+ throw "Fallback Ninja command failed with exit code ${LASTEXITCODE}: $pendingCommand"
+ }
+ }
+ }
+ finally {
+ Pop-Location
+ }
+}
+
+function Invoke-CMakeBuild {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$BuildDir,
+
+ [Parameter(Mandatory = $true)]
+ [int]$Parallel,
+
+ [Parameter()]
+ [int]$TimeoutSeconds = 0
+ )
+
+ Remove-NinjaLock -Path $BuildDir
+ $isNinjaBuild = Test-IsNinjaBuildDirectory -BuildDir $BuildDir
+
+ if ($TimeoutSeconds -le 0) {
+ if ($isNinjaBuild) {
+ Invoke-NativeCommand -FilePath 'ninja' -Arguments @(
+ '-C', $BuildDir,
+ '-j', $Parallel.ToString()
+ )
+ }
+ else {
+ Invoke-NativeCommand -FilePath 'cmake' -Arguments @(
+ '--build', $BuildDir,
+ '--parallel', $Parallel.ToString()
+ )
+ }
+ return
+ }
+
+ if ($isNinjaBuild) {
+ $buildToolName = 'Ninja'
+ $buildResult = Invoke-ProcessWithTimeout -FilePath 'ninja' -Arguments @(
+ '-C', $BuildDir,
+ '-j', $Parallel.ToString()
+ ) -TimeoutSeconds $TimeoutSeconds
+ }
+ else {
+ $buildToolName = 'CMake'
+ $buildResult = Invoke-ProcessWithTimeout -FilePath 'cmake' -Arguments @(
+ '--build', $BuildDir,
+ '--parallel', $Parallel.ToString()
+ ) -TimeoutSeconds $TimeoutSeconds
+ }
+
+ if ($buildResult.Completed -and ($buildResult.ExitCode -eq 0)) {
+ return
+ }
+
+ if (-not $isNinjaBuild) {
+ if (-not $buildResult.Completed) {
+ throw "$buildToolName build timed out after $TimeoutSeconds seconds in $BuildDir"
+ }
+
+ throw "$buildToolName build failed with exit code $($buildResult.ExitCode) in $BuildDir"
+ }
+
+ if ($buildResult.Completed) {
+ throw "$buildToolName build failed with exit code $($buildResult.ExitCode) in $BuildDir"
+ }
+
+ Write-Warning "$buildToolName build timed out after $TimeoutSeconds seconds in $BuildDir. Replaying pending Ninja commands from PowerShell."
+
+ Remove-NinjaLock -Path $BuildDir
+ Invoke-NinjaFallbackBuild -BuildDir $BuildDir
+}