Files
filex/scripts/fx_windows_common.ps1
T
Frédéric DesbiensandCopilot 7cbe022bab Added Win64/VS 2022 port for FileX (#94)
- New port: ports/win64/vs_2022/inc/fx_port.h
  - Based on the Win32 port; updated ALIGN_TYPE to ULONG64 for 64-bit
    pointers, added FX_REGRESSION_TEST hooks from the Linux port, and
    bumped the port name string to Win64/Visual.
- New CMake infrastructure: ports/win64/vs_2022/CMakeLists.txt
  - Exposes only the inc/ directory (port is header-only).
- Updated test/cmake/CMakeLists.txt
  - Added MSVC guards: skip -m32 / GCC coverage flags; locate VS 2022
    toolchain; build ThreadX from source alongside FileX via
    THREADX_SOURCE_DIR.
- Updated test/cmake/regression/CMakeLists.txt
  - Added MSVC-compatible compile options (/W3 /Zi instead of -Wall
    -fprofile-arcs); added win64_compat include path for standalone +
    MSVC builds; added per-test TIMEOUT overrides for the two inherently
    long-running fault-tolerant tests on Windows.
- Updated test/cmake/samples/CMakeLists.txt
  - Added MSVC-compatible flag handling.
- New PowerShell scripts: scripts/build_fx.ps1, scripts/test_fx.ps1,
  scripts/fx_windows_common.ps1
  - Support all 9 build configurations; -Clean, -Config, -Verbose flags;
    modelled on the ThreadX Win64 build/test scripts.
- New Windows compatibility shims for standalone builds:
  test/regression_test/win64_compat/pthread.h
  test/regression_test/win64_compat/unistd.h
  - Minimal pthreads (CreateThread / WaitForSingleObject / TerminateThread)
    and usleep (Sleep) shims so standalone test files compile on MSVC.
- Fixed test/regression_test/fx_ram_driver_test.c and .h
  - RAM driver: added bounds check so out-of-range sector READs use a
    safe scratch buffer instead of segfaulting.
  - MSVC: large test buffers (ram_disk_memory_large, large_data_buffer,
    and standalone-mode ram_disk_memory / ram_disk_memory1) moved from
    BSS to calloc() via a .CRT\ constructor.  This avoids the MSVC
    PE image 2 GB hard limit (LNK1248) and eliminates demand-zero page-
    fault overhead that would otherwise slow the test suite.
- Bulk-fixed 93 fault-tolerant test files
  - [FAULT_TOLERANT_SIZE] -> [FAULT_TOLERANT_SIZE > 0 ? FAULT_TOLERANT_SIZE : 1]
    so that MSVC C2466 (zero-length array) is not triggered when
    FX_ENABLE_FAULT_TOLERANT is not defined.
- Fixed filextestcontrol.c: standalone test runner thread exit
  - The Win64 pthread shim makes pthread_exit() a no-op so that
    test_control_thread_entry() proceeds to exit(failed_tests),
    giving ctest the correct process exit code.
- Fixed common/src/fx_utility_logical_sector_write.c
  - Added missing bounds guard to prevent sector-write past end of
    RAM disk during fault-tolerant interrupt simulation tests.

All 9 build configurations pass 136/136 regression tests on Win64/VS 2022.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-15 13:15:22 -04:00

940 lines
27 KiB
PowerShell

[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 @{
FileXArch = 'win32'
FileXToolchain = 'vs_2019'
VsArch = 'x86'
}
}
'win64' {
return @{
FileXArch = 'win64'
FileXToolchain = 'vs_2022'
VsArch = 'amd64'
}
}
default {
throw "Unsupported architecture: $SelectedArch"
}
}
}
function Get-RegressionConfigurations {
return @(
'default_build_coverage',
'no_cache_build',
'no_cache_standalone_build',
'fault_tolerant_build_coverage',
'no_check_build',
'no_cache_fault_tolerant_build',
'standalone_build_coverage',
'standalone_fault_tolerant_build_coverage',
'standalone_no_cache_fault_tolerant_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' }
'no_cache_build' { return 'nc' }
'no_cache_standalone_build' { return 'ncs' }
'fault_tolerant_build_coverage' { return 'ft' }
'no_check_build' { return 'nck' }
'no_cache_fault_tolerant_build' { return 'ncft' }
'standalone_build_coverage' { return 'sa' }
'standalone_fault_tolerant_build_coverage' { return 'saft' }
'standalone_no_cache_fault_tolerant_build' { return 'sancft' }
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 ('FileX.WindowsMiniDump' -as [type])) {
return
}
Add-Type -TypeDefinition @'
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace FileX
{
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 = [FileX.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
}