From 853a5e15b6aa4f35b4996df9e09b1aa1a299f889 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 1 Apr 2026 14:23:37 -0400 Subject: [PATCH 1/4] Improve Visual Studio toolchain detection for Windows builds --- Build/Agent/FwBuildEnvironment.psm1 | 540 +++++++++++++++------------- Build/Src/FwBuildTasks/Make.cs | 49 ++- 2 files changed, 342 insertions(+), 247 deletions(-) diff --git a/Build/Agent/FwBuildEnvironment.psm1 b/Build/Agent/FwBuildEnvironment.psm1 index 5b73699100..70ea6dae61 100644 --- a/Build/Agent/FwBuildEnvironment.psm1 +++ b/Build/Agent/FwBuildEnvironment.psm1 @@ -1,148 +1,196 @@ <# .SYNOPSIS - Visual Studio and build tool environment helpers for FieldWorks. + Visual Studio and build tool environment helpers for FieldWorks. .DESCRIPTION - Provides VS environment initialization, MSBuild execution, and - VSTest path discovery. + Provides VS environment initialization, MSBuild execution, and + VSTest path discovery. .NOTES - Used by FwBuildHelpers.psm1 - do not import directly. + Used by FwBuildHelpers.psm1 - do not import directly. #> # ============================================================================= # VS Environment Functions # ============================================================================= +function Get-PreferredVcToolBinPath { + <# + .SYNOPSIS + Returns the preferred HostX64\x64 MSVC tool bin directory. + #> + if (-not $env:VCINSTALLDIR) { + return $null + } + + $toolsRoot = Join-Path $env:VCINSTALLDIR 'Tools\MSVC' + if (-not (Test-Path $toolsRoot)) { + return $null + } + + $preferred = Get-ChildItem -Path $toolsRoot -Directory -ErrorAction SilentlyContinue | + Sort-Object Name -Descending | + ForEach-Object { Join-Path $_.FullName 'bin\HostX64\x64' } | + Where-Object { Test-Path $_ } | + Select-Object -First 1 + + return $preferred +} + +function Ensure-PreferredVcToolPath { + <# + .SYNOPSIS + Moves the preferred HostX64\x64 MSVC bin directory to the front of PATH. + #> + $preferred = Get-PreferredVcToolBinPath + if (-not $preferred) { + return + } + + $pathEntries = @() + if (-not [string]::IsNullOrWhiteSpace($env:PATH)) { + $pathEntries = $env:PATH -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + } + + $filteredEntries = $pathEntries | Where-Object { + -not [string]::Equals($_.TrimEnd('\'), $preferred.TrimEnd('\'), [System.StringComparison]::OrdinalIgnoreCase) + } + + $env:PATH = (@($preferred) + $filteredEntries) -join ';' +} + function Initialize-VsDevEnvironment { - <# - .SYNOPSIS - Initializes the Visual Studio Developer environment. - .DESCRIPTION - Sets up environment variables for native C++ compilation (x64 only). - Safe to call multiple times - will skip if already initialized. - #> - if ($env:OS -ne 'Windows_NT') { - return - } - - if ($env:VCINSTALLDIR) { - Write-Host '[OK] Visual Studio environment already initialized' -ForegroundColor Green - return - } - - Write-Host 'Initializing Visual Studio Developer environment...' -ForegroundColor Yellow - - $vswhereCandidates = @() - if ($env:ProgramFiles) { - $pfVswhere = Join-Path -Path $env:ProgramFiles -ChildPath 'Microsoft Visual Studio\Installer\vswhere.exe' - if (Test-Path $pfVswhere) { $vswhereCandidates += $pfVswhere } - } - $programFilesX86 = ${env:ProgramFiles(x86)} - if ($programFilesX86) { - $pf86Vswhere = Join-Path -Path $programFilesX86 -ChildPath 'Microsoft Visual Studio\Installer\vswhere.exe' - if (Test-Path $pf86Vswhere) { $vswhereCandidates += $pf86Vswhere } - } - - if (-not $vswhereCandidates) { - Write-Host '' - Write-Host '[ERROR] Visual Studio 2017+ not found' -ForegroundColor Red - Write-Host ' Install from: https://visualstudio.microsoft.com/downloads/' -ForegroundColor Yellow - throw 'Visual Studio not found' - } - - $vsInstallPath = & $vswhereCandidates[0] -latest -requires Microsoft.Component.MSBuild Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -products * -property installationPath - if (-not $vsInstallPath) { - Write-Host '' - Write-Host '[ERROR] Visual Studio found but missing required C++ tools' -ForegroundColor Red - Write-Host ' Please install the "Desktop development with C++" workload' -ForegroundColor Yellow - throw 'Visual Studio C++ tools not found' - } - - $vsDevCmd = Join-Path -Path $vsInstallPath -ChildPath 'Common7\Tools\VsDevCmd.bat' - if (-not (Test-Path $vsDevCmd)) { - throw "Unable to locate VsDevCmd.bat under '$vsInstallPath'." - } - - # x64-only build - $arch = 'amd64' - $vsVersion = Split-Path (Split-Path (Split-Path (Split-Path $vsInstallPath))) -Leaf - Write-Host " Found Visual Studio $vsVersion at: $vsInstallPath" -ForegroundColor Gray - Write-Host " Setting up environment for $arch..." -ForegroundColor Gray - - $cmdArgs = "`"$vsDevCmd`" -no_logo -arch=$arch -host_arch=$arch && set" - $envOutput = & cmd.exe /c $cmdArgs 2>&1 - if ($LASTEXITCODE -ne 0) { - throw 'Failed to initialize Visual Studio environment' - } - - foreach ($line in $envOutput) { - $parts = $line -split '=', 2 - if ($parts.Length -eq 2 -and $parts[0]) { - Set-Item -Path "Env:$($parts[0])" -Value $parts[1] - } - } - - if (-not $env:VCINSTALLDIR) { - throw 'Visual Studio C++ environment not configured' - } - - Write-Host '[OK] Visual Studio environment initialized successfully' -ForegroundColor Green - Write-Host " VCINSTALLDIR: $env:VCINSTALLDIR" -ForegroundColor Gray + <# + .SYNOPSIS + Initializes the Visual Studio Developer environment. + .DESCRIPTION + Sets up environment variables for native C++ compilation (x64 only). + Safe to call multiple times - will skip if already initialized. + #> + if ($env:OS -ne 'Windows_NT') { + return + } + + if ($env:VCINSTALLDIR) { + Ensure-PreferredVcToolPath + Write-Host '[OK] Visual Studio environment already initialized' -ForegroundColor Green + return + } + + Write-Host 'Initializing Visual Studio Developer environment...' -ForegroundColor Yellow + + $vswhereCandidates = @() + if ($env:ProgramFiles) { + $pfVswhere = Join-Path -Path $env:ProgramFiles -ChildPath 'Microsoft Visual Studio\Installer\vswhere.exe' + if (Test-Path $pfVswhere) { $vswhereCandidates += $pfVswhere } + } + $programFilesX86 = ${env:ProgramFiles(x86)} + if ($programFilesX86) { + $pf86Vswhere = Join-Path -Path $programFilesX86 -ChildPath 'Microsoft Visual Studio\Installer\vswhere.exe' + if (Test-Path $pf86Vswhere) { $vswhereCandidates += $pf86Vswhere } + } + + if (-not $vswhereCandidates) { + Write-Host '' + Write-Host '[ERROR] Visual Studio 2017+ not found' -ForegroundColor Red + Write-Host ' Install from: https://visualstudio.microsoft.com/downloads/' -ForegroundColor Yellow + throw 'Visual Studio not found' + } + + $vsInstallPath = & $vswhereCandidates[0] -latest -requires Microsoft.Component.MSBuild Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -products * -property installationPath + if (-not $vsInstallPath) { + Write-Host '' + Write-Host '[ERROR] Visual Studio found but missing required C++ tools' -ForegroundColor Red + Write-Host ' Please install the "Desktop development with C++" workload' -ForegroundColor Yellow + throw 'Visual Studio C++ tools not found' + } + + $vsDevCmd = Join-Path -Path $vsInstallPath -ChildPath 'Common7\Tools\VsDevCmd.bat' + if (-not (Test-Path $vsDevCmd)) { + throw "Unable to locate VsDevCmd.bat under '$vsInstallPath'." + } + + # x64-only build + $arch = 'amd64' + $vsVersion = Split-Path (Split-Path (Split-Path (Split-Path $vsInstallPath))) -Leaf + Write-Host " Found Visual Studio $vsVersion at: $vsInstallPath" -ForegroundColor Gray + Write-Host " Setting up environment for $arch..." -ForegroundColor Gray + + $cmdArgs = "`"$vsDevCmd`" -no_logo -arch=$arch -host_arch=$arch && set" + $envOutput = & cmd.exe /c $cmdArgs 2>&1 + if ($LASTEXITCODE -ne 0) { + throw 'Failed to initialize Visual Studio environment' + } + + foreach ($line in $envOutput) { + $parts = $line -split '=', 2 + if ($parts.Length -eq 2 -and $parts[0]) { + Set-Item -Path "Env:$($parts[0])" -Value $parts[1] + } + } + + if (-not $env:VCINSTALLDIR) { + throw 'Visual Studio C++ environment not configured' + } + + Ensure-PreferredVcToolPath + + Write-Host '[OK] Visual Studio environment initialized successfully' -ForegroundColor Green + Write-Host " VCINSTALLDIR: $env:VCINSTALLDIR" -ForegroundColor Gray } function Get-CvtresDiagnostics { - <# - .SYNOPSIS - Returns details about the cvtres.exe resolved in the current session. - #> - $result = [ordered]@{ - Path = $null - IsVcToolset = $false - IsDotNetFramework = $false - } - - $cmd = Get-Command "cvtres.exe" -ErrorAction SilentlyContinue - if ($cmd) { - $result.Path = $cmd.Source - $lower = $result.Path.ToLowerInvariant() - $result.IsVcToolset = $lower -match "[\\/]vc[\\/]tools[\\/]msvc[\\/][^\\/]+[\\/]bin[\\/]hostx64[\\/]x64[\\/]cvtres\.exe$" - $result.IsDotNetFramework = $lower -match "windows[\\/]microsoft\.net[\\/]framework" - return $result - } - - if ($env:VCINSTALLDIR) { - $candidates = Get-ChildItem -Path (Join-Path $env:VCINSTALLDIR "Tools\MSVC\*") -Filter cvtres.exe -Recurse -ErrorAction SilentlyContinue | - Sort-Object FullName -Descending - if ($candidates -and $candidates.Count -gt 0) { - $result.Path = $candidates[0].FullName - $lower = $result.Path.ToLowerInvariant() - $result.IsVcToolset = $lower -match "[\\/]vc[\\/]tools[\\/]msvc[\\/][^\\/]+[\\/]bin[\\/]hostx64[\\/]x64[\\/]cvtres\.exe$" - $result.IsDotNetFramework = $lower -match "windows[\\/]microsoft\.net[\\/]framework" - } - } - - return $result + <# + .SYNOPSIS + Returns details about the cvtres.exe resolved in the current session. + #> + $result = [ordered]@{ + Path = $null + IsVcToolset = $false + IsDotNetFramework = $false + } + + $cmd = Get-Command "cvtres.exe" -ErrorAction SilentlyContinue + if ($cmd) { + $result.Path = $cmd.Source + $lower = $result.Path.ToLowerInvariant() + $result.IsVcToolset = $lower -match "[\\/]vc[\\/]tools[\\/]msvc[\\/][^\\/]+[\\/]bin[\\/]hostx64[\\/]x64[\\/]cvtres\.exe$" + $result.IsDotNetFramework = $lower -match "windows[\\/]microsoft\.net[\\/]framework" + return $result + } + + if ($env:VCINSTALLDIR) { + $candidates = Get-ChildItem -Path (Join-Path $env:VCINSTALLDIR "Tools\MSVC\*") -Filter cvtres.exe -Recurse -ErrorAction SilentlyContinue | + Sort-Object FullName -Descending + if ($candidates -and $candidates.Count -gt 0) { + $result.Path = $candidates[0].FullName + $lower = $result.Path.ToLowerInvariant() + $result.IsVcToolset = $lower -match "[\\/]vc[\\/]tools[\\/]msvc[\\/][^\\/]+[\\/]bin[\\/]hostx64[\\/]x64[\\/]cvtres\.exe$" + $result.IsDotNetFramework = $lower -match "windows[\\/]microsoft\.net[\\/]framework" + } + } + + return $result } function Test-CvtresCompatibility { - <# - .SYNOPSIS - Emits warnings if cvtres.exe resolves to a non-VC toolset binary. - #> - $diag = Get-CvtresDiagnostics - - if (-not $diag.Path) { - Write-Host "[WARN] cvtres.exe not found after VS environment setup. Toolchain may be incomplete." -ForegroundColor Yellow - return - } - - if ($diag.IsDotNetFramework) { - Write-Host "[WARN] cvtres.exe resolves to a .NET Framework path. Prefer the VC toolset version (Hostx64\\x64). $($diag.Path)" -ForegroundColor Yellow - } - elseif (-not $diag.IsVcToolset) { - Write-Host "[WARN] cvtres.exe is not from the VC toolset Hostx64\\x64 folder. Confirm PATH ordering. $($diag.Path)" -ForegroundColor Yellow - } + <# + .SYNOPSIS + Emits warnings if cvtres.exe resolves to a non-VC toolset binary. + #> + $diag = Get-CvtresDiagnostics + + if (-not $diag.Path) { + Write-Host "[WARN] cvtres.exe not found after VS environment setup. Toolchain may be incomplete." -ForegroundColor Yellow + return + } + + if ($diag.IsDotNetFramework) { + Write-Host "[WARN] cvtres.exe resolves to a .NET Framework path. Prefer the VC toolset version (Hostx64\\x64). $($diag.Path)" -ForegroundColor Yellow + } + elseif (-not $diag.IsVcToolset) { + Write-Host "[WARN] cvtres.exe is not from the VC toolset Hostx64\\x64 folder. Confirm PATH ordering. $($diag.Path)" -ForegroundColor Yellow + } } # ============================================================================= @@ -150,89 +198,89 @@ function Test-CvtresCompatibility { # ============================================================================= function Get-MSBuildPath { - <# - .SYNOPSIS - Gets the path to MSBuild.exe. - .DESCRIPTION - Returns the MSBuild command, either from PATH or 'msbuild' as fallback. - #> - $msbuildCmd = Get-Command msbuild -ErrorAction SilentlyContinue - if ($msbuildCmd) { - return $msbuildCmd.Source - } - return 'msbuild' + <# + .SYNOPSIS + Gets the path to MSBuild.exe. + .DESCRIPTION + Returns the MSBuild command, either from PATH or 'msbuild' as fallback. + #> + $msbuildCmd = Get-Command msbuild -ErrorAction SilentlyContinue + if ($msbuildCmd) { + return $msbuildCmd.Source + } + return 'msbuild' } function Invoke-MSBuild { - <# - .SYNOPSIS - Executes MSBuild with proper error handling. - .DESCRIPTION - Runs MSBuild with the specified arguments and handles errors appropriately. - .PARAMETER Arguments - Array of arguments to pass to MSBuild. - .PARAMETER Description - Human-readable description of the build step. - .PARAMETER LogPath - Optional path to write build output to a log file. - .PARAMETER TailLines - If specified, only displays the last N lines of output. - #> - param( - [Parameter(Mandatory)] - [string[]]$Arguments, - [Parameter(Mandatory)] - [string]$Description, - [string]$LogPath = '', - [int]$TailLines = 0 - ) - - $msbuildCmd = Get-MSBuildPath - Write-Host "Running $Description..." -ForegroundColor Cyan - - if ($TailLines -gt 0) { - # Capture all output, optionally log to file, then display tail - $output = & $msbuildCmd $Arguments 2>&1 | ForEach-Object { $_.ToString() } - $exitCode = $LASTEXITCODE - - if ($LogPath) { - $logDir = Split-Path -Parent $LogPath - if ($logDir -and -not (Test-Path $logDir)) { - New-Item -Path $logDir -ItemType Directory -Force | Out-Null - } - $output | Out-File -FilePath $LogPath -Encoding utf8 - } - - # Display last N lines - $totalLines = $output.Count - if ($totalLines -gt $TailLines) { - Write-Host "... ($($totalLines - $TailLines) lines omitted, showing last $TailLines) ..." -ForegroundColor DarkGray - $output | Select-Object -Last $TailLines | ForEach-Object { Write-Host $_ } - } - else { - $output | ForEach-Object { Write-Host $_ } - } - - $LASTEXITCODE = $exitCode - } - elseif ($LogPath) { - $logDir = Split-Path -Parent $LogPath - if ($logDir -and -not (Test-Path $logDir)) { - New-Item -Path $logDir -ItemType Directory -Force | Out-Null - } - & $msbuildCmd $Arguments | Tee-Object -FilePath $LogPath - } - else { - & $msbuildCmd $Arguments - } - - if ($LASTEXITCODE -ne 0) { - $errorMsg = "MSBuild failed during $Description with exit code $LASTEXITCODE" - if ($LASTEXITCODE -eq -1073741819) { - $errorMsg += " (0xC0000005 - Access Violation). This indicates a crash in native code during build." - } - throw $errorMsg - } + <# + .SYNOPSIS + Executes MSBuild with proper error handling. + .DESCRIPTION + Runs MSBuild with the specified arguments and handles errors appropriately. + .PARAMETER Arguments + Array of arguments to pass to MSBuild. + .PARAMETER Description + Human-readable description of the build step. + .PARAMETER LogPath + Optional path to write build output to a log file. + .PARAMETER TailLines + If specified, only displays the last N lines of output. + #> + param( + [Parameter(Mandatory)] + [string[]]$Arguments, + [Parameter(Mandatory)] + [string]$Description, + [string]$LogPath = '', + [int]$TailLines = 0 + ) + + $msbuildCmd = Get-MSBuildPath + Write-Host "Running $Description..." -ForegroundColor Cyan + + if ($TailLines -gt 0) { + # Capture all output, optionally log to file, then display tail + $output = & $msbuildCmd $Arguments 2>&1 | ForEach-Object { $_.ToString() } + $exitCode = $LASTEXITCODE + + if ($LogPath) { + $logDir = Split-Path -Parent $LogPath + if ($logDir -and -not (Test-Path $logDir)) { + New-Item -Path $logDir -ItemType Directory -Force | Out-Null + } + $output | Out-File -FilePath $LogPath -Encoding utf8 + } + + # Display last N lines + $totalLines = $output.Count + if ($totalLines -gt $TailLines) { + Write-Host "... ($($totalLines - $TailLines) lines omitted, showing last $TailLines) ..." -ForegroundColor DarkGray + $output | Select-Object -Last $TailLines | ForEach-Object { Write-Host $_ } + } + else { + $output | ForEach-Object { Write-Host $_ } + } + + $LASTEXITCODE = $exitCode + } + elseif ($LogPath) { + $logDir = Split-Path -Parent $LogPath + if ($logDir -and -not (Test-Path $logDir)) { + New-Item -Path $logDir -ItemType Directory -Force | Out-Null + } + & $msbuildCmd $Arguments | Tee-Object -FilePath $LogPath + } + else { + & $msbuildCmd $Arguments + } + + if ($LASTEXITCODE -ne 0) { + $errorMsg = "MSBuild failed during $Description with exit code $LASTEXITCODE" + if ($LASTEXITCODE -eq -1073741819) { + $errorMsg += " (0xC0000005 - Access Violation). This indicates a crash in native code during build." + } + throw $errorMsg + } } # ============================================================================= @@ -240,42 +288,42 @@ function Invoke-MSBuild { # ============================================================================= function Get-VSTestPath { - <# - .SYNOPSIS - Finds vstest.console.exe in PATH or known locations. - .DESCRIPTION - First checks PATH, then falls back to known VS installation paths. - #> - - # Try PATH first (setup scripts add vstest to PATH) - $vstestFromPath = Get-Command "vstest.console.exe" -ErrorAction SilentlyContinue - if ($vstestFromPath) { - return $vstestFromPath.Source - } - - # Fall back to known installation paths - $programFilesX86 = ${env:ProgramFiles(x86)} - if (-not $programFilesX86) { $programFilesX86 = "C:\Program Files (x86)" } - - $vstestCandidates = @( - # BuildTools - "$programFilesX86\Microsoft Visual Studio\2022\BuildTools\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", - "C:\BuildTools\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", - # TestAgent (sometimes installed separately) - "$programFilesX86\Microsoft Visual Studio\2022\TestAgent\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", - # Full VS installations - "${env:ProgramFiles}\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", - "${env:ProgramFiles}\Microsoft Visual Studio\2022\Professional\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", - "${env:ProgramFiles}\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe" - ) - - foreach ($candidate in $vstestCandidates) { - if (Test-Path $candidate) { - return $candidate - } - } - - return $null + <# + .SYNOPSIS + Finds vstest.console.exe in PATH or known locations. + .DESCRIPTION + First checks PATH, then falls back to known VS installation paths. + #> + + # Try PATH first (setup scripts add vstest to PATH) + $vstestFromPath = Get-Command "vstest.console.exe" -ErrorAction SilentlyContinue + if ($vstestFromPath) { + return $vstestFromPath.Source + } + + # Fall back to known installation paths + $programFilesX86 = ${env:ProgramFiles(x86)} + if (-not $programFilesX86) { $programFilesX86 = "C:\Program Files (x86)" } + + $vstestCandidates = @( + # BuildTools + "$programFilesX86\Microsoft Visual Studio\2022\BuildTools\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", + "C:\BuildTools\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", + # TestAgent (sometimes installed separately) + "$programFilesX86\Microsoft Visual Studio\2022\TestAgent\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", + # Full VS installations + "${env:ProgramFiles}\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", + "${env:ProgramFiles}\Microsoft Visual Studio\2022\Professional\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", + "${env:ProgramFiles}\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe" + ) + + foreach ($candidate in $vstestCandidates) { + if (Test-Path $candidate) { + return $candidate + } + } + + return $null } # ============================================================================= @@ -283,9 +331,9 @@ function Get-VSTestPath { # ============================================================================= Export-ModuleMember -Function @( - 'Initialize-VsDevEnvironment', + 'Initialize-VsDevEnvironment', 'Test-CvtresCompatibility', - 'Get-MSBuildPath', - 'Invoke-MSBuild', - 'Get-VSTestPath' + 'Get-MSBuildPath', + 'Invoke-MSBuild', + 'Get-VSTestPath' ) diff --git a/Build/Src/FwBuildTasks/Make.cs b/Build/Src/FwBuildTasks/Make.cs index 55458e919d..07b62dd083 100644 --- a/Build/Src/FwBuildTasks/Make.cs +++ b/Build/Src/FwBuildTasks/Make.cs @@ -111,6 +111,39 @@ protected override string ToolName } } + private static string FindVisualStudioToolPath(string vcInstallDir, string toolName) + { + if (String.IsNullOrEmpty(vcInstallDir) || !Directory.Exists(vcInstallDir)) + return null; + + string toolsRoot = Path.Combine(vcInstallDir, "Tools", "MSVC"); + if (!Directory.Exists(toolsRoot)) + return null; + + string[] versionDirs = Directory.GetDirectories(toolsRoot); + Array.Sort(versionDirs, StringComparer.OrdinalIgnoreCase); + Array.Reverse(versionDirs); + + foreach (string versionDir in versionDirs) + { + string[] candidateDirs = + { + Path.Combine(versionDir, "bin", "Hostx64", "x64"), + Path.Combine(versionDir, "bin", "Hostx64", "x86"), + Path.Combine(versionDir, "bin", "Hostx86", "x86"), + Path.Combine(versionDir, "bin", "Hostx86", "x64") + }; + + foreach (string candidateDir in candidateDirs) + { + if (File.Exists(Path.Combine(candidateDir, toolName))) + return candidateDir; + } + } + + return null; + } + private void CheckToolPath() { string path = Environment.GetEnvironmentVariable("PATH"); @@ -140,7 +173,21 @@ private void CheckToolPath() // Fall Back to the install directory (if VCINSTALLDIR is set) if (!String.IsNullOrEmpty(vcInstallDir)) { - ToolPath = Path.Combine(vcInstallDir, "bin"); + string visualStudioToolPath = FindVisualStudioToolPath(vcInstallDir, ToolName); + if (!String.IsNullOrEmpty(visualStudioToolPath)) + { + ToolPath = visualStudioToolPath; + return; + } + + string legacyToolPath = Path.Combine(vcInstallDir, "bin"); + if (File.Exists(Path.Combine(legacyToolPath, ToolName))) + { + ToolPath = legacyToolPath; + return; + } + + ToolPath = String.Empty; } else { From 65f04fd7622741c01b2cefac0b6f1217305b63d4 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 1 Apr 2026 14:23:37 -0400 Subject: [PATCH 2/4] Fix toolchain version sorting and docs --- Build/Agent/FwBuildEnvironment.psm1 | 21 +- Build/Agent/Verify-FwDependencies.ps1 | 434 +++++++++++++------------- Build/Src/FwBuildTasks/Make.cs | 27 +- 3 files changed, 265 insertions(+), 217 deletions(-) diff --git a/Build/Agent/FwBuildEnvironment.psm1 b/Build/Agent/FwBuildEnvironment.psm1 index 70ea6dae61..7d775afb64 100644 --- a/Build/Agent/FwBuildEnvironment.psm1 +++ b/Build/Agent/FwBuildEnvironment.psm1 @@ -28,8 +28,27 @@ function Get-PreferredVcToolBinPath { return $null } + $versionSort = { + $parsedVersion = [version]'0.0' + $isVersion = [version]::TryParse($_.Name, [ref]$parsedVersion) + if ($isVersion) { + return $parsedVersion + } + + return [version]'0.0' + } + + $sortProperties = @( + @{ Expression = { + $parsedVersion = [version]'0.0' + [version]::TryParse($_.Name, [ref]$parsedVersion) + }; Descending = $true } + @{ Expression = $versionSort; Descending = $true } + @{ Expression = { $_.Name }; Descending = $true } + ) + $preferred = Get-ChildItem -Path $toolsRoot -Directory -ErrorAction SilentlyContinue | - Sort-Object Name -Descending | + Sort-Object -Property $sortProperties | ForEach-Object { Join-Path $_.FullName 'bin\HostX64\x64' } | Where-Object { Test-Path $_ } | Select-Object -First 1 diff --git a/Build/Agent/Verify-FwDependencies.ps1 b/Build/Agent/Verify-FwDependencies.ps1 index 10c943dc16..18510ffced 100644 --- a/Build/Agent/Verify-FwDependencies.ps1 +++ b/Build/Agent/Verify-FwDependencies.ps1 @@ -1,118 +1,124 @@ <# .SYNOPSIS - Verifies that all FieldWorks build dependencies are available. + Verifies that all FieldWorks build dependencies are available. .DESCRIPTION - Checks for required tools and SDKs needed to build FieldWorks. - Can be run locally for testing or called from GitHub Actions workflows. - - Expected dependencies (typically pre-installed on windows-latest): - - Visual Studio 2022 with Desktop & C++ workloads - - MSBuild - - .NET Framework 4.8.1 SDK & Targeting Pack - - Windows SDK - - WiX Toolset v6 (installer builds restore via NuGet) - - .NET SDK 8.x+ + Checks for required tools and SDKs needed to build FieldWorks. + Can be run locally for testing or called from GitHub Actions workflows. + By default, the script writes host output only and does not emit result objects. + Use -PassThru when a caller needs structured results returned on the pipeline. + + Expected dependencies (typically pre-installed on windows-latest): + - Visual Studio 2022 with Desktop & C++ workloads + - MSBuild + - .NET Framework 4.8.1 SDK & Targeting Pack + - Windows SDK + - WiX Toolset v6 (installer builds restore via NuGet) + - .NET SDK 8.x+ .PARAMETER FailOnMissing - If specified, exits with non-zero code if any required dependency is missing. + If specified, exits with non-zero code if any required dependency is missing. .PARAMETER IncludeOptional - If specified, also checks optional dependencies like clangd for Serena. + If specified, also checks optional dependencies like clangd for Serena. .PARAMETER Detailed - If specified, prints the full per-dependency section headers and success details instead of the compact summary-only output. + If specified, prints the full per-dependency section headers and success details instead of the compact summary-only output. .PARAMETER PassThru - If specified, returns the dependency result objects for scripting callers instead of writing them implicitly. + If specified, returns the dependency result objects for scripting callers. + Without -PassThru, the script is quiet-by-default and writes host output only. .EXAMPLE - # Quick check - .\Build\Agent\Verify-FwDependencies.ps1 + # Quick check + .\Build\Agent\Verify-FwDependencies.ps1 .EXAMPLE - # Strict check for CI - .\Build\Agent\Verify-FwDependencies.ps1 -FailOnMissing + # Strict check for CI + .\Build\Agent\Verify-FwDependencies.ps1 -FailOnMissing .EXAMPLE - # Include Serena dependencies - .\Build\Agent\Verify-FwDependencies.ps1 -IncludeOptional + # Include Serena dependencies + .\Build\Agent\Verify-FwDependencies.ps1 -IncludeOptional .EXAMPLE - # Show full dependency-by-dependency output - .\Build\Agent\Verify-FwDependencies.ps1 -IncludeOptional -Detailed + # Show full dependency-by-dependency output + .\Build\Agent\Verify-FwDependencies.ps1 -IncludeOptional -Detailed .EXAMPLE - # Capture structured results for automation - $results = .\Build\Agent\Verify-FwDependencies.ps1 -IncludeOptional -PassThru + # Capture structured results for automation + $results = .\Build\Agent\Verify-FwDependencies.ps1 -IncludeOptional -PassThru + +.NOTES + Behavioral change: this script no longer emits dependency result objects unless -PassThru is specified. #> [CmdletBinding()] param( - [switch]$FailOnMissing, - [switch]$IncludeOptional, - [switch]$Detailed, - [switch]$PassThru + [switch]$FailOnMissing, + [switch]$IncludeOptional, + [switch]$Detailed, + [switch]$PassThru ) $ErrorActionPreference = 'Stop' function Test-Dependency { - param( - [string]$Name, - [scriptblock]$Check, - [string]$Required = "Required" - ) - - try { - $result = & $Check - if ($result) { - if ($Detailed) { - Write-Host "[OK] $Name" -ForegroundColor Green - if ($result -is [string] -and $result.Length -gt 0 -and $result.Length -lt 100) { - Write-Host " $result" -ForegroundColor DarkGray - } - } - return @{ Name = $Name; Found = $true; IsRequired = ($Required -eq "Required"); Info = $result } - } - else { - throw "Check returned null/false" - } - } - catch { - $color = if ($Required -eq "Required") { "Red" } else { "Yellow" } - $status = if ($Required -eq "Required") { "[FAIL]" } else { "[WARN]" } - Write-Host "$status $Name" -ForegroundColor $color - Write-Host " $_" -ForegroundColor DarkGray - return @{ Name = $Name; Found = $false; IsRequired = ($Required -eq "Required"); Error = $_.ToString() } - } + param( + [string]$Name, + [scriptblock]$Check, + [string]$Required = "Required" + ) + + try { + $result = & $Check + if ($result) { + if ($Detailed) { + Write-Host "[OK] $Name" -ForegroundColor Green + if ($result -is [string] -and $result.Length -gt 0 -and $result.Length -lt 100) { + Write-Host " $result" -ForegroundColor DarkGray + } + } + return @{ Name = $Name; Found = $true; IsRequired = ($Required -eq "Required"); Info = $result } + } + else { + throw "Check returned null/false" + } + } + catch { + $color = if ($Required -eq "Required") { "Red" } else { "Yellow" } + $status = if ($Required -eq "Required") { "[FAIL]" } else { "[WARN]" } + Write-Host "$status $Name" -ForegroundColor $color + Write-Host " $_" -ForegroundColor DarkGray + return @{ Name = $Name; Found = $false; IsRequired = ($Required -eq "Required"); Error = $_.ToString() } + } } function Find-DotNetFrameworkSdkTool { - param([Parameter(Mandatory)][string]$ToolName) + param([Parameter(Mandatory)][string]$ToolName) - $programFilesX86 = ${env:ProgramFiles(x86)} - if (-not $programFilesX86) { return $null } + $programFilesX86 = ${env:ProgramFiles(x86)} + if (-not $programFilesX86) { return $null } - $sdkBase = Join-Path $programFilesX86 'Microsoft SDKs\Windows\v10.0A\bin' - if (-not (Test-Path $sdkBase)) { return $null } + $sdkBase = Join-Path $programFilesX86 'Microsoft SDKs\Windows\v10.0A\bin' + if (-not (Test-Path $sdkBase)) { return $null } - $toolCandidates = @() - $netfxDirs = Get-ChildItem -Path $sdkBase -Directory -Filter 'NETFX*' -ErrorAction SilentlyContinue | - Sort-Object Name -Descending + $toolCandidates = @() + $netfxDirs = Get-ChildItem -Path $sdkBase -Directory -Filter 'NETFX*' -ErrorAction SilentlyContinue | + Sort-Object Name -Descending - foreach ($dir in $netfxDirs) { - $toolCandidates += (Join-Path $dir.FullName (Join-Path 'x64' $ToolName)) - $toolCandidates += (Join-Path $dir.FullName $ToolName) - } + foreach ($dir in $netfxDirs) { + $toolCandidates += (Join-Path $dir.FullName (Join-Path 'x64' $ToolName)) + $toolCandidates += (Join-Path $dir.FullName $ToolName) + } - foreach ($candidate in $toolCandidates) { - if (Test-Path $candidate) { - return $candidate - } - } + foreach ($candidate in $toolCandidates) { + if (Test-Path $candidate) { + return $candidate + } + } - return $null + return $null } # ============================================================================ @@ -120,8 +126,8 @@ function Find-DotNetFrameworkSdkTool { # ============================================================================ if ($Detailed) { - Write-Host "=== FieldWorks Dependency Verification ===" -ForegroundColor Cyan - Write-Host "" + Write-Host "=== FieldWorks Dependency Verification ===" -ForegroundColor Cyan + Write-Host "" } $results = @() @@ -130,171 +136,171 @@ $results = @() # Required Dependencies # ---------------------------------------------------------------------------- if ($Detailed) { - Write-Host "--- Required Dependencies ---" -ForegroundColor Cyan + Write-Host "--- Required Dependencies ---" -ForegroundColor Cyan } # .NET Framework targeting pack (4.8+) $results += Test-Dependency -Name ".NET Framework Targeting Pack (4.8+)" -Check { - $base = "${env:ProgramFiles(x86)}\Reference Assemblies\Microsoft\Framework\.NETFramework" - $candidates = @('v4.8.1', 'v4.8') - foreach ($version in $candidates) { - $path = Join-Path $base $version - if (Test-Path $path) { - return "$version at $path" - } - } - throw "Neither v4.8.1 nor v4.8 targeting pack was found under $base" + $base = "${env:ProgramFiles(x86)}\Reference Assemblies\Microsoft\Framework\.NETFramework" + $candidates = @('v4.8.1', 'v4.8') + foreach ($version in $candidates) { + $path = Join-Path $base $version + if (Test-Path $path) { + return "$version at $path" + } + } + throw "Neither v4.8.1 nor v4.8 targeting pack was found under $base" } # Windows SDK $results += Test-Dependency -Name "Windows SDK" -Check { - $path = "${env:ProgramFiles(x86)}\Windows Kits\10\Include" - if (Test-Path $path) { - $versions = (Get-ChildItem $path -Directory | Sort-Object Name -Descending | Select-Object -First 3).Name -join ', ' - return "Versions: $versions" - } - throw "Not found at $path" + $path = "${env:ProgramFiles(x86)}\Windows Kits\10\Include" + if (Test-Path $path) { + $versions = (Get-ChildItem $path -Directory | Sort-Object Name -Descending | Select-Object -First 3).Name -join ', ' + return "Versions: $versions" + } + throw "Not found at $path" } # Visual Studio / MSBuild $results += Test-Dependency -Name "Visual Studio 2022" -Check { - $vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - if (-not (Test-Path $vsWhere)) { throw "vswhere.exe not found" } - $vsPath = & $vsWhere -latest -requires Microsoft.Component.MSBuild -products * -property installationPath - if (-not $vsPath) { throw "No VS installation with MSBuild found" } - $version = & $vsWhere -latest -property catalog_productDisplayVersion - return "Version $version at $vsPath" + $vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + if (-not (Test-Path $vsWhere)) { throw "vswhere.exe not found" } + $vsPath = & $vsWhere -latest -requires Microsoft.Component.MSBuild -products * -property installationPath + if (-not $vsPath) { throw "No VS installation with MSBuild found" } + $version = & $vsWhere -latest -property catalog_productDisplayVersion + return "Version $version at $vsPath" } # MSBuild $results += Test-Dependency -Name "MSBuild" -Check { - $msbuild = Get-Command msbuild.exe -ErrorAction SilentlyContinue - if ($msbuild) { - $version = (& msbuild.exe -version -nologo 2>$null | Select-Object -Last 1) - return "Version $version" - } - # Try via vswhere - $vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - $vsPath = & $vsWhere -latest -requires Microsoft.Component.MSBuild -products * -property installationPath 2>$null - if ($vsPath) { - $msbuildPath = Join-Path $vsPath 'MSBuild\Current\Bin\MSBuild.exe' - if (Test-Path $msbuildPath) { - return "Found at $msbuildPath (not in PATH)" - } - } - throw "MSBuild not found in PATH or VS installation" + $msbuild = Get-Command msbuild.exe -ErrorAction SilentlyContinue + if ($msbuild) { + $version = (& msbuild.exe -version -nologo 2>$null | Select-Object -Last 1) + return "Version $version" + } + # Try via vswhere + $vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $vsPath = & $vsWhere -latest -requires Microsoft.Component.MSBuild -products * -property installationPath 2>$null + if ($vsPath) { + $msbuildPath = Join-Path $vsPath 'MSBuild\Current\Bin\MSBuild.exe' + if (Test-Path $msbuildPath) { + return "Found at $msbuildPath (not in PATH)" + } + } + throw "MSBuild not found in PATH or VS installation" } # .NET Framework SDK tools used by localization tasks $results += Test-Dependency -Name "ResGen.exe (.NET Framework SDK)" -Check { - $resgen = Find-DotNetFrameworkSdkTool -ToolName 'ResGen.exe' - if ($resgen) { return $resgen } - throw "ResGen.exe not found in Windows SDK NETFX tool folders" + $resgen = Find-DotNetFrameworkSdkTool -ToolName 'ResGen.exe' + if ($resgen) { return $resgen } + throw "ResGen.exe not found in Windows SDK NETFX tool folders" } $results += Test-Dependency -Name "al.exe (.NET Framework SDK)" -Check { - $al = Find-DotNetFrameworkSdkTool -ToolName 'al.exe' - if ($al) { return $al } - throw "al.exe not found in Windows SDK NETFX tool folders" + $al = Find-DotNetFrameworkSdkTool -ToolName 'al.exe' + if ($al) { return $al } + throw "al.exe not found in Windows SDK NETFX tool folders" } # .NET SDK $results += Test-Dependency -Name ".NET SDK" -Check { - $dotnet = Get-Command dotnet.exe -ErrorAction SilentlyContinue - if ($dotnet) { - $version = (& dotnet.exe --version 2>&1) - return "Version $version" - } - throw "dotnet.exe not found in PATH" + $dotnet = Get-Command dotnet.exe -ErrorAction SilentlyContinue + if ($dotnet) { + $version = (& dotnet.exe --version 2>&1) + return "Version $version" + } + throw "dotnet.exe not found in PATH" } # WiX Toolset (v6) # Installer projects use WixToolset.Sdk via NuGet restore; no global WiX 3.x install is required. $results += Test-Dependency -Name "WiX Toolset (v6 via NuGet)" -Required "Optional" -Check { - $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) - $wixProj = Join-Path $repoRoot "FLExInstaller\wix6\FieldWorks.Installer.wixproj" - if (-not (Test-Path $wixProj)) { - throw "Installer project not found: $wixProj" - } - - [xml]$wixProjXml = Get-Content -LiteralPath $wixProj - $projectNode = $wixProjXml.Project - $hasWixSdk = $false - - if ($projectNode -and $projectNode.Sdk -match 'WixToolset\.Sdk') { - $hasWixSdk = $true - } - - if (-not $hasWixSdk) { - $wixSdkReference = $wixProjXml.SelectSingleNode("//*[local-name()='PackageReference' and @Include='WixToolset.Sdk']") - if ($wixSdkReference) { - $hasWixSdk = $true - } - } - - if ($hasWixSdk) { - return "Configured in $wixProj (restored during build)" - } - - throw "WixToolset.Sdk not referenced in $wixProj" + $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) + $wixProj = Join-Path $repoRoot "FLExInstaller\wix6\FieldWorks.Installer.wixproj" + if (-not (Test-Path $wixProj)) { + throw "Installer project not found: $wixProj" + } + + [xml]$wixProjXml = Get-Content -LiteralPath $wixProj -Raw + $projectNode = $wixProjXml.Project + $hasWixSdk = $false + + if ($projectNode -and $projectNode.Sdk -match 'WixToolset\.Sdk') { + $hasWixSdk = $true + } + + if (-not $hasWixSdk) { + $wixSdkReference = $wixProjXml.SelectSingleNode("//*[local-name()='PackageReference' and @Include='WixToolset.Sdk']") + if ($wixSdkReference) { + $hasWixSdk = $true + } + } + + if ($hasWixSdk) { + return "Configured in $wixProj (restored during build)" + } + + throw "WixToolset.Sdk not referenced in $wixProj" } # ---------------------------------------------------------------------------- # Optional Dependencies (for Serena MCP) # ---------------------------------------------------------------------------- if ($IncludeOptional) { - if ($Detailed) { - Write-Host "" - Write-Host "--- Optional Dependencies (Serena MCP) ---" -ForegroundColor Cyan - } - - # Python - $results += Test-Dependency -Name "Python" -Required "Optional" -Check { - $python = Get-Command python.exe -ErrorAction SilentlyContinue - if (-not $python) { $python = Get-Command python3.exe -ErrorAction SilentlyContinue } - if ($python) { - $version = (& $python.Source --version 2>&1) - return $version - } - throw "python.exe not found in PATH" - } - - # uv (Python package manager) - $results += Test-Dependency -Name "uv (Python package manager)" -Required "Optional" -Check { - $uv = Get-Command uv -ErrorAction SilentlyContinue - if ($uv) { - $version = (& uv --version 2>&1) - return $version - } - throw "uv not found - install with: winget install astral-sh.uv" - } - - # clangd (C++ language server) - $results += Test-Dependency -Name "clangd (C++ language server)" -Required "Optional" -Check { - $clangd = Get-Command clangd -ErrorAction SilentlyContinue - if ($clangd) { - $version = (& clangd --version 2>&1 | Select-Object -First 1) - return $version - } - throw "clangd not found - Serena will auto-download if needed" - } - - # Serena project config - $results += Test-Dependency -Name "Serena project config" -Required "Optional" -Check { - $configPath = ".serena/project.yml" - if (Test-Path $configPath) { - return "Found at $configPath" - } - throw "No .serena/project.yml - Serena not configured for this repo" - } + if ($Detailed) { + Write-Host "" + Write-Host "--- Optional Dependencies (Serena MCP) ---" -ForegroundColor Cyan + } + + # Python + $results += Test-Dependency -Name "Python" -Required "Optional" -Check { + $python = Get-Command python.exe -ErrorAction SilentlyContinue + if (-not $python) { $python = Get-Command python3.exe -ErrorAction SilentlyContinue } + if ($python) { + $version = (& $python.Source --version 2>&1) + return $version + } + throw "python.exe not found in PATH" + } + + # uv (Python package manager) + $results += Test-Dependency -Name "uv (Python package manager)" -Required "Optional" -Check { + $uv = Get-Command uv -ErrorAction SilentlyContinue + if ($uv) { + $version = (& uv --version 2>&1) + return $version + } + throw "uv not found - install with: winget install astral-sh.uv" + } + + # clangd (C++ language server) + $results += Test-Dependency -Name "clangd (C++ language server)" -Required "Optional" -Check { + $clangd = Get-Command clangd -ErrorAction SilentlyContinue + if ($clangd) { + $version = (& clangd --version 2>&1 | Select-Object -First 1) + return $version + } + throw "clangd not found - Serena will auto-download if needed" + } + + # Serena project config + $results += Test-Dependency -Name "Serena project config" -Required "Optional" -Check { + $configPath = ".serena/project.yml" + if (Test-Path $configPath) { + return "Found at $configPath" + } + throw "No .serena/project.yml - Serena not configured for this repo" + } } # ---------------------------------------------------------------------------- # Summary # ---------------------------------------------------------------------------- if ($Detailed) { - Write-Host "" - Write-Host "=== Summary ===" -ForegroundColor Cyan + Write-Host "" + Write-Host "=== Summary ===" -ForegroundColor Cyan } $required = $results | Where-Object { $_.IsRequired -ne $false } @@ -307,28 +313,28 @@ $foundRequired = ($required | Where-Object { $_.Found } | Measure-Object).Count Write-Host "Dependency preflight: required $foundRequired/$totalRequired found" if ($IncludeOptional) { - $totalOptional = ($optional | Measure-Object).Count - $foundOptional = ($optional | Where-Object { $_.Found } | Measure-Object).Count - Write-Host "Dependency preflight: optional $foundOptional/$totalOptional found" + $totalOptional = ($optional | Measure-Object).Count + $foundOptional = ($optional | Where-Object { $_.Found } | Measure-Object).Count + Write-Host "Dependency preflight: optional $foundOptional/$totalOptional found" } if ($missing.Count -gt 0) { - Write-Host "" - Write-Host "Missing required dependencies:" -ForegroundColor Red - foreach ($m in $missing) { - Write-Host " - $($m.Name)" -ForegroundColor Red - } - - if ($FailOnMissing) { - Write-Host "" - Write-Host "Exiting with error (FailOnMissing specified)" -ForegroundColor Red - exit 1 - } + Write-Host "" + Write-Host "Missing required dependencies:" -ForegroundColor Red + foreach ($m in $missing) { + Write-Host " - $($m.Name)" -ForegroundColor Red + } + + if ($FailOnMissing) { + Write-Host "" + Write-Host "Exiting with error (FailOnMissing specified)" -ForegroundColor Red + exit 1 + } } else { - Write-Host "Dependency preflight: all required dependencies are available" -ForegroundColor Green + Write-Host "Dependency preflight: all required dependencies are available" -ForegroundColor Green } if ($PassThru) { - return $results + return $results } diff --git a/Build/Src/FwBuildTasks/Make.cs b/Build/Src/FwBuildTasks/Make.cs index 07b62dd083..933b5999fd 100644 --- a/Build/Src/FwBuildTasks/Make.cs +++ b/Build/Src/FwBuildTasks/Make.cs @@ -121,8 +121,7 @@ private static string FindVisualStudioToolPath(string vcInstallDir, string toolN return null; string[] versionDirs = Directory.GetDirectories(toolsRoot); - Array.Sort(versionDirs, StringComparer.OrdinalIgnoreCase); - Array.Reverse(versionDirs); + Array.Sort(versionDirs, CompareVersionDirectories); foreach (string versionDir in versionDirs) { @@ -144,6 +143,30 @@ private static string FindVisualStudioToolPath(string vcInstallDir, string toolN return null; } + private static int CompareVersionDirectories(string left, string right) + { + string leftName = Path.GetFileName(left); + string rightName = Path.GetFileName(right); + + Version leftVersion; + Version rightVersion; + bool leftIsVersion = Version.TryParse(leftName, out leftVersion); + bool rightIsVersion = Version.TryParse(rightName, out rightVersion); + + if (leftIsVersion && rightIsVersion) + { + int versionComparison = rightVersion.CompareTo(leftVersion); + if (versionComparison != 0) + return versionComparison; + } + else if (leftIsVersion != rightIsVersion) + { + return rightIsVersion.CompareTo(leftIsVersion); + } + + return StringComparer.OrdinalIgnoreCase.Compare(rightName, leftName); + } + private void CheckToolPath() { string path = Environment.GetEnvironmentVariable("PATH"); From 2de0eeca0127b363bedc20bbfbccceaf2f8be9e8 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 1 Apr 2026 16:21:42 -0400 Subject: [PATCH 3/4] Cleanup --- .vscode/settings.json | 1 - Build/Agent/FwBuildEnvironment.psm1 | 312 +++++++++++++++++++------- Build/Agent/Run-VsTests.ps1 | 260 +++++++++++---------- Build/Agent/Setup-FwBuildEnv.ps1 | 279 +++++++++++------------ Build/Agent/Setup-InstallerBuild.ps1 | 85 ++++--- Build/Agent/Verify-FwDependencies.ps1 | 23 +- Build/Src/FwBuildTasks/Make.cs | 73 ++---- Setup-Developer-Machine.ps1 | 43 ++-- 8 files changed, 566 insertions(+), 510 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 521141af5a..1ecd074674 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -77,7 +77,6 @@ "resharper.build.useResharperBuild": false, "resharper.build.restorePackagesOnBuild": true, "resharper.build.smartNugetRestore": true, - "resharper.build.customMsbuildPath": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\amd64\\MSBuild.exe", // Git Graph extension settings "git-graph.dialog.merge.noFastForward": false, "git-graph.dialog.merge.squashCommits": false, diff --git a/Build/Agent/FwBuildEnvironment.psm1 b/Build/Agent/FwBuildEnvironment.psm1 index 7d775afb64..3e35080796 100644 --- a/Build/Agent/FwBuildEnvironment.psm1 +++ b/Build/Agent/FwBuildEnvironment.psm1 @@ -14,54 +14,217 @@ # VS Environment Functions # ============================================================================= -function Get-PreferredVcToolBinPath { +function Get-VsWherePath { <# .SYNOPSIS - Returns the preferred HostX64\x64 MSVC tool bin directory. + Returns the path to the Microsoft-provided vswhere executable. #> - if (-not $env:VCINSTALLDIR) { + $candidates = @() + if ($env:ProgramFiles) { + $candidates += (Join-Path -Path $env:ProgramFiles -ChildPath 'Microsoft Visual Studio\Installer\vswhere.exe') + } + + $programFilesX86 = ${env:ProgramFiles(x86)} + if ($programFilesX86) { + $candidates += (Join-Path -Path $programFilesX86 -ChildPath 'Microsoft Visual Studio\Installer\vswhere.exe') + } + + foreach ($candidate in $candidates | Select-Object -Unique) { + if (Test-Path $candidate) { + return $candidate + } + } + + return $null +} + +function Get-VsInstallationInfo { + <# + .SYNOPSIS + Returns installation metadata for the latest matching Visual Studio instance. + #> + param( + [string[]]$Requires = @() + ) + + $vsWhere = Get-VsWherePath + if (-not $vsWhere) { return $null } - $toolsRoot = Join-Path $env:VCINSTALLDIR 'Tools\MSVC' - if (-not (Test-Path $toolsRoot)) { + $vsWhereArgs = @('-latest', '-products', '*') + if ($Requires -and $Requires.Count -gt 0) { + $vsWhereArgs += '-requires' + $vsWhereArgs += $Requires + } + + $installationPath = & $vsWhere @vsWhereArgs -property installationPath + if (-not $installationPath) { return $null } - $versionSort = { - $parsedVersion = [version]'0.0' - $isVersion = [version]::TryParse($_.Name, [ref]$parsedVersion) - if ($isVersion) { - return $parsedVersion - } + $displayVersion = & $vsWhere @vsWhereArgs -property catalog_productDisplayVersion + + return [pscustomobject]@{ + VsWherePath = $vsWhere + InstallationPath = $installationPath + DisplayVersion = $displayVersion + } +} + +function Get-VsToolchainInfo { + <# + .SYNOPSIS + Returns derived toolchain paths for the latest matching Visual Studio instance. + #> + param( + [string[]]$Requires = @('Microsoft.Component.MSBuild') + ) + + $vsInfo = Get-VsInstallationInfo -Requires $Requires + if (-not $vsInfo) { + return $null + } + + $installationPath = $vsInfo.InstallationPath + $vsDevCmdPath = Join-Path $installationPath 'Common7\Tools\VsDevCmd.bat' + if (-not (Test-Path $vsDevCmdPath)) { + $vsDevCmdPath = $null + } + + $msbuildCandidates = @( + (Join-Path $installationPath 'MSBuild\Current\Bin\amd64\MSBuild.exe'), + (Join-Path $installationPath 'MSBuild\Current\Bin\MSBuild.exe') + ) + $msbuildPath = $msbuildCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1 - return [version]'0.0' + $vsTestPath = Join-Path $installationPath 'Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe' + if (-not (Test-Path $vsTestPath)) { + $vsTestPath = $null } - $sortProperties = @( - @{ Expression = { - $parsedVersion = [version]'0.0' - [version]::TryParse($_.Name, [ref]$parsedVersion) - }; Descending = $true } - @{ Expression = $versionSort; Descending = $true } - @{ Expression = { $_.Name }; Descending = $true } + $vcInstallDir = Join-Path $installationPath 'VC' + if (-not (Test-Path $vcInstallDir)) { + $vcInstallDir = $null + } + + $vcTargetsPath = Join-Path $installationPath 'MSBuild\Microsoft\VC\v170' + if (-not (Test-Path $vcTargetsPath)) { + $vcTargetsPath = $null + } + + return [pscustomobject]@{ + VsWherePath = $vsInfo.VsWherePath + InstallationPath = $installationPath + DisplayVersion = $vsInfo.DisplayVersion + VsDevCmdPath = $vsDevCmdPath + MSBuildPath = $msbuildPath + VSTestPath = $vsTestPath + VcInstallDir = $vcInstallDir + VCTargetsPath = $vcTargetsPath + } +} + +function Get-VsDevEnvironmentVariables { + <# + .SYNOPSIS + Returns the environment variables produced by VsDevCmd.bat. + #> + param( + [string]$Architecture = 'amd64', + [string]$HostArchitecture = 'amd64', + [string[]]$Requires = @('Microsoft.Component.MSBuild', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64') ) - $preferred = Get-ChildItem -Path $toolsRoot -Directory -ErrorAction SilentlyContinue | - Sort-Object -Property $sortProperties | - ForEach-Object { Join-Path $_.FullName 'bin\HostX64\x64' } | - Where-Object { Test-Path $_ } | - Select-Object -First 1 + $toolchain = Get-VsToolchainInfo -Requires $Requires + if (-not $toolchain) { + return $null + } + + if (-not $toolchain.VsDevCmdPath) { + throw "Unable to locate VsDevCmd.bat under '$($toolchain.InstallationPath)'." + } + + $cmdArgs = "`"$($toolchain.VsDevCmdPath)`" -no_logo -arch=$Architecture -host_arch=$HostArchitecture && set" + $envOutput = & cmd.exe /c $cmdArgs 2>&1 + if ($LASTEXITCODE -ne 0) { + throw 'Failed to initialize Visual Studio environment' + } + + $variables = [ordered]@{} + foreach ($line in $envOutput) { + $parts = $line -split '=', 2 + if ($parts.Length -eq 2 -and $parts[0]) { + $variables[$parts[0]] = $parts[1] + } + } + + return [pscustomobject]@{ + Toolchain = $toolchain + Variables = [pscustomobject]$variables + } +} + +function Get-ActiveVcToolBinPath { + <# + .SYNOPSIS + Returns the HostX64\x64 tool bin directory for the active VC toolset. + #> + if (-not [string]::IsNullOrWhiteSpace($env:VCToolsInstallDir)) { + $preferred = Join-Path $env:VCToolsInstallDir 'bin\HostX64\x64' + if (Test-Path (Join-Path $preferred 'cl.exe')) { + return $preferred + } + } + + if (-not [string]::IsNullOrWhiteSpace($env:VCINSTALLDIR)) { + $legacy = Join-Path $env:VCINSTALLDIR 'bin' + if (Test-Path (Join-Path $legacy 'cl.exe')) { + return $legacy + } + } + + return $null +} + +function Test-VsDevEnvironmentActive { + <# + .SYNOPSIS + Returns true when a full VsDevCmd environment is already active. + #> + if ($env:OS -ne 'Windows_NT') { + return $false + } + + if ([string]::IsNullOrWhiteSpace($env:VSCMD_VER) -or [string]::IsNullOrWhiteSpace($env:VCToolsInstallDir)) { + return $false + } + + $activeVcToolPath = Get-ActiveVcToolBinPath + if (-not $activeVcToolPath) { + return $false + } + + $cl = Get-Command 'cl.exe' -ErrorAction SilentlyContinue + $nmake = Get-Command 'nmake.exe' -ErrorAction SilentlyContinue + if (-not $cl -or -not $nmake) { + return $false + } + + $normalizedToolPath = $activeVcToolPath.TrimEnd('\') + $clDirectory = (Split-Path -Parent $cl.Source).TrimEnd('\') + $nmakeDirectory = (Split-Path -Parent $nmake.Source).TrimEnd('\') - return $preferred + return [string]::Equals($clDirectory, $normalizedToolPath, [System.StringComparison]::OrdinalIgnoreCase) -and + [string]::Equals($nmakeDirectory, $normalizedToolPath, [System.StringComparison]::OrdinalIgnoreCase) } function Ensure-PreferredVcToolPath { <# .SYNOPSIS - Moves the preferred HostX64\x64 MSVC bin directory to the front of PATH. + Moves the active HostX64\x64 MSVC bin directory to the front of PATH. #> - $preferred = Get-PreferredVcToolBinPath + $preferred = Get-ActiveVcToolBinPath if (-not $preferred) { return } @@ -90,65 +253,52 @@ function Initialize-VsDevEnvironment { return } - if ($env:VCINSTALLDIR) { + if (Test-VsDevEnvironmentActive) { Ensure-PreferredVcToolPath Write-Host '[OK] Visual Studio environment already initialized' -ForegroundColor Green return } + if ($env:VCINSTALLDIR -or $env:VCToolsInstallDir -or $env:VSCMD_VER) { + Write-Host '[WARN] Partial Visual Studio environment detected. Reinitializing...' -ForegroundColor Yellow + } + Write-Host 'Initializing Visual Studio Developer environment...' -ForegroundColor Yellow - $vswhereCandidates = @() - if ($env:ProgramFiles) { - $pfVswhere = Join-Path -Path $env:ProgramFiles -ChildPath 'Microsoft Visual Studio\Installer\vswhere.exe' - if (Test-Path $pfVswhere) { $vswhereCandidates += $pfVswhere } - } - $programFilesX86 = ${env:ProgramFiles(x86)} - if ($programFilesX86) { - $pf86Vswhere = Join-Path -Path $programFilesX86 -ChildPath 'Microsoft Visual Studio\Installer\vswhere.exe' - if (Test-Path $pf86Vswhere) { $vswhereCandidates += $pf86Vswhere } - } + $vsToolchain = Get-VsToolchainInfo -Requires @('Microsoft.Component.MSBuild', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64') - if (-not $vswhereCandidates) { + if (-not $vsToolchain) { + $vsWhere = Get-VsWherePath Write-Host '' - Write-Host '[ERROR] Visual Studio 2017+ not found' -ForegroundColor Red - Write-Host ' Install from: https://visualstudio.microsoft.com/downloads/' -ForegroundColor Yellow - throw 'Visual Studio not found' - } + if (-not $vsWhere) { + Write-Host '[ERROR] Visual Studio 2017+ not found' -ForegroundColor Red + Write-Host ' Install from: https://visualstudio.microsoft.com/downloads/' -ForegroundColor Yellow + throw 'Visual Studio not found' + } - $vsInstallPath = & $vswhereCandidates[0] -latest -requires Microsoft.Component.MSBuild Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -products * -property installationPath - if (-not $vsInstallPath) { - Write-Host '' Write-Host '[ERROR] Visual Studio found but missing required C++ tools' -ForegroundColor Red Write-Host ' Please install the "Desktop development with C++" workload' -ForegroundColor Yellow throw 'Visual Studio C++ tools not found' } - $vsDevCmd = Join-Path -Path $vsInstallPath -ChildPath 'Common7\Tools\VsDevCmd.bat' - if (-not (Test-Path $vsDevCmd)) { - throw "Unable to locate VsDevCmd.bat under '$vsInstallPath'." - } - # x64-only build $arch = 'amd64' - $vsVersion = Split-Path (Split-Path (Split-Path (Split-Path $vsInstallPath))) -Leaf + $vsInstallPath = $vsToolchain.InstallationPath + $vsVersion = if ([string]::IsNullOrWhiteSpace($vsToolchain.DisplayVersion)) { + Split-Path (Split-Path (Split-Path (Split-Path $vsInstallPath))) -Leaf + } + else { + $vsToolchain.DisplayVersion + } Write-Host " Found Visual Studio $vsVersion at: $vsInstallPath" -ForegroundColor Gray Write-Host " Setting up environment for $arch..." -ForegroundColor Gray - $cmdArgs = "`"$vsDevCmd`" -no_logo -arch=$arch -host_arch=$arch && set" - $envOutput = & cmd.exe /c $cmdArgs 2>&1 - if ($LASTEXITCODE -ne 0) { - throw 'Failed to initialize Visual Studio environment' - } - - foreach ($line in $envOutput) { - $parts = $line -split '=', 2 - if ($parts.Length -eq 2 -and $parts[0]) { - Set-Item -Path "Env:$($parts[0])" -Value $parts[1] - } + $vsEnvironment = Get-VsDevEnvironmentVariables -Architecture $arch -HostArchitecture $arch + foreach ($variable in $vsEnvironment.Variables.PSObject.Properties) { + Set-Item -Path "Env:$($variable.Name)" -Value $variable.Value } - if (-not $env:VCINSTALLDIR) { + if (-not (Test-VsDevEnvironmentActive)) { throw 'Visual Studio C++ environment not configured' } @@ -227,6 +377,12 @@ function Get-MSBuildPath { if ($msbuildCmd) { return $msbuildCmd.Source } + + $toolchain = Get-VsToolchainInfo -Requires @('Microsoft.Component.MSBuild') + if ($toolchain -and $toolchain.MSBuildPath) { + return $toolchain.MSBuildPath + } + return 'msbuild' } @@ -320,26 +476,9 @@ function Get-VSTestPath { return $vstestFromPath.Source } - # Fall back to known installation paths - $programFilesX86 = ${env:ProgramFiles(x86)} - if (-not $programFilesX86) { $programFilesX86 = "C:\Program Files (x86)" } - - $vstestCandidates = @( - # BuildTools - "$programFilesX86\Microsoft Visual Studio\2022\BuildTools\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", - "C:\BuildTools\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", - # TestAgent (sometimes installed separately) - "$programFilesX86\Microsoft Visual Studio\2022\TestAgent\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", - # Full VS installations - "${env:ProgramFiles}\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", - "${env:ProgramFiles}\Microsoft Visual Studio\2022\Professional\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", - "${env:ProgramFiles}\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe" - ) - - foreach ($candidate in $vstestCandidates) { - if (Test-Path $candidate) { - return $candidate - } + $toolchain = Get-VsToolchainInfo -Requires @('Microsoft.Component.MSBuild') + if ($toolchain -and $toolchain.VSTestPath) { + return $toolchain.VSTestPath } return $null @@ -350,6 +489,11 @@ function Get-VSTestPath { # ============================================================================= Export-ModuleMember -Function @( + 'Get-VsWherePath', + 'Get-VsInstallationInfo', + 'Get-VsToolchainInfo', + 'Get-VsDevEnvironmentVariables', + 'Test-VsDevEnvironmentActive', 'Initialize-VsDevEnvironment', 'Test-CvtresCompatibility', 'Get-MSBuildPath', diff --git a/Build/Agent/Run-VsTests.ps1 b/Build/Agent/Run-VsTests.ps1 index 64ed1c1540..dc562f4d21 100644 --- a/Build/Agent/Run-VsTests.ps1 +++ b/Build/Agent/Run-VsTests.ps1 @@ -1,125 +1,121 @@ <# .SYNOPSIS - Run VSTest for FieldWorks test assemblies with proper result parsing. + Run VSTest for FieldWorks test assemblies with proper result parsing. .DESCRIPTION - This script runs vstest.console.exe on specified test DLLs and parses the results - to provide clear pass/fail/skip counts. It handles the InIsolation mode configured - in Test.runsettings and properly interprets exit codes. + This script runs vstest.console.exe on specified test DLLs and parses the results + to provide clear pass/fail/skip counts. It handles the InIsolation mode configured + in Test.runsettings and properly interprets exit codes. .PARAMETER TestDlls - Array of test DLL names (e.g., "FwUtilsTests.dll") or paths. - If just names are provided, looks in Output\Debug by default. + Array of test DLL names (e.g., "FwUtilsTests.dll") or paths. + If just names are provided, looks in Output\Debug by default. .PARAMETER OutputDir - Directory containing test DLLs. Defaults to Output\Debug. + Directory containing test DLLs. Defaults to Output\Debug. .PARAMETER Filter - Optional VSTest filter expression (e.g., "TestCategory!=Slow"). + Optional VSTest filter expression (e.g., "TestCategory!=Slow"). .PARAMETER Rebuild - If specified, rebuilds the test projects before running tests. + If specified, rebuilds the test projects before running tests. .PARAMETER All - If specified, runs all *Tests.dll files found in OutputDir. + If specified, runs all *Tests.dll files found in OutputDir. .EXAMPLE - .\Run-VsTests.ps1 -TestDlls FwUtilsTests.dll - Runs FwUtilsTests.dll and shows results. + .\Run-VsTests.ps1 -TestDlls FwUtilsTests.dll + Runs FwUtilsTests.dll and shows results. .EXAMPLE - .\Run-VsTests.ps1 -TestDlls FwUtilsTests.dll,xCoreTests.dll - Runs multiple test DLLs and shows aggregate results. + .\Run-VsTests.ps1 -TestDlls FwUtilsTests.dll,xCoreTests.dll + Runs multiple test DLLs and shows aggregate results. .EXAMPLE - .\Run-VsTests.ps1 -All - Runs all test DLLs in Output\Debug. + .\Run-VsTests.ps1 -All + Runs all test DLLs in Output\Debug. .EXAMPLE - .\Run-VsTests.ps1 -TestDlls FwUtilsTests.dll -Rebuild - Rebuilds the test project first, then runs tests. + .\Run-VsTests.ps1 -TestDlls FwUtilsTests.dll -Rebuild + Rebuilds the test project first, then runs tests. #> [CmdletBinding()] param( - [Parameter(Position = 0)] - [string[]]$TestDlls, + [Parameter(Position = 0)] + [string[]]$TestDlls, - [string]$OutputDir, + [string]$OutputDir, - [string]$Filter, + [string]$Filter, - [switch]$Rebuild, + [switch]$Rebuild, - [switch]$All + [switch]$All ) $ErrorActionPreference = 'Continue' # Don't stop on stderr output from vstest +Import-Module (Join-Path $PSScriptRoot 'FwBuildEnvironment.psm1') -Force # Find repo root (where FieldWorks.sln is) $repoRoot = $PSScriptRoot while ($repoRoot -and -not (Test-Path (Join-Path $repoRoot "FieldWorks.sln"))) { - $repoRoot = Split-Path $repoRoot -Parent + $repoRoot = Split-Path $repoRoot -Parent } if (-not $repoRoot) { - Write-Error "Could not find repository root (FieldWorks.sln)" - exit 1 + Write-Error "Could not find repository root (FieldWorks.sln)" + exit 1 } # Set defaults if (-not $OutputDir) { - $OutputDir = Join-Path $repoRoot "Output\Debug" + $OutputDir = Join-Path $repoRoot "Output\Debug" } $runSettings = Join-Path $repoRoot "Test.runsettings" -$vsTestPath = "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe" +$vsTestPath = Get-VSTestPath if (-not (Test-Path $vsTestPath)) { - # Try BuildTools path - $vsTestPath = "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe" -} - -if (-not (Test-Path $vsTestPath)) { - Write-Error "vstest.console.exe not found. Install Visual Studio 2022 or Build Tools." - exit 1 + Write-Error "vstest.console.exe not found. Install Visual Studio 2022 or Build Tools." + exit 1 } # Collect test DLLs if ($All) { - $TestDlls = Get-ChildItem $OutputDir -Filter "*Tests.dll" | - Where-Object { $_.Name -notmatch "\.resources\." } | - Select-Object -ExpandProperty Name - Write-Host "Found $($TestDlls.Count) test assemblies" -ForegroundColor Cyan + $TestDlls = Get-ChildItem $OutputDir -Filter "*Tests.dll" | + Where-Object { $_.Name -notmatch "\.resources\." } | + Select-Object -ExpandProperty Name + Write-Host "Found $($TestDlls.Count) test assemblies" -ForegroundColor Cyan } if (-not $TestDlls -or $TestDlls.Count -eq 0) { - Write-Host "Usage: Run-VsTests.ps1 [-TestDlls] [-All] [-Rebuild] [-Filter ]" -ForegroundColor Yellow - Write-Host "" - Write-Host "Examples:" - Write-Host " Run-VsTests.ps1 FwUtilsTests.dll" - Write-Host " Run-VsTests.ps1 FwUtilsTests.dll,xCoreTests.dll" - Write-Host " Run-VsTests.ps1 -All" - Write-Host " Run-VsTests.ps1 FwUtilsTests.dll -Rebuild" - exit 0 + Write-Host "Usage: Run-VsTests.ps1 [-TestDlls] [-All] [-Rebuild] [-Filter ]" -ForegroundColor Yellow + Write-Host "" + Write-Host "Examples:" + Write-Host " Run-VsTests.ps1 FwUtilsTests.dll" + Write-Host " Run-VsTests.ps1 FwUtilsTests.dll,xCoreTests.dll" + Write-Host " Run-VsTests.ps1 -All" + Write-Host " Run-VsTests.ps1 FwUtilsTests.dll -Rebuild" + exit 0 } # Rebuild if requested if ($Rebuild) { - Write-Host "Rebuilding test projects..." -ForegroundColor Cyan - foreach ($dll in $TestDlls) { - $dllName = [System.IO.Path]::GetFileNameWithoutExtension($dll) - $csprojPattern = Join-Path $repoRoot "Src\**\$dllName.csproj" - $csproj = Get-ChildItem -Path (Join-Path $repoRoot "Src") -Recurse -Filter "$dllName.csproj" | Select-Object -First 1 - - if ($csproj) { - Write-Host " Building $($csproj.Name)..." -ForegroundColor Gray - $buildOutput = dotnet build $csproj.FullName -c Debug --no-incremental -v q 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Warning "Build failed for $($csproj.Name)" - $buildOutput | Write-Host - } - } - } - Write-Host "" + Write-Host "Rebuilding test projects..." -ForegroundColor Cyan + foreach ($dll in $TestDlls) { + $dllName = [System.IO.Path]::GetFileNameWithoutExtension($dll) + $csprojPattern = Join-Path $repoRoot "Src\**\$dllName.csproj" + $csproj = Get-ChildItem -Path (Join-Path $repoRoot "Src") -Recurse -Filter "$dllName.csproj" | Select-Object -First 1 + + if ($csproj) { + Write-Host " Building $($csproj.Name)..." -ForegroundColor Gray + $buildOutput = dotnet build $csproj.FullName -c Debug --no-incremental -v q 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Warning "Build failed for $($csproj.Name)" + $buildOutput | Write-Host + } + } + } + Write-Host "" } # Run tests @@ -132,63 +128,63 @@ Write-Host "Running tests..." -ForegroundColor Cyan Write-Host "" foreach ($dll in $TestDlls) { - # Resolve full path - if (-not [System.IO.Path]::IsPathRooted($dll)) { - $dllPath = Join-Path $OutputDir $dll - } else { - $dllPath = $dll - } - - if (-not (Test-Path $dllPath)) { - Write-Warning "Not found: $dll" - continue - } - - $dllName = [System.IO.Path]::GetFileName($dllPath) - - # Build arguments - $args = @($dllPath, "/Settings:$runSettings") - if ($Filter) { - $args += "/TestCaseFilter:$Filter" - } - - # Run vstest - $output = & $vsTestPath @args 2>&1 - - # Parse results - $passed = ($output | Select-String "^\s+Passed").Count - $failed = ($output | Select-String "^\s+Failed").Count - $skipped = ($output | Select-String "^\s+Skipped").Count - $exitCode = $LASTEXITCODE - - $totalPassed += $passed - $totalFailed += $failed - $totalSkipped += $skipped - - # Determine status - if ($failed -gt 0) { - $status = "FAIL" - $color = "Red" - } elseif ($passed -eq 0 -and $skipped -eq 0) { - $status = "NONE" - $color = "Yellow" - } else { - $status = "PASS" - $color = "Green" - } - - # Output result - $resultLine = "{0,-40} {1,6} passed, {2,4} failed, {3,4} skipped [{4}]" -f $dllName, $passed, $failed, $skipped, $status - Write-Host $resultLine -ForegroundColor $color - - $results += [PSCustomObject]@{ - DLL = $dllName - Passed = $passed - Failed = $failed - Skipped = $skipped - Status = $status - Output = $output - } + # Resolve full path + if (-not [System.IO.Path]::IsPathRooted($dll)) { + $dllPath = Join-Path $OutputDir $dll + } else { + $dllPath = $dll + } + + if (-not (Test-Path $dllPath)) { + Write-Warning "Not found: $dll" + continue + } + + $dllName = [System.IO.Path]::GetFileName($dllPath) + + # Build arguments + $args = @($dllPath, "/Settings:$runSettings") + if ($Filter) { + $args += "/TestCaseFilter:$Filter" + } + + # Run vstest + $output = & $vsTestPath @args 2>&1 + + # Parse results + $passed = ($output | Select-String "^\s+Passed").Count + $failed = ($output | Select-String "^\s+Failed").Count + $skipped = ($output | Select-String "^\s+Skipped").Count + $exitCode = $LASTEXITCODE + + $totalPassed += $passed + $totalFailed += $failed + $totalSkipped += $skipped + + # Determine status + if ($failed -gt 0) { + $status = "FAIL" + $color = "Red" + } elseif ($passed -eq 0 -and $skipped -eq 0) { + $status = "NONE" + $color = "Yellow" + } else { + $status = "PASS" + $color = "Green" + } + + # Output result + $resultLine = "{0,-40} {1,6} passed, {2,4} failed, {3,4} skipped [{4}]" -f $dllName, $passed, $failed, $skipped, $status + Write-Host $resultLine -ForegroundColor $color + + $results += [PSCustomObject]@{ + DLL = $dllName + Passed = $passed + Failed = $failed + Skipped = $skipped + Status = $status + Output = $output + } } # Summary @@ -196,22 +192,22 @@ Write-Host "" Write-Host ("=" * 70) -ForegroundColor Cyan $summaryLine = "TOTAL: {0} passed, {1} failed, {2} skipped" -f $totalPassed, $totalFailed, $totalSkipped if ($totalFailed -gt 0) { - Write-Host $summaryLine -ForegroundColor Red - $exitCode = 1 + Write-Host $summaryLine -ForegroundColor Red + $exitCode = 1 } else { - Write-Host $summaryLine -ForegroundColor Green - $exitCode = 0 + Write-Host $summaryLine -ForegroundColor Green + $exitCode = 0 } # Show failures if any if ($totalFailed -gt 0) { - Write-Host "" - Write-Host "Failed tests:" -ForegroundColor Red - foreach ($r in $results | Where-Object { $_.Failed -gt 0 }) { - Write-Host "" - Write-Host "=== $($r.DLL) ===" -ForegroundColor Yellow - $r.Output | Select-String "^\s+Failed" -Context 0,5 | ForEach-Object { Write-Host $_ } - } + Write-Host "" + Write-Host "Failed tests:" -ForegroundColor Red + foreach ($r in $results | Where-Object { $_.Failed -gt 0 }) { + Write-Host "" + Write-Host "=== $($r.DLL) ===" -ForegroundColor Yellow + $r.Output | Select-String "^\s+Failed" -Context 0,5 | ForEach-Object { Write-Host $_ } + } } exit $exitCode diff --git a/Build/Agent/Setup-FwBuildEnv.ps1 b/Build/Agent/Setup-FwBuildEnv.ps1 index a95fe4c6f5..91ad908451 100644 --- a/Build/Agent/Setup-FwBuildEnv.ps1 +++ b/Build/Agent/Setup-FwBuildEnv.ps1 @@ -1,91 +1,93 @@ <# .SYNOPSIS - Configures the FieldWorks build environment on Windows. + Configures the FieldWorks build environment on Windows. .DESCRIPTION - Sets up environment variables and PATH entries needed for FieldWorks builds. - Can be run locally for testing or called from GitHub Actions workflows. + Sets up environment variables and PATH entries needed for FieldWorks builds. + Can be run locally for testing or called from GitHub Actions workflows. - This script is idempotent - safe to run multiple times. + This script is idempotent - safe to run multiple times. .PARAMETER OutputGitHubEnv - If specified, outputs environment variables to GITHUB_ENV and GITHUB_PATH - for use in GitHub Actions. Otherwise, sets them in the current process. + If specified, outputs environment variables to GITHUB_ENV and GITHUB_PATH + for use in GitHub Actions. Otherwise, sets them in the current process. .PARAMETER Verify - If specified, runs verification checks and exits with non-zero on failure. + If specified, runs verification checks and exits with non-zero on failure. .EXAMPLE - # Local testing - just configure current session - .\Build\Agent\Setup-FwBuildEnv.ps1 + # Local testing - just configure current session + .\Build\Agent\Setup-FwBuildEnv.ps1 .EXAMPLE - # GitHub Actions - output to GITHUB_ENV - .\Build\Agent\Setup-FwBuildEnv.ps1 -OutputGitHubEnv + # GitHub Actions - output to GITHUB_ENV + .\Build\Agent\Setup-FwBuildEnv.ps1 -OutputGitHubEnv .EXAMPLE - # Verify all dependencies are available - .\Build\Agent\Setup-FwBuildEnv.ps1 -Verify + # Verify all dependencies are available + .\Build\Agent\Setup-FwBuildEnv.ps1 -Verify #> [CmdletBinding()] param( - [switch]$OutputGitHubEnv, - [switch]$Verify + [switch]$OutputGitHubEnv, + [switch]$Verify ) $ErrorActionPreference = 'Stop' +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +Import-Module (Join-Path $scriptDir 'FwBuildEnvironment.psm1') -Force function Write-Status { - param([string]$Message, [string]$Status = "INFO", [string]$Color = "White") - $prefix = switch ($Status) { - "OK" { "[OK] "; $Color = "Green" } - "FAIL" { "[FAIL] "; $Color = "Red" } - "WARN" { "[WARN] "; $Color = "Yellow" } - "SKIP" { "[SKIP] "; $Color = "DarkGray" } - default { "[INFO] " } - } - Write-Host "$prefix$Message" -ForegroundColor $Color + param([string]$Message, [string]$Status = "INFO", [string]$Color = "White") + $prefix = switch ($Status) { + "OK" { "[OK] "; $Color = "Green" } + "FAIL" { "[FAIL] "; $Color = "Red" } + "WARN" { "[WARN] "; $Color = "Yellow" } + "SKIP" { "[SKIP] "; $Color = "DarkGray" } + default { "[INFO] " } + } + Write-Host "$prefix$Message" -ForegroundColor $Color } function Set-EnvVar { - param([string]$Name, [string]$Value) - - if ($OutputGitHubEnv -and $env:GITHUB_ENV) { - # GitHub Actions format - "$Name=$Value" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - Write-Status "Set $Name (GITHUB_ENV)" - } - else { - # Local session - [Environment]::SetEnvironmentVariable($Name, $Value, 'Process') - Write-Status "Set $Name = $Value" - } + param([string]$Name, [string]$Value) + + if ($OutputGitHubEnv -and $env:GITHUB_ENV) { + # GitHub Actions format + "$Name=$Value" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + Write-Status "Set $Name (GITHUB_ENV)" + } + else { + # Local session + [Environment]::SetEnvironmentVariable($Name, $Value, 'Process') + Write-Status "Set $Name = $Value" + } } function Add-ToPath { - param([string]$Path) - - if (-not (Test-Path $Path)) { - Write-Status "Path does not exist: $Path" -Status "WARN" - return $false - } - - if ($OutputGitHubEnv -and $env:GITHUB_PATH) { - $Path | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - Write-Status "Added to PATH (GITHUB_PATH): $Path" - } - else { - $currentPath = [Environment]::GetEnvironmentVariable('PATH', 'Process') - if ($currentPath -notlike "*$Path*") { - [Environment]::SetEnvironmentVariable('PATH', "$Path;$currentPath", 'Process') - Write-Status "Added to PATH: $Path" - } - else { - Write-Status "Already in PATH: $Path" -Status "SKIP" - } - } - return $true + param([string]$Path) + + if (-not (Test-Path $Path)) { + Write-Status "Path does not exist: $Path" -Status "WARN" + return $false + } + + if ($OutputGitHubEnv -and $env:GITHUB_PATH) { + $Path | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + Write-Status "Added to PATH (GITHUB_PATH): $Path" + } + else { + $currentPath = [Environment]::GetEnvironmentVariable('PATH', 'Process') + if ($currentPath -notlike "*$Path*") { + [Environment]::SetEnvironmentVariable('PATH', "$Path;$currentPath", 'Process') + Write-Status "Added to PATH: $Path" + } + else { + Write-Status "Already in PATH: $Path" -Status "SKIP" + } + } + return $true } # ============================================================================ @@ -97,7 +99,6 @@ Write-Host "OutputGitHubEnv: $OutputGitHubEnv" Write-Host "Verify: $Verify" Write-Host "" -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $repoRoot = Resolve-Path "$scriptDir\..\.." # Set FW_ROOT_CODE_DIR and FW_ROOT_DATA_DIR for DirectoryFinder fallback @@ -107,9 +108,10 @@ Set-EnvVar -Name "FW_ROOT_CODE_DIR" -Value $distFiles Set-EnvVar -Name "FW_ROOT_DATA_DIR" -Value $distFiles $results = @{ - VSPath = $null - MSBuildPath = $null - Errors = @() + VSPath = $null + MSBuildPath = $null + VSTestPath = $null + Errors = @() } # ---------------------------------------------------------------------------- @@ -117,31 +119,38 @@ $results = @{ # ---------------------------------------------------------------------------- Write-Host "--- Locating Visual Studio ---" -ForegroundColor Cyan -$vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -if (Test-Path $vsWhere) { - $vsPath = & $vsWhere -latest -requires Microsoft.Component.MSBuild -products * -property installationPath - if ($vsPath) { - Write-Status "Visual Studio: $vsPath" -Status "OK" - $results.VSPath = $vsPath - - # Set VS environment variables - Set-EnvVar -Name "VSINSTALLDIR" -Value "$vsPath\" - Set-EnvVar -Name "VCINSTALLDIR" -Value "$vsPath\VC\" - - # VCTargetsPath for C++ builds - $vcTargets = Join-Path $vsPath 'MSBuild\Microsoft\VC\v170' - if (Test-Path $vcTargets) { - Set-EnvVar -Name "VCTargetsPath" -Value $vcTargets - } - } - else { - Write-Status "Visual Studio not found via vswhere" -Status "FAIL" - $results.Errors += "Visual Studio not found" - } +$toolchain = Get-VsToolchainInfo -Requires @('Microsoft.Component.MSBuild', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64') +if ($toolchain) { + $results.VSPath = $toolchain.InstallationPath + $results.MSBuildPath = $toolchain.MSBuildPath + $results.VSTestPath = $toolchain.VSTestPath + + if ([string]::IsNullOrWhiteSpace($toolchain.DisplayVersion)) { + Write-Status "Visual Studio: $($toolchain.InstallationPath)" -Status "OK" + } + else { + Write-Status "Visual Studio $($toolchain.DisplayVersion): $($toolchain.InstallationPath)" -Status "OK" + } + + # Export installation hints only; build/test scripts still self-initialize via VsDevCmd. + Set-EnvVar -Name "VSINSTALLDIR" -Value ($toolchain.InstallationPath.TrimEnd('\') + '\') + if ($toolchain.VcInstallDir) { + Set-EnvVar -Name "VCINSTALLDIR" -Value ($toolchain.VcInstallDir.TrimEnd('\') + '\') + } + if ($toolchain.VCTargetsPath) { + Set-EnvVar -Name "VCTargetsPath" -Value $toolchain.VCTargetsPath + } } else { - Write-Status "vswhere.exe not found at: $vsWhere" -Status "FAIL" - $results.Errors += "vswhere.exe not found" + $vsWhere = Get-VsWherePath + if ($vsWhere) { + Write-Status "Visual Studio with MSBuild and C++ tools not found" -Status "FAIL" + $results.Errors += "Visual Studio with MSBuild and C++ tools not found" + } + else { + Write-Status "vswhere.exe not found" -Status "FAIL" + $results.Errors += "vswhere.exe not found" + } } # ---------------------------------------------------------------------------- @@ -150,27 +159,13 @@ else { Write-Host "" Write-Host "--- Locating MSBuild ---" -ForegroundColor Cyan -$msbuildCandidates = @() -if ($results.VSPath) { - $msbuildCandidates += Join-Path $results.VSPath 'MSBuild\Current\Bin\MSBuild.exe' - $msbuildCandidates += Join-Path $results.VSPath 'MSBuild\Current\Bin\amd64\MSBuild.exe' -} -$msbuildCandidates += "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe" -$msbuildCandidates += "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe" -$msbuildCandidates += "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe" -$msbuildCandidates += "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" - -foreach ($candidate in $msbuildCandidates) { - if (Test-Path $candidate) { - $results.MSBuildPath = $candidate - Write-Status "MSBuild: $candidate" -Status "OK" - break - } +if ($results.MSBuildPath) { + Write-Status "MSBuild: $($results.MSBuildPath)" -Status "OK" + Add-ToPath -Path (Split-Path -Parent $results.MSBuildPath) | Out-Null } - -if (-not $results.MSBuildPath) { - Write-Status "MSBuild not found" -Status "FAIL" - $results.Errors += "MSBuild not found" +else { + Write-Status "MSBuild not found" -Status "FAIL" + $results.Errors += "MSBuild not found" } # ---------------------------------------------------------------------------- @@ -180,21 +175,21 @@ Write-Host "" Write-Host "--- Configuring PATH ---" -ForegroundColor Cyan $netfxPaths = @( - "${env:ProgramFiles(x86)}\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8.1 Tools", - "${env:ProgramFiles(x86)}\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools", - "${env:ProgramFiles(x86)}\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools" + "${env:ProgramFiles(x86)}\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8.1 Tools", + "${env:ProgramFiles(x86)}\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools", + "${env:ProgramFiles(x86)}\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools" ) $foundNetfx = $false foreach ($p in $netfxPaths) { - if (Test-Path $p) { - Add-ToPath -Path $p | Out-Null - $foundNetfx = $true - break - } + if (Test-Path $p) { + Add-ToPath -Path $p | Out-Null + $foundNetfx = $true + break + } } if (-not $foundNetfx) { - Write-Status "NETFX tools not found (sn.exe may not work)" -Status "WARN" + Write-Status "NETFX tools not found (sn.exe may not work)" -Status "WARN" } # ---------------------------------------------------------------------------- @@ -203,37 +198,12 @@ if (-not $foundNetfx) { Write-Host "" Write-Host "--- Locating VSTest ---" -ForegroundColor Cyan -$vstestPath = $null -$vstestCandidates = @() - -# Check VS installation paths first -if ($results.VSPath) { - $vstestCandidates += Join-Path $results.VSPath 'Common7\IDE\CommonExtensions\Microsoft\TestWindow' -} - -# Add known installation paths (BuildTools, TestAgent, etc.) -$vstestCandidates += @( - 'C:\BuildTools\Common7\IDE\CommonExtensions\Microsoft\TestWindow', - "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\Common7\IDE\CommonExtensions\Microsoft\TestWindow", - "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\TestAgent\Common7\IDE\CommonExtensions\Microsoft\TestWindow", - "${env:ProgramFiles}\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow", - "${env:ProgramFiles}\Microsoft Visual Studio\2022\Professional\Common7\IDE\CommonExtensions\Microsoft\TestWindow", - "${env:ProgramFiles}\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\TestWindow" -) - -foreach ($candidate in $vstestCandidates) { - if ($candidate -and (Test-Path (Join-Path $candidate 'vstest.console.exe'))) { - $vstestPath = $candidate - Add-ToPath -Path $vstestPath | Out-Null - break - } -} - -if (-not $vstestPath) { - Write-Status "vstest.console.exe not found" -Status "WARN" +if ($results.VSTestPath) { + Add-ToPath -Path (Split-Path -Parent $results.VSTestPath) | Out-Null + Write-Status "VSTest: $($results.VSTestPath)" -Status "OK" } else { - Write-Status "VSTest: $vstestPath" -Status "OK" + Write-Status "vstest.console.exe not found" -Status "WARN" } # ---------------------------------------------------------------------------- @@ -244,23 +214,24 @@ Write-Host "=== Setup Complete ===" -ForegroundColor Cyan # Output key paths for GitHub Actions if ($OutputGitHubEnv -and $env:GITHUB_OUTPUT) { - "msbuild-path=$($results.MSBuildPath)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - "vs-install-path=$($results.VSPath)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "msbuild-path=$($results.MSBuildPath)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "vs-install-path=$($results.VSPath)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "vstest-path=$($results.VSTestPath)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append } # Return results object for programmatic use if ($results.Errors.Count -gt 0) { - Write-Host "" - Write-Status "Setup completed with errors:" -Status "FAIL" - foreach ($err in $results.Errors) { - Write-Host " - $err" -ForegroundColor Red - } - if ($Verify) { - exit 1 - } + Write-Host "" + Write-Status "Setup completed with errors:" -Status "FAIL" + foreach ($err in $results.Errors) { + Write-Host " - $err" -ForegroundColor Red + } + if ($Verify) { + exit 1 + } } else { - Write-Status "All environment configuration successful" -Status "OK" + Write-Status "All environment configuration successful" -Status "OK" } return $results diff --git a/Build/Agent/Setup-InstallerBuild.ps1 b/Build/Agent/Setup-InstallerBuild.ps1 index 32f6e4ae92..35552da080 100644 --- a/Build/Agent/Setup-InstallerBuild.ps1 +++ b/Build/Agent/Setup-InstallerBuild.ps1 @@ -47,6 +47,7 @@ param( $ErrorActionPreference = 'Stop' $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $repoRoot = Split-Path -Parent (Split-Path -Parent $scriptDir) +Import-Module (Join-Path $scriptDir 'FwBuildEnvironment.psm1') -Force Write-Host "========================================" -ForegroundColor Cyan Write-Host " FieldWorks Installer Build Setup" -ForegroundColor Cyan @@ -55,6 +56,10 @@ Write-Host "" $issues = @() $warnings = @() +$vsDevEnvActive = $false +$restoreWrappedCommand = $null +$buildWrappedCommand = $null +$patchWrappedCommand = $null #region WiX Toolset Validation @@ -90,57 +95,41 @@ if (Test-Path $heatFromRepoPackages) { Write-Host "`n--- Checking Visual Studio / MSBuild ---" -ForegroundColor Yellow -$vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -$vsInstall = $null -$vsDevEnvActive = $false +$toolchain = Get-VsToolchainInfo -Requires @('Microsoft.Component.MSBuild', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64') +$vsDevEnvActive = Test-VsDevEnvironmentActive -if (Test-Path $vsWhere) { - $vsInstall = & $vsWhere -latest -property installationPath 2>$null - if ($vsInstall) { - $vsVersion = & $vsWhere -latest -property catalog_productDisplayVersion 2>$null +if ($toolchain) { + $vsVersion = if ([string]::IsNullOrWhiteSpace($toolchain.DisplayVersion)) { 'unknown version' } else { $toolchain.DisplayVersion } Write-Host "[OK] Visual Studio 2022: $vsVersion" -ForegroundColor Green - # Check for MSBuild - $msbuildPath = Join-Path $vsInstall "MSBuild\Current\Bin\MSBuild.exe" - if (Test-Path $msbuildPath) { - Write-Host "[OK] MSBuild found: $msbuildPath" -ForegroundColor Green + if ($toolchain.MSBuildPath) { + Write-Host "[OK] MSBuild found: $($toolchain.MSBuildPath)" -ForegroundColor Green } else { $issues += "MSBuild not found in VS installation" } - # Check for VsDevCmd - $vsDevCmd = Join-Path $vsInstall "Common7\Tools\VsDevCmd.bat" - $launchVsDevShell = Join-Path $vsInstall "Common7\Tools\Launch-VsDevShell.ps1" - if ((Test-Path $vsDevCmd) -or (Test-Path $launchVsDevShell)) { + if ($toolchain.VsDevCmdPath) { Write-Host "[OK] VS Developer environment scripts available" -ForegroundColor Green + $restoreWrappedCommand = 'cmd /c "call ""{0}"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:RestorePackages /p:Configuration=Debug /p:Platform=x64"' -f $toolchain.VsDevCmdPath + $buildWrappedCommand = 'cmd /c "call ""{0}"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:BuildInstaller /p:Configuration=Debug /p:Platform=x64 /p:config=release /m /v:n"' -f $toolchain.VsDevCmdPath + $patchWrappedCommand = 'cmd /c "call ""{0}"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:BuildPatchInstaller /p:Configuration=Debug /p:Platform=x64 /p:config=release /m /v:n"' -f $toolchain.VsDevCmdPath + } else { + $issues += "VsDevCmd.bat not found in VS installation" } - # Check if VS Developer environment is active (nmake in PATH) - $nmake = Get-Command nmake.exe -ErrorAction SilentlyContinue - if ($nmake) { - Write-Host "[OK] VS Developer environment active (nmake in PATH)" -ForegroundColor Green - $vsDevEnvActive = $true + if ($vsDevEnvActive) { + Write-Host "[OK] VS Developer environment active" -ForegroundColor Green } else { - # Check if nmake exists in VS installation - $nmakePath = Join-Path $vsInstall "VC\Tools\MSVC\*\bin\Hostx64\x64\nmake.exe" - $nmakeExists = Get-ChildItem -Path $nmakePath -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($nmakeExists) { Write-Host "[WARN] VS Developer environment NOT active" -ForegroundColor Yellow - Write-Host " nmake.exe exists but is not in PATH" -ForegroundColor Yellow - Write-Host " Run builds from VS Developer Command Prompt or use:" -ForegroundColor Yellow - Write-Host " cmd /c `"call `"$vsDevCmd`" -arch=amd64 && msbuild ...`"" -ForegroundColor Cyan - $warnings += "VS Developer environment not active (nmake not in PATH)" - } else { - Write-Host "[MISSING] C++ build tools (nmake.exe) not found" -ForegroundColor Red - Write-Host " Install 'Desktop development with C++' workload in VS Installer" -ForegroundColor Red - $issues += "C++ build tools not installed (nmake.exe missing)" - } - } - } else { - $issues += "Visual Studio 2022 not installed" + Write-Host " Run builds from VS Developer Command Prompt or use the detected VsDevCmd wrapper commands below" -ForegroundColor Yellow + $warnings += "VS Developer environment not active" } } else { + if (Get-VsWherePath) { + $issues += "Visual Studio 2022 with MSBuild and C++ tools not installed" + } else { $issues += "Visual Studio Installer not found" + } } #endregion @@ -344,17 +333,21 @@ if ($issues.Count -eq 0) { Write-Host " # Option 1: Open VS Developer Command Prompt and run commands there" -ForegroundColor Gray Write-Host " # Option 2: Use these one-liner commands from any PowerShell:" -ForegroundColor Gray Write-Host "" - Write-Host " # Restore packages" -ForegroundColor Gray - Write-Host ' cmd /c "call ""C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:RestorePackages /p:Configuration=Release /p:Platform=x64"' -ForegroundColor Cyan - Write-Host "" - Write-Host " # Build base installer" -ForegroundColor Gray - Write-Host ' cmd /c "call ""C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:BuildInstaller /p:Configuration=Release /p:Platform=x64 /p:config=release /m /v:n"' -ForegroundColor Cyan - Write-Host "" - - if ($SetupPatch) { - Write-Host " # Build patch installer" -ForegroundColor Gray - Write-Host ' cmd /c "call ""C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:BuildPatchInstaller /p:Configuration=Release /p:Platform=x64 /p:config=release /m /v:n"' -ForegroundColor Cyan + if ($restoreWrappedCommand -and $buildWrappedCommand) { + Write-Host " # Restore packages" -ForegroundColor Gray + Write-Host " $restoreWrappedCommand" -ForegroundColor Cyan Write-Host "" + Write-Host " # Build base installer" -ForegroundColor Gray + Write-Host " $buildWrappedCommand" -ForegroundColor Cyan + Write-Host "" + + if ($SetupPatch -and $patchWrappedCommand) { + Write-Host " # Build patch installer" -ForegroundColor Gray + Write-Host " $patchWrappedCommand" -ForegroundColor Cyan + Write-Host "" + } + } else { + Write-Host " Unable to derive a VsDevCmd wrapper command for this installation." -ForegroundColor Yellow } } diff --git a/Build/Agent/Verify-FwDependencies.ps1 b/Build/Agent/Verify-FwDependencies.ps1 index 18510ffced..626a8bbccd 100644 --- a/Build/Agent/Verify-FwDependencies.ps1 +++ b/Build/Agent/Verify-FwDependencies.ps1 @@ -62,6 +62,7 @@ param( ) $ErrorActionPreference = 'Stop' +Import-Module (Join-Path $PSScriptRoot 'FwBuildEnvironment.psm1') -Force function Test-Dependency { param( @@ -164,12 +165,14 @@ $results += Test-Dependency -Name "Windows SDK" -Check { # Visual Studio / MSBuild $results += Test-Dependency -Name "Visual Studio 2022" -Check { - $vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - if (-not (Test-Path $vsWhere)) { throw "vswhere.exe not found" } - $vsPath = & $vsWhere -latest -requires Microsoft.Component.MSBuild -products * -property installationPath - if (-not $vsPath) { throw "No VS installation with MSBuild found" } - $version = & $vsWhere -latest -property catalog_productDisplayVersion - return "Version $version at $vsPath" + $vsInfo = Get-VsInstallationInfo -Requires @('Microsoft.Component.MSBuild', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64') + if (-not $vsInfo) { + $vsWhere = Get-VsWherePath + if (-not $vsWhere) { throw "vswhere.exe not found" } + throw "No VS installation with MSBuild and C++ tools found" + } + + return "Version $($vsInfo.DisplayVersion) at $($vsInfo.InstallationPath)" } # MSBuild @@ -179,11 +182,9 @@ $results += Test-Dependency -Name "MSBuild" -Check { $version = (& msbuild.exe -version -nologo 2>$null | Select-Object -Last 1) return "Version $version" } - # Try via vswhere - $vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - $vsPath = & $vsWhere -latest -requires Microsoft.Component.MSBuild -products * -property installationPath 2>$null - if ($vsPath) { - $msbuildPath = Join-Path $vsPath 'MSBuild\Current\Bin\MSBuild.exe' + $vsInfo = Get-VsInstallationInfo -Requires @('Microsoft.Component.MSBuild') + if ($vsInfo) { + $msbuildPath = Join-Path $vsInfo.InstallationPath 'MSBuild\Current\Bin\MSBuild.exe' if (Test-Path $msbuildPath) { return "Found at $msbuildPath (not in PATH)" } diff --git a/Build/Src/FwBuildTasks/Make.cs b/Build/Src/FwBuildTasks/Make.cs index 933b5999fd..4a91985688 100644 --- a/Build/Src/FwBuildTasks/Make.cs +++ b/Build/Src/FwBuildTasks/Make.cs @@ -111,65 +111,21 @@ protected override string ToolName } } - private static string FindVisualStudioToolPath(string vcInstallDir, string toolName) + private static string FindToolInDirectory(string directory, string toolName) { - if (String.IsNullOrEmpty(vcInstallDir) || !Directory.Exists(vcInstallDir)) + if (String.IsNullOrEmpty(directory) || !Directory.Exists(directory)) return null; - string toolsRoot = Path.Combine(vcInstallDir, "Tools", "MSVC"); - if (!Directory.Exists(toolsRoot)) - return null; - - string[] versionDirs = Directory.GetDirectories(toolsRoot); - Array.Sort(versionDirs, CompareVersionDirectories); - - foreach (string versionDir in versionDirs) - { - string[] candidateDirs = - { - Path.Combine(versionDir, "bin", "Hostx64", "x64"), - Path.Combine(versionDir, "bin", "Hostx64", "x86"), - Path.Combine(versionDir, "bin", "Hostx86", "x86"), - Path.Combine(versionDir, "bin", "Hostx86", "x64") - }; - - foreach (string candidateDir in candidateDirs) - { - if (File.Exists(Path.Combine(candidateDir, toolName))) - return candidateDir; - } - } + if (File.Exists(Path.Combine(directory, toolName))) + return directory; return null; } - private static int CompareVersionDirectories(string left, string right) - { - string leftName = Path.GetFileName(left); - string rightName = Path.GetFileName(right); - - Version leftVersion; - Version rightVersion; - bool leftIsVersion = Version.TryParse(leftName, out leftVersion); - bool rightIsVersion = Version.TryParse(rightName, out rightVersion); - - if (leftIsVersion && rightIsVersion) - { - int versionComparison = rightVersion.CompareTo(leftVersion); - if (versionComparison != 0) - return versionComparison; - } - else if (leftIsVersion != rightIsVersion) - { - return rightIsVersion.CompareTo(leftIsVersion); - } - - return StringComparer.OrdinalIgnoreCase.Compare(rightName, leftName); - } - private void CheckToolPath() { string path = Environment.GetEnvironmentVariable("PATH"); + string vcToolsInstallDir = Environment.GetEnvironmentVariable("VCToolsInstallDir"); string vcInstallDir = Environment.GetEnvironmentVariable("VCINSTALLDIR"); //Console.WriteLine("DEBUG Make Task: PATH='{0}'", path); string makePath = ToolPath == null ? String.Empty : ToolPath.Trim(); @@ -184,7 +140,7 @@ private void CheckToolPath() if (File.Exists(Path.Combine(ToolPath, ToolName))) return; } - string[] splitPath = path.Split(new char[] { Path.PathSeparator }); + string[] splitPath = String.IsNullOrEmpty(path) ? new string[0] : path.Split(new[] { Path.PathSeparator }); foreach (var dir in splitPath) { if (File.Exists(Path.Combine(dir, ToolName))) @@ -193,18 +149,21 @@ private void CheckToolPath() return; } } - // Fall Back to the install directory (if VCINSTALLDIR is set) - if (!String.IsNullOrEmpty(vcInstallDir)) + if (!String.IsNullOrEmpty(vcToolsInstallDir)) { - string visualStudioToolPath = FindVisualStudioToolPath(vcInstallDir, ToolName); - if (!String.IsNullOrEmpty(visualStudioToolPath)) + string activeToolPath = FindToolInDirectory(Path.Combine(vcToolsInstallDir, "bin", "Hostx64", "x64"), ToolName); + if (!String.IsNullOrEmpty(activeToolPath)) { - ToolPath = visualStudioToolPath; + ToolPath = activeToolPath; return; } + } - string legacyToolPath = Path.Combine(vcInstallDir, "bin"); - if (File.Exists(Path.Combine(legacyToolPath, ToolName))) + // Fall back to the legacy VC install directory (if VCINSTALLDIR is set) + if (!String.IsNullOrEmpty(vcInstallDir)) + { + string legacyToolPath = FindToolInDirectory(Path.Combine(vcInstallDir, "bin"), ToolName); + if (!String.IsNullOrEmpty(legacyToolPath)) { ToolPath = legacyToolPath; return; diff --git a/Setup-Developer-Machine.ps1 b/Setup-Developer-Machine.ps1 index e2526a7520..05d96cee49 100644 --- a/Setup-Developer-Machine.ps1 +++ b/Setup-Developer-Machine.ps1 @@ -26,6 +26,8 @@ param( $ErrorActionPreference = 'Stop' $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +Import-Module (Join-Path $scriptDir 'Build\Agent\FwBuildEnvironment.psm1') -Force +$vsToolchain = $null Write-Host "========================================" -ForegroundColor Cyan Write-Host " FieldWorks Developer Machine Setup" -ForegroundColor Cyan @@ -57,22 +59,16 @@ if ($git) { # Check Visual Studio 2022 if (-not $SkipVSCheck) { - $vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - if (Test-Path $vsWhere) { - $vsInstall = & $vsWhere -latest -property installationPath 2>$null - if ($vsInstall) { - $vsVersion = & $vsWhere -latest -property catalog_productDisplayVersion 2>$null - Write-Host "[OK] Visual Studio 2022: $vsVersion" -ForegroundColor Green - - # Check required workloads - $workloads = & $vsWhere -latest -property catalog_productLineVersion 2>$null - Write-Host " Location: $vsInstall" -ForegroundColor Gray - } else { - Write-Host "[MISSING] Visual Studio 2022 - Please install with:" -ForegroundColor Red - Write-Host " - .NET desktop development workload" -ForegroundColor Red - Write-Host " - Desktop development with C++ workload" -ForegroundColor Red - exit 1 - } + $vsToolchain = Get-VsToolchainInfo -Requires @('Microsoft.Component.MSBuild', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64') + if ($vsToolchain) { + $vsVersion = if ([string]::IsNullOrWhiteSpace($vsToolchain.DisplayVersion)) { 'unknown version' } else { $vsToolchain.DisplayVersion } + Write-Host "[OK] Visual Studio 2022: $vsVersion" -ForegroundColor Green + Write-Host " Location: $($vsToolchain.InstallationPath)" -ForegroundColor Gray + } elseif (Get-VsWherePath) { + Write-Host "[MISSING] Visual Studio 2022 - Please install with:" -ForegroundColor Red + Write-Host " - .NET desktop development workload" -ForegroundColor Red + Write-Host " - Desktop development with C++ workload" -ForegroundColor Red + exit 1 } else { Write-Host "[MISSING] Visual Studio 2022 - Please install from https://visualstudio.microsoft.com/" -ForegroundColor Red exit 1 @@ -245,15 +241,12 @@ Write-Host "`n--- Configuring PATH ---" -ForegroundColor Yellow $pathsToAdd = @() # VSTest (Visual Studio 2022) -$vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -if (Test-Path $vsWhere) { - $vsInstall = & $vsWhere -latest -property installationPath 2>$null - if ($vsInstall) { - $vstestPath = Join-Path $vsInstall 'Common7\IDE\CommonExtensions\Microsoft\TestWindow' - if (Test-Path (Join-Path $vstestPath 'vstest.console.exe')) { - $pathsToAdd += $vstestPath - } - } +if (-not $vsToolchain -and -not $SkipVSCheck) { + $vsToolchain = Get-VsToolchainInfo -Requires @('Microsoft.Component.MSBuild', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64') +} + +if ($vsToolchain -and $vsToolchain.VSTestPath) { + $pathsToAdd += (Split-Path -Parent $vsToolchain.VSTestPath) } # Update PATH From 036af1db62dcaa6e584c95d8f274adb24e744a65 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 1 Apr 2026 17:11:07 -0400 Subject: [PATCH 4/4] whitespace fix --- Build/Agent/Run-VsTests.ps1 | 252 +++++++-------- Build/Agent/Setup-FwBuildEnv.ps1 | 236 +++++++------- Build/Agent/Setup-InstallerBuild.ps1 | 34 +- Build/Agent/Verify-FwDependencies.ps1 | 436 +++++++++++++------------- 4 files changed, 479 insertions(+), 479 deletions(-) diff --git a/Build/Agent/Run-VsTests.ps1 b/Build/Agent/Run-VsTests.ps1 index dc562f4d21..9de229e6b0 100644 --- a/Build/Agent/Run-VsTests.ps1 +++ b/Build/Agent/Run-VsTests.ps1 @@ -1,56 +1,56 @@ <# .SYNOPSIS - Run VSTest for FieldWorks test assemblies with proper result parsing. + Run VSTest for FieldWorks test assemblies with proper result parsing. .DESCRIPTION - This script runs vstest.console.exe on specified test DLLs and parses the results - to provide clear pass/fail/skip counts. It handles the InIsolation mode configured - in Test.runsettings and properly interprets exit codes. + This script runs vstest.console.exe on specified test DLLs and parses the results + to provide clear pass/fail/skip counts. It handles the InIsolation mode configured + in Test.runsettings and properly interprets exit codes. .PARAMETER TestDlls - Array of test DLL names (e.g., "FwUtilsTests.dll") or paths. - If just names are provided, looks in Output\Debug by default. + Array of test DLL names (e.g., "FwUtilsTests.dll") or paths. + If just names are provided, looks in Output\Debug by default. .PARAMETER OutputDir - Directory containing test DLLs. Defaults to Output\Debug. + Directory containing test DLLs. Defaults to Output\Debug. .PARAMETER Filter - Optional VSTest filter expression (e.g., "TestCategory!=Slow"). + Optional VSTest filter expression (e.g., "TestCategory!=Slow"). .PARAMETER Rebuild - If specified, rebuilds the test projects before running tests. + If specified, rebuilds the test projects before running tests. .PARAMETER All - If specified, runs all *Tests.dll files found in OutputDir. + If specified, runs all *Tests.dll files found in OutputDir. .EXAMPLE - .\Run-VsTests.ps1 -TestDlls FwUtilsTests.dll - Runs FwUtilsTests.dll and shows results. + .\Run-VsTests.ps1 -TestDlls FwUtilsTests.dll + Runs FwUtilsTests.dll and shows results. .EXAMPLE - .\Run-VsTests.ps1 -TestDlls FwUtilsTests.dll,xCoreTests.dll - Runs multiple test DLLs and shows aggregate results. + .\Run-VsTests.ps1 -TestDlls FwUtilsTests.dll,xCoreTests.dll + Runs multiple test DLLs and shows aggregate results. .EXAMPLE - .\Run-VsTests.ps1 -All - Runs all test DLLs in Output\Debug. + .\Run-VsTests.ps1 -All + Runs all test DLLs in Output\Debug. .EXAMPLE - .\Run-VsTests.ps1 -TestDlls FwUtilsTests.dll -Rebuild - Rebuilds the test project first, then runs tests. + .\Run-VsTests.ps1 -TestDlls FwUtilsTests.dll -Rebuild + Rebuilds the test project first, then runs tests. #> [CmdletBinding()] param( - [Parameter(Position = 0)] - [string[]]$TestDlls, + [Parameter(Position = 0)] + [string[]]$TestDlls, - [string]$OutputDir, + [string]$OutputDir, - [string]$Filter, + [string]$Filter, - [switch]$Rebuild, + [switch]$Rebuild, - [switch]$All + [switch]$All ) $ErrorActionPreference = 'Continue' # Don't stop on stderr output from vstest @@ -59,63 +59,63 @@ Import-Module (Join-Path $PSScriptRoot 'FwBuildEnvironment.psm1') -Force # Find repo root (where FieldWorks.sln is) $repoRoot = $PSScriptRoot while ($repoRoot -and -not (Test-Path (Join-Path $repoRoot "FieldWorks.sln"))) { - $repoRoot = Split-Path $repoRoot -Parent + $repoRoot = Split-Path $repoRoot -Parent } if (-not $repoRoot) { - Write-Error "Could not find repository root (FieldWorks.sln)" - exit 1 + Write-Error "Could not find repository root (FieldWorks.sln)" + exit 1 } # Set defaults if (-not $OutputDir) { - $OutputDir = Join-Path $repoRoot "Output\Debug" + $OutputDir = Join-Path $repoRoot "Output\Debug" } $runSettings = Join-Path $repoRoot "Test.runsettings" $vsTestPath = Get-VSTestPath if (-not (Test-Path $vsTestPath)) { - Write-Error "vstest.console.exe not found. Install Visual Studio 2022 or Build Tools." - exit 1 + Write-Error "vstest.console.exe not found. Install Visual Studio 2022 or Build Tools." + exit 1 } # Collect test DLLs if ($All) { - $TestDlls = Get-ChildItem $OutputDir -Filter "*Tests.dll" | - Where-Object { $_.Name -notmatch "\.resources\." } | - Select-Object -ExpandProperty Name - Write-Host "Found $($TestDlls.Count) test assemblies" -ForegroundColor Cyan + $TestDlls = Get-ChildItem $OutputDir -Filter "*Tests.dll" | + Where-Object { $_.Name -notmatch "\.resources\." } | + Select-Object -ExpandProperty Name + Write-Host "Found $($TestDlls.Count) test assemblies" -ForegroundColor Cyan } if (-not $TestDlls -or $TestDlls.Count -eq 0) { - Write-Host "Usage: Run-VsTests.ps1 [-TestDlls] [-All] [-Rebuild] [-Filter ]" -ForegroundColor Yellow - Write-Host "" - Write-Host "Examples:" - Write-Host " Run-VsTests.ps1 FwUtilsTests.dll" - Write-Host " Run-VsTests.ps1 FwUtilsTests.dll,xCoreTests.dll" - Write-Host " Run-VsTests.ps1 -All" - Write-Host " Run-VsTests.ps1 FwUtilsTests.dll -Rebuild" - exit 0 + Write-Host "Usage: Run-VsTests.ps1 [-TestDlls] [-All] [-Rebuild] [-Filter ]" -ForegroundColor Yellow + Write-Host "" + Write-Host "Examples:" + Write-Host " Run-VsTests.ps1 FwUtilsTests.dll" + Write-Host " Run-VsTests.ps1 FwUtilsTests.dll,xCoreTests.dll" + Write-Host " Run-VsTests.ps1 -All" + Write-Host " Run-VsTests.ps1 FwUtilsTests.dll -Rebuild" + exit 0 } # Rebuild if requested if ($Rebuild) { - Write-Host "Rebuilding test projects..." -ForegroundColor Cyan - foreach ($dll in $TestDlls) { - $dllName = [System.IO.Path]::GetFileNameWithoutExtension($dll) - $csprojPattern = Join-Path $repoRoot "Src\**\$dllName.csproj" - $csproj = Get-ChildItem -Path (Join-Path $repoRoot "Src") -Recurse -Filter "$dllName.csproj" | Select-Object -First 1 - - if ($csproj) { - Write-Host " Building $($csproj.Name)..." -ForegroundColor Gray - $buildOutput = dotnet build $csproj.FullName -c Debug --no-incremental -v q 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Warning "Build failed for $($csproj.Name)" - $buildOutput | Write-Host - } - } - } - Write-Host "" + Write-Host "Rebuilding test projects..." -ForegroundColor Cyan + foreach ($dll in $TestDlls) { + $dllName = [System.IO.Path]::GetFileNameWithoutExtension($dll) + $csprojPattern = Join-Path $repoRoot "Src\**\$dllName.csproj" + $csproj = Get-ChildItem -Path (Join-Path $repoRoot "Src") -Recurse -Filter "$dllName.csproj" | Select-Object -First 1 + + if ($csproj) { + Write-Host " Building $($csproj.Name)..." -ForegroundColor Gray + $buildOutput = dotnet build $csproj.FullName -c Debug --no-incremental -v q 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Warning "Build failed for $($csproj.Name)" + $buildOutput | Write-Host + } + } + } + Write-Host "" } # Run tests @@ -128,63 +128,63 @@ Write-Host "Running tests..." -ForegroundColor Cyan Write-Host "" foreach ($dll in $TestDlls) { - # Resolve full path - if (-not [System.IO.Path]::IsPathRooted($dll)) { - $dllPath = Join-Path $OutputDir $dll - } else { - $dllPath = $dll - } - - if (-not (Test-Path $dllPath)) { - Write-Warning "Not found: $dll" - continue - } - - $dllName = [System.IO.Path]::GetFileName($dllPath) - - # Build arguments - $args = @($dllPath, "/Settings:$runSettings") - if ($Filter) { - $args += "/TestCaseFilter:$Filter" - } - - # Run vstest - $output = & $vsTestPath @args 2>&1 - - # Parse results - $passed = ($output | Select-String "^\s+Passed").Count - $failed = ($output | Select-String "^\s+Failed").Count - $skipped = ($output | Select-String "^\s+Skipped").Count - $exitCode = $LASTEXITCODE - - $totalPassed += $passed - $totalFailed += $failed - $totalSkipped += $skipped - - # Determine status - if ($failed -gt 0) { - $status = "FAIL" - $color = "Red" - } elseif ($passed -eq 0 -and $skipped -eq 0) { - $status = "NONE" - $color = "Yellow" - } else { - $status = "PASS" - $color = "Green" - } - - # Output result - $resultLine = "{0,-40} {1,6} passed, {2,4} failed, {3,4} skipped [{4}]" -f $dllName, $passed, $failed, $skipped, $status - Write-Host $resultLine -ForegroundColor $color - - $results += [PSCustomObject]@{ - DLL = $dllName - Passed = $passed - Failed = $failed - Skipped = $skipped - Status = $status - Output = $output - } + # Resolve full path + if (-not [System.IO.Path]::IsPathRooted($dll)) { + $dllPath = Join-Path $OutputDir $dll + } else { + $dllPath = $dll + } + + if (-not (Test-Path $dllPath)) { + Write-Warning "Not found: $dll" + continue + } + + $dllName = [System.IO.Path]::GetFileName($dllPath) + + # Build arguments + $args = @($dllPath, "/Settings:$runSettings") + if ($Filter) { + $args += "/TestCaseFilter:$Filter" + } + + # Run vstest + $output = & $vsTestPath @args 2>&1 + + # Parse results + $passed = ($output | Select-String "^\s+Passed").Count + $failed = ($output | Select-String "^\s+Failed").Count + $skipped = ($output | Select-String "^\s+Skipped").Count + $exitCode = $LASTEXITCODE + + $totalPassed += $passed + $totalFailed += $failed + $totalSkipped += $skipped + + # Determine status + if ($failed -gt 0) { + $status = "FAIL" + $color = "Red" + } elseif ($passed -eq 0 -and $skipped -eq 0) { + $status = "NONE" + $color = "Yellow" + } else { + $status = "PASS" + $color = "Green" + } + + # Output result + $resultLine = "{0,-40} {1,6} passed, {2,4} failed, {3,4} skipped [{4}]" -f $dllName, $passed, $failed, $skipped, $status + Write-Host $resultLine -ForegroundColor $color + + $results += [PSCustomObject]@{ + DLL = $dllName + Passed = $passed + Failed = $failed + Skipped = $skipped + Status = $status + Output = $output + } } # Summary @@ -192,22 +192,22 @@ Write-Host "" Write-Host ("=" * 70) -ForegroundColor Cyan $summaryLine = "TOTAL: {0} passed, {1} failed, {2} skipped" -f $totalPassed, $totalFailed, $totalSkipped if ($totalFailed -gt 0) { - Write-Host $summaryLine -ForegroundColor Red - $exitCode = 1 + Write-Host $summaryLine -ForegroundColor Red + $exitCode = 1 } else { - Write-Host $summaryLine -ForegroundColor Green - $exitCode = 0 + Write-Host $summaryLine -ForegroundColor Green + $exitCode = 0 } # Show failures if any if ($totalFailed -gt 0) { - Write-Host "" - Write-Host "Failed tests:" -ForegroundColor Red - foreach ($r in $results | Where-Object { $_.Failed -gt 0 }) { - Write-Host "" - Write-Host "=== $($r.DLL) ===" -ForegroundColor Yellow - $r.Output | Select-String "^\s+Failed" -Context 0,5 | ForEach-Object { Write-Host $_ } - } + Write-Host "" + Write-Host "Failed tests:" -ForegroundColor Red + foreach ($r in $results | Where-Object { $_.Failed -gt 0 }) { + Write-Host "" + Write-Host "=== $($r.DLL) ===" -ForegroundColor Yellow + $r.Output | Select-String "^\s+Failed" -Context 0,5 | ForEach-Object { Write-Host $_ } + } } exit $exitCode diff --git a/Build/Agent/Setup-FwBuildEnv.ps1 b/Build/Agent/Setup-FwBuildEnv.ps1 index 91ad908451..f6f4784d3a 100644 --- a/Build/Agent/Setup-FwBuildEnv.ps1 +++ b/Build/Agent/Setup-FwBuildEnv.ps1 @@ -1,37 +1,37 @@ <# .SYNOPSIS - Configures the FieldWorks build environment on Windows. + Configures the FieldWorks build environment on Windows. .DESCRIPTION - Sets up environment variables and PATH entries needed for FieldWorks builds. - Can be run locally for testing or called from GitHub Actions workflows. + Sets up environment variables and PATH entries needed for FieldWorks builds. + Can be run locally for testing or called from GitHub Actions workflows. - This script is idempotent - safe to run multiple times. + This script is idempotent - safe to run multiple times. .PARAMETER OutputGitHubEnv - If specified, outputs environment variables to GITHUB_ENV and GITHUB_PATH - for use in GitHub Actions. Otherwise, sets them in the current process. + If specified, outputs environment variables to GITHUB_ENV and GITHUB_PATH + for use in GitHub Actions. Otherwise, sets them in the current process. .PARAMETER Verify - If specified, runs verification checks and exits with non-zero on failure. + If specified, runs verification checks and exits with non-zero on failure. .EXAMPLE - # Local testing - just configure current session - .\Build\Agent\Setup-FwBuildEnv.ps1 + # Local testing - just configure current session + .\Build\Agent\Setup-FwBuildEnv.ps1 .EXAMPLE - # GitHub Actions - output to GITHUB_ENV - .\Build\Agent\Setup-FwBuildEnv.ps1 -OutputGitHubEnv + # GitHub Actions - output to GITHUB_ENV + .\Build\Agent\Setup-FwBuildEnv.ps1 -OutputGitHubEnv .EXAMPLE - # Verify all dependencies are available - .\Build\Agent\Setup-FwBuildEnv.ps1 -Verify + # Verify all dependencies are available + .\Build\Agent\Setup-FwBuildEnv.ps1 -Verify #> [CmdletBinding()] param( - [switch]$OutputGitHubEnv, - [switch]$Verify + [switch]$OutputGitHubEnv, + [switch]$Verify ) $ErrorActionPreference = 'Stop' @@ -39,55 +39,55 @@ $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path Import-Module (Join-Path $scriptDir 'FwBuildEnvironment.psm1') -Force function Write-Status { - param([string]$Message, [string]$Status = "INFO", [string]$Color = "White") - $prefix = switch ($Status) { - "OK" { "[OK] "; $Color = "Green" } - "FAIL" { "[FAIL] "; $Color = "Red" } - "WARN" { "[WARN] "; $Color = "Yellow" } - "SKIP" { "[SKIP] "; $Color = "DarkGray" } - default { "[INFO] " } - } - Write-Host "$prefix$Message" -ForegroundColor $Color + param([string]$Message, [string]$Status = "INFO", [string]$Color = "White") + $prefix = switch ($Status) { + "OK" { "[OK] "; $Color = "Green" } + "FAIL" { "[FAIL] "; $Color = "Red" } + "WARN" { "[WARN] "; $Color = "Yellow" } + "SKIP" { "[SKIP] "; $Color = "DarkGray" } + default { "[INFO] " } + } + Write-Host "$prefix$Message" -ForegroundColor $Color } function Set-EnvVar { - param([string]$Name, [string]$Value) - - if ($OutputGitHubEnv -and $env:GITHUB_ENV) { - # GitHub Actions format - "$Name=$Value" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - Write-Status "Set $Name (GITHUB_ENV)" - } - else { - # Local session - [Environment]::SetEnvironmentVariable($Name, $Value, 'Process') - Write-Status "Set $Name = $Value" - } + param([string]$Name, [string]$Value) + + if ($OutputGitHubEnv -and $env:GITHUB_ENV) { + # GitHub Actions format + "$Name=$Value" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + Write-Status "Set $Name (GITHUB_ENV)" + } + else { + # Local session + [Environment]::SetEnvironmentVariable($Name, $Value, 'Process') + Write-Status "Set $Name = $Value" + } } function Add-ToPath { - param([string]$Path) - - if (-not (Test-Path $Path)) { - Write-Status "Path does not exist: $Path" -Status "WARN" - return $false - } - - if ($OutputGitHubEnv -and $env:GITHUB_PATH) { - $Path | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - Write-Status "Added to PATH (GITHUB_PATH): $Path" - } - else { - $currentPath = [Environment]::GetEnvironmentVariable('PATH', 'Process') - if ($currentPath -notlike "*$Path*") { - [Environment]::SetEnvironmentVariable('PATH', "$Path;$currentPath", 'Process') - Write-Status "Added to PATH: $Path" - } - else { - Write-Status "Already in PATH: $Path" -Status "SKIP" - } - } - return $true + param([string]$Path) + + if (-not (Test-Path $Path)) { + Write-Status "Path does not exist: $Path" -Status "WARN" + return $false + } + + if ($OutputGitHubEnv -and $env:GITHUB_PATH) { + $Path | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + Write-Status "Added to PATH (GITHUB_PATH): $Path" + } + else { + $currentPath = [Environment]::GetEnvironmentVariable('PATH', 'Process') + if ($currentPath -notlike "*$Path*") { + [Environment]::SetEnvironmentVariable('PATH', "$Path;$currentPath", 'Process') + Write-Status "Added to PATH: $Path" + } + else { + Write-Status "Already in PATH: $Path" -Status "SKIP" + } + } + return $true } # ============================================================================ @@ -108,10 +108,10 @@ Set-EnvVar -Name "FW_ROOT_CODE_DIR" -Value $distFiles Set-EnvVar -Name "FW_ROOT_DATA_DIR" -Value $distFiles $results = @{ - VSPath = $null - MSBuildPath = $null - VSTestPath = $null - Errors = @() + VSPath = $null + MSBuildPath = $null + VSTestPath = $null + Errors = @() } # ---------------------------------------------------------------------------- @@ -121,36 +121,36 @@ Write-Host "--- Locating Visual Studio ---" -ForegroundColor Cyan $toolchain = Get-VsToolchainInfo -Requires @('Microsoft.Component.MSBuild', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64') if ($toolchain) { - $results.VSPath = $toolchain.InstallationPath - $results.MSBuildPath = $toolchain.MSBuildPath - $results.VSTestPath = $toolchain.VSTestPath - - if ([string]::IsNullOrWhiteSpace($toolchain.DisplayVersion)) { - Write-Status "Visual Studio: $($toolchain.InstallationPath)" -Status "OK" - } - else { - Write-Status "Visual Studio $($toolchain.DisplayVersion): $($toolchain.InstallationPath)" -Status "OK" - } - - # Export installation hints only; build/test scripts still self-initialize via VsDevCmd. - Set-EnvVar -Name "VSINSTALLDIR" -Value ($toolchain.InstallationPath.TrimEnd('\') + '\') - if ($toolchain.VcInstallDir) { - Set-EnvVar -Name "VCINSTALLDIR" -Value ($toolchain.VcInstallDir.TrimEnd('\') + '\') - } - if ($toolchain.VCTargetsPath) { - Set-EnvVar -Name "VCTargetsPath" -Value $toolchain.VCTargetsPath - } + $results.VSPath = $toolchain.InstallationPath + $results.MSBuildPath = $toolchain.MSBuildPath + $results.VSTestPath = $toolchain.VSTestPath + + if ([string]::IsNullOrWhiteSpace($toolchain.DisplayVersion)) { + Write-Status "Visual Studio: $($toolchain.InstallationPath)" -Status "OK" + } + else { + Write-Status "Visual Studio $($toolchain.DisplayVersion): $($toolchain.InstallationPath)" -Status "OK" + } + + # Export installation hints only; build/test scripts still self-initialize via VsDevCmd. + Set-EnvVar -Name "VSINSTALLDIR" -Value ($toolchain.InstallationPath.TrimEnd('\') + '\') + if ($toolchain.VcInstallDir) { + Set-EnvVar -Name "VCINSTALLDIR" -Value ($toolchain.VcInstallDir.TrimEnd('\') + '\') + } + if ($toolchain.VCTargetsPath) { + Set-EnvVar -Name "VCTargetsPath" -Value $toolchain.VCTargetsPath + } } else { - $vsWhere = Get-VsWherePath - if ($vsWhere) { - Write-Status "Visual Studio with MSBuild and C++ tools not found" -Status "FAIL" - $results.Errors += "Visual Studio with MSBuild and C++ tools not found" - } - else { - Write-Status "vswhere.exe not found" -Status "FAIL" - $results.Errors += "vswhere.exe not found" - } + $vsWhere = Get-VsWherePath + if ($vsWhere) { + Write-Status "Visual Studio with MSBuild and C++ tools not found" -Status "FAIL" + $results.Errors += "Visual Studio with MSBuild and C++ tools not found" + } + else { + Write-Status "vswhere.exe not found" -Status "FAIL" + $results.Errors += "vswhere.exe not found" + } } # ---------------------------------------------------------------------------- @@ -160,12 +160,12 @@ Write-Host "" Write-Host "--- Locating MSBuild ---" -ForegroundColor Cyan if ($results.MSBuildPath) { - Write-Status "MSBuild: $($results.MSBuildPath)" -Status "OK" - Add-ToPath -Path (Split-Path -Parent $results.MSBuildPath) | Out-Null + Write-Status "MSBuild: $($results.MSBuildPath)" -Status "OK" + Add-ToPath -Path (Split-Path -Parent $results.MSBuildPath) | Out-Null } else { - Write-Status "MSBuild not found" -Status "FAIL" - $results.Errors += "MSBuild not found" + Write-Status "MSBuild not found" -Status "FAIL" + $results.Errors += "MSBuild not found" } # ---------------------------------------------------------------------------- @@ -175,21 +175,21 @@ Write-Host "" Write-Host "--- Configuring PATH ---" -ForegroundColor Cyan $netfxPaths = @( - "${env:ProgramFiles(x86)}\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8.1 Tools", - "${env:ProgramFiles(x86)}\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools", - "${env:ProgramFiles(x86)}\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools" + "${env:ProgramFiles(x86)}\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8.1 Tools", + "${env:ProgramFiles(x86)}\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools", + "${env:ProgramFiles(x86)}\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools" ) $foundNetfx = $false foreach ($p in $netfxPaths) { - if (Test-Path $p) { - Add-ToPath -Path $p | Out-Null - $foundNetfx = $true - break - } + if (Test-Path $p) { + Add-ToPath -Path $p | Out-Null + $foundNetfx = $true + break + } } if (-not $foundNetfx) { - Write-Status "NETFX tools not found (sn.exe may not work)" -Status "WARN" + Write-Status "NETFX tools not found (sn.exe may not work)" -Status "WARN" } # ---------------------------------------------------------------------------- @@ -199,11 +199,11 @@ Write-Host "" Write-Host "--- Locating VSTest ---" -ForegroundColor Cyan if ($results.VSTestPath) { - Add-ToPath -Path (Split-Path -Parent $results.VSTestPath) | Out-Null - Write-Status "VSTest: $($results.VSTestPath)" -Status "OK" + Add-ToPath -Path (Split-Path -Parent $results.VSTestPath) | Out-Null + Write-Status "VSTest: $($results.VSTestPath)" -Status "OK" } else { - Write-Status "vstest.console.exe not found" -Status "WARN" + Write-Status "vstest.console.exe not found" -Status "WARN" } # ---------------------------------------------------------------------------- @@ -214,24 +214,24 @@ Write-Host "=== Setup Complete ===" -ForegroundColor Cyan # Output key paths for GitHub Actions if ($OutputGitHubEnv -and $env:GITHUB_OUTPUT) { - "msbuild-path=$($results.MSBuildPath)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - "vs-install-path=$($results.VSPath)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - "vstest-path=$($results.VSTestPath)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "msbuild-path=$($results.MSBuildPath)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "vs-install-path=$($results.VSPath)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "vstest-path=$($results.VSTestPath)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append } # Return results object for programmatic use if ($results.Errors.Count -gt 0) { - Write-Host "" - Write-Status "Setup completed with errors:" -Status "FAIL" - foreach ($err in $results.Errors) { - Write-Host " - $err" -ForegroundColor Red - } - if ($Verify) { - exit 1 - } + Write-Host "" + Write-Status "Setup completed with errors:" -Status "FAIL" + foreach ($err in $results.Errors) { + Write-Host " - $err" -ForegroundColor Red + } + if ($Verify) { + exit 1 + } } else { - Write-Status "All environment configuration successful" -Status "OK" + Write-Status "All environment configuration successful" -Status "OK" } return $results diff --git a/Build/Agent/Setup-InstallerBuild.ps1 b/Build/Agent/Setup-InstallerBuild.ps1 index 35552da080..87d7bd0b47 100644 --- a/Build/Agent/Setup-InstallerBuild.ps1 +++ b/Build/Agent/Setup-InstallerBuild.ps1 @@ -99,37 +99,37 @@ $toolchain = Get-VsToolchainInfo -Requires @('Microsoft.Component.MSBuild', 'Mic $vsDevEnvActive = Test-VsDevEnvironmentActive if ($toolchain) { - $vsVersion = if ([string]::IsNullOrWhiteSpace($toolchain.DisplayVersion)) { 'unknown version' } else { $toolchain.DisplayVersion } + $vsVersion = if ([string]::IsNullOrWhiteSpace($toolchain.DisplayVersion)) { 'unknown version' } else { $toolchain.DisplayVersion } Write-Host "[OK] Visual Studio 2022: $vsVersion" -ForegroundColor Green - if ($toolchain.MSBuildPath) { - Write-Host "[OK] MSBuild found: $($toolchain.MSBuildPath)" -ForegroundColor Green + if ($toolchain.MSBuildPath) { + Write-Host "[OK] MSBuild found: $($toolchain.MSBuildPath)" -ForegroundColor Green } else { $issues += "MSBuild not found in VS installation" } - if ($toolchain.VsDevCmdPath) { + if ($toolchain.VsDevCmdPath) { Write-Host "[OK] VS Developer environment scripts available" -ForegroundColor Green - $restoreWrappedCommand = 'cmd /c "call ""{0}"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:RestorePackages /p:Configuration=Debug /p:Platform=x64"' -f $toolchain.VsDevCmdPath - $buildWrappedCommand = 'cmd /c "call ""{0}"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:BuildInstaller /p:Configuration=Debug /p:Platform=x64 /p:config=release /m /v:n"' -f $toolchain.VsDevCmdPath - $patchWrappedCommand = 'cmd /c "call ""{0}"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:BuildPatchInstaller /p:Configuration=Debug /p:Platform=x64 /p:config=release /m /v:n"' -f $toolchain.VsDevCmdPath - } else { - $issues += "VsDevCmd.bat not found in VS installation" + $restoreWrappedCommand = 'cmd /c "call ""{0}"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:RestorePackages /p:Configuration=Debug /p:Platform=x64"' -f $toolchain.VsDevCmdPath + $buildWrappedCommand = 'cmd /c "call ""{0}"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:BuildInstaller /p:Configuration=Debug /p:Platform=x64 /p:config=release /m /v:n"' -f $toolchain.VsDevCmdPath + $patchWrappedCommand = 'cmd /c "call ""{0}"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:BuildPatchInstaller /p:Configuration=Debug /p:Platform=x64 /p:config=release /m /v:n"' -f $toolchain.VsDevCmdPath + } else { + $issues += "VsDevCmd.bat not found in VS installation" } - if ($vsDevEnvActive) { - Write-Host "[OK] VS Developer environment active" -ForegroundColor Green + if ($vsDevEnvActive) { + Write-Host "[OK] VS Developer environment active" -ForegroundColor Green } else { Write-Host "[WARN] VS Developer environment NOT active" -ForegroundColor Yellow - Write-Host " Run builds from VS Developer Command Prompt or use the detected VsDevCmd wrapper commands below" -ForegroundColor Yellow - $warnings += "VS Developer environment not active" + Write-Host " Run builds from VS Developer Command Prompt or use the detected VsDevCmd wrapper commands below" -ForegroundColor Yellow + $warnings += "VS Developer environment not active" } } else { - if (Get-VsWherePath) { - $issues += "Visual Studio 2022 with MSBuild and C++ tools not installed" - } else { + if (Get-VsWherePath) { + $issues += "Visual Studio 2022 with MSBuild and C++ tools not installed" + } else { $issues += "Visual Studio Installer not found" - } + } } #endregion diff --git a/Build/Agent/Verify-FwDependencies.ps1 b/Build/Agent/Verify-FwDependencies.ps1 index 626a8bbccd..60250ad7a4 100644 --- a/Build/Agent/Verify-FwDependencies.ps1 +++ b/Build/Agent/Verify-FwDependencies.ps1 @@ -1,125 +1,125 @@ <# .SYNOPSIS - Verifies that all FieldWorks build dependencies are available. + Verifies that all FieldWorks build dependencies are available. .DESCRIPTION - Checks for required tools and SDKs needed to build FieldWorks. - Can be run locally for testing or called from GitHub Actions workflows. - By default, the script writes host output only and does not emit result objects. - Use -PassThru when a caller needs structured results returned on the pipeline. - - Expected dependencies (typically pre-installed on windows-latest): - - Visual Studio 2022 with Desktop & C++ workloads - - MSBuild - - .NET Framework 4.8.1 SDK & Targeting Pack - - Windows SDK - - WiX Toolset v6 (installer builds restore via NuGet) - - .NET SDK 8.x+ + Checks for required tools and SDKs needed to build FieldWorks. + Can be run locally for testing or called from GitHub Actions workflows. + By default, the script writes host output only and does not emit result objects. + Use -PassThru when a caller needs structured results returned on the pipeline. + + Expected dependencies (typically pre-installed on windows-latest): + - Visual Studio 2022 with Desktop & C++ workloads + - MSBuild + - .NET Framework 4.8.1 SDK & Targeting Pack + - Windows SDK + - WiX Toolset v6 (installer builds restore via NuGet) + - .NET SDK 8.x+ .PARAMETER FailOnMissing - If specified, exits with non-zero code if any required dependency is missing. + If specified, exits with non-zero code if any required dependency is missing. .PARAMETER IncludeOptional - If specified, also checks optional dependencies like clangd for Serena. + If specified, also checks optional dependencies like clangd for Serena. .PARAMETER Detailed - If specified, prints the full per-dependency section headers and success details instead of the compact summary-only output. + If specified, prints the full per-dependency section headers and success details instead of the compact summary-only output. .PARAMETER PassThru - If specified, returns the dependency result objects for scripting callers. - Without -PassThru, the script is quiet-by-default and writes host output only. + If specified, returns the dependency result objects for scripting callers. + Without -PassThru, the script is quiet-by-default and writes host output only. .EXAMPLE - # Quick check - .\Build\Agent\Verify-FwDependencies.ps1 + # Quick check + .\Build\Agent\Verify-FwDependencies.ps1 .EXAMPLE - # Strict check for CI - .\Build\Agent\Verify-FwDependencies.ps1 -FailOnMissing + # Strict check for CI + .\Build\Agent\Verify-FwDependencies.ps1 -FailOnMissing .EXAMPLE - # Include Serena dependencies - .\Build\Agent\Verify-FwDependencies.ps1 -IncludeOptional + # Include Serena dependencies + .\Build\Agent\Verify-FwDependencies.ps1 -IncludeOptional .EXAMPLE - # Show full dependency-by-dependency output - .\Build\Agent\Verify-FwDependencies.ps1 -IncludeOptional -Detailed + # Show full dependency-by-dependency output + .\Build\Agent\Verify-FwDependencies.ps1 -IncludeOptional -Detailed .EXAMPLE - # Capture structured results for automation - $results = .\Build\Agent\Verify-FwDependencies.ps1 -IncludeOptional -PassThru + # Capture structured results for automation + $results = .\Build\Agent\Verify-FwDependencies.ps1 -IncludeOptional -PassThru .NOTES - Behavioral change: this script no longer emits dependency result objects unless -PassThru is specified. + Behavioral change: this script no longer emits dependency result objects unless -PassThru is specified. #> [CmdletBinding()] param( - [switch]$FailOnMissing, - [switch]$IncludeOptional, - [switch]$Detailed, - [switch]$PassThru + [switch]$FailOnMissing, + [switch]$IncludeOptional, + [switch]$Detailed, + [switch]$PassThru ) $ErrorActionPreference = 'Stop' Import-Module (Join-Path $PSScriptRoot 'FwBuildEnvironment.psm1') -Force function Test-Dependency { - param( - [string]$Name, - [scriptblock]$Check, - [string]$Required = "Required" - ) - - try { - $result = & $Check - if ($result) { - if ($Detailed) { - Write-Host "[OK] $Name" -ForegroundColor Green - if ($result -is [string] -and $result.Length -gt 0 -and $result.Length -lt 100) { - Write-Host " $result" -ForegroundColor DarkGray - } - } - return @{ Name = $Name; Found = $true; IsRequired = ($Required -eq "Required"); Info = $result } - } - else { - throw "Check returned null/false" - } - } - catch { - $color = if ($Required -eq "Required") { "Red" } else { "Yellow" } - $status = if ($Required -eq "Required") { "[FAIL]" } else { "[WARN]" } - Write-Host "$status $Name" -ForegroundColor $color - Write-Host " $_" -ForegroundColor DarkGray - return @{ Name = $Name; Found = $false; IsRequired = ($Required -eq "Required"); Error = $_.ToString() } - } + param( + [string]$Name, + [scriptblock]$Check, + [string]$Required = "Required" + ) + + try { + $result = & $Check + if ($result) { + if ($Detailed) { + Write-Host "[OK] $Name" -ForegroundColor Green + if ($result -is [string] -and $result.Length -gt 0 -and $result.Length -lt 100) { + Write-Host " $result" -ForegroundColor DarkGray + } + } + return @{ Name = $Name; Found = $true; IsRequired = ($Required -eq "Required"); Info = $result } + } + else { + throw "Check returned null/false" + } + } + catch { + $color = if ($Required -eq "Required") { "Red" } else { "Yellow" } + $status = if ($Required -eq "Required") { "[FAIL]" } else { "[WARN]" } + Write-Host "$status $Name" -ForegroundColor $color + Write-Host " $_" -ForegroundColor DarkGray + return @{ Name = $Name; Found = $false; IsRequired = ($Required -eq "Required"); Error = $_.ToString() } + } } function Find-DotNetFrameworkSdkTool { - param([Parameter(Mandatory)][string]$ToolName) + param([Parameter(Mandatory)][string]$ToolName) - $programFilesX86 = ${env:ProgramFiles(x86)} - if (-not $programFilesX86) { return $null } + $programFilesX86 = ${env:ProgramFiles(x86)} + if (-not $programFilesX86) { return $null } - $sdkBase = Join-Path $programFilesX86 'Microsoft SDKs\Windows\v10.0A\bin' - if (-not (Test-Path $sdkBase)) { return $null } + $sdkBase = Join-Path $programFilesX86 'Microsoft SDKs\Windows\v10.0A\bin' + if (-not (Test-Path $sdkBase)) { return $null } - $toolCandidates = @() - $netfxDirs = Get-ChildItem -Path $sdkBase -Directory -Filter 'NETFX*' -ErrorAction SilentlyContinue | - Sort-Object Name -Descending + $toolCandidates = @() + $netfxDirs = Get-ChildItem -Path $sdkBase -Directory -Filter 'NETFX*' -ErrorAction SilentlyContinue | + Sort-Object Name -Descending - foreach ($dir in $netfxDirs) { - $toolCandidates += (Join-Path $dir.FullName (Join-Path 'x64' $ToolName)) - $toolCandidates += (Join-Path $dir.FullName $ToolName) - } + foreach ($dir in $netfxDirs) { + $toolCandidates += (Join-Path $dir.FullName (Join-Path 'x64' $ToolName)) + $toolCandidates += (Join-Path $dir.FullName $ToolName) + } - foreach ($candidate in $toolCandidates) { - if (Test-Path $candidate) { - return $candidate - } - } + foreach ($candidate in $toolCandidates) { + if (Test-Path $candidate) { + return $candidate + } + } - return $null + return $null } # ============================================================================ @@ -127,8 +127,8 @@ function Find-DotNetFrameworkSdkTool { # ============================================================================ if ($Detailed) { - Write-Host "=== FieldWorks Dependency Verification ===" -ForegroundColor Cyan - Write-Host "" + Write-Host "=== FieldWorks Dependency Verification ===" -ForegroundColor Cyan + Write-Host "" } $results = @() @@ -137,171 +137,171 @@ $results = @() # Required Dependencies # ---------------------------------------------------------------------------- if ($Detailed) { - Write-Host "--- Required Dependencies ---" -ForegroundColor Cyan + Write-Host "--- Required Dependencies ---" -ForegroundColor Cyan } # .NET Framework targeting pack (4.8+) $results += Test-Dependency -Name ".NET Framework Targeting Pack (4.8+)" -Check { - $base = "${env:ProgramFiles(x86)}\Reference Assemblies\Microsoft\Framework\.NETFramework" - $candidates = @('v4.8.1', 'v4.8') - foreach ($version in $candidates) { - $path = Join-Path $base $version - if (Test-Path $path) { - return "$version at $path" - } - } - throw "Neither v4.8.1 nor v4.8 targeting pack was found under $base" + $base = "${env:ProgramFiles(x86)}\Reference Assemblies\Microsoft\Framework\.NETFramework" + $candidates = @('v4.8.1', 'v4.8') + foreach ($version in $candidates) { + $path = Join-Path $base $version + if (Test-Path $path) { + return "$version at $path" + } + } + throw "Neither v4.8.1 nor v4.8 targeting pack was found under $base" } # Windows SDK $results += Test-Dependency -Name "Windows SDK" -Check { - $path = "${env:ProgramFiles(x86)}\Windows Kits\10\Include" - if (Test-Path $path) { - $versions = (Get-ChildItem $path -Directory | Sort-Object Name -Descending | Select-Object -First 3).Name -join ', ' - return "Versions: $versions" - } - throw "Not found at $path" + $path = "${env:ProgramFiles(x86)}\Windows Kits\10\Include" + if (Test-Path $path) { + $versions = (Get-ChildItem $path -Directory | Sort-Object Name -Descending | Select-Object -First 3).Name -join ', ' + return "Versions: $versions" + } + throw "Not found at $path" } # Visual Studio / MSBuild $results += Test-Dependency -Name "Visual Studio 2022" -Check { - $vsInfo = Get-VsInstallationInfo -Requires @('Microsoft.Component.MSBuild', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64') - if (-not $vsInfo) { - $vsWhere = Get-VsWherePath - if (-not $vsWhere) { throw "vswhere.exe not found" } - throw "No VS installation with MSBuild and C++ tools found" - } - - return "Version $($vsInfo.DisplayVersion) at $($vsInfo.InstallationPath)" + $vsInfo = Get-VsInstallationInfo -Requires @('Microsoft.Component.MSBuild', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64') + if (-not $vsInfo) { + $vsWhere = Get-VsWherePath + if (-not $vsWhere) { throw "vswhere.exe not found" } + throw "No VS installation with MSBuild and C++ tools found" + } + + return "Version $($vsInfo.DisplayVersion) at $($vsInfo.InstallationPath)" } # MSBuild $results += Test-Dependency -Name "MSBuild" -Check { - $msbuild = Get-Command msbuild.exe -ErrorAction SilentlyContinue - if ($msbuild) { - $version = (& msbuild.exe -version -nologo 2>$null | Select-Object -Last 1) - return "Version $version" - } - $vsInfo = Get-VsInstallationInfo -Requires @('Microsoft.Component.MSBuild') - if ($vsInfo) { - $msbuildPath = Join-Path $vsInfo.InstallationPath 'MSBuild\Current\Bin\MSBuild.exe' - if (Test-Path $msbuildPath) { - return "Found at $msbuildPath (not in PATH)" - } - } - throw "MSBuild not found in PATH or VS installation" + $msbuild = Get-Command msbuild.exe -ErrorAction SilentlyContinue + if ($msbuild) { + $version = (& msbuild.exe -version -nologo 2>$null | Select-Object -Last 1) + return "Version $version" + } + $vsInfo = Get-VsInstallationInfo -Requires @('Microsoft.Component.MSBuild') + if ($vsInfo) { + $msbuildPath = Join-Path $vsInfo.InstallationPath 'MSBuild\Current\Bin\MSBuild.exe' + if (Test-Path $msbuildPath) { + return "Found at $msbuildPath (not in PATH)" + } + } + throw "MSBuild not found in PATH or VS installation" } # .NET Framework SDK tools used by localization tasks $results += Test-Dependency -Name "ResGen.exe (.NET Framework SDK)" -Check { - $resgen = Find-DotNetFrameworkSdkTool -ToolName 'ResGen.exe' - if ($resgen) { return $resgen } - throw "ResGen.exe not found in Windows SDK NETFX tool folders" + $resgen = Find-DotNetFrameworkSdkTool -ToolName 'ResGen.exe' + if ($resgen) { return $resgen } + throw "ResGen.exe not found in Windows SDK NETFX tool folders" } $results += Test-Dependency -Name "al.exe (.NET Framework SDK)" -Check { - $al = Find-DotNetFrameworkSdkTool -ToolName 'al.exe' - if ($al) { return $al } - throw "al.exe not found in Windows SDK NETFX tool folders" + $al = Find-DotNetFrameworkSdkTool -ToolName 'al.exe' + if ($al) { return $al } + throw "al.exe not found in Windows SDK NETFX tool folders" } # .NET SDK $results += Test-Dependency -Name ".NET SDK" -Check { - $dotnet = Get-Command dotnet.exe -ErrorAction SilentlyContinue - if ($dotnet) { - $version = (& dotnet.exe --version 2>&1) - return "Version $version" - } - throw "dotnet.exe not found in PATH" + $dotnet = Get-Command dotnet.exe -ErrorAction SilentlyContinue + if ($dotnet) { + $version = (& dotnet.exe --version 2>&1) + return "Version $version" + } + throw "dotnet.exe not found in PATH" } # WiX Toolset (v6) # Installer projects use WixToolset.Sdk via NuGet restore; no global WiX 3.x install is required. $results += Test-Dependency -Name "WiX Toolset (v6 via NuGet)" -Required "Optional" -Check { - $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) - $wixProj = Join-Path $repoRoot "FLExInstaller\wix6\FieldWorks.Installer.wixproj" - if (-not (Test-Path $wixProj)) { - throw "Installer project not found: $wixProj" - } - - [xml]$wixProjXml = Get-Content -LiteralPath $wixProj -Raw - $projectNode = $wixProjXml.Project - $hasWixSdk = $false - - if ($projectNode -and $projectNode.Sdk -match 'WixToolset\.Sdk') { - $hasWixSdk = $true - } - - if (-not $hasWixSdk) { - $wixSdkReference = $wixProjXml.SelectSingleNode("//*[local-name()='PackageReference' and @Include='WixToolset.Sdk']") - if ($wixSdkReference) { - $hasWixSdk = $true - } - } - - if ($hasWixSdk) { - return "Configured in $wixProj (restored during build)" - } - - throw "WixToolset.Sdk not referenced in $wixProj" + $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) + $wixProj = Join-Path $repoRoot "FLExInstaller\wix6\FieldWorks.Installer.wixproj" + if (-not (Test-Path $wixProj)) { + throw "Installer project not found: $wixProj" + } + + [xml]$wixProjXml = Get-Content -LiteralPath $wixProj -Raw + $projectNode = $wixProjXml.Project + $hasWixSdk = $false + + if ($projectNode -and $projectNode.Sdk -match 'WixToolset\.Sdk') { + $hasWixSdk = $true + } + + if (-not $hasWixSdk) { + $wixSdkReference = $wixProjXml.SelectSingleNode("//*[local-name()='PackageReference' and @Include='WixToolset.Sdk']") + if ($wixSdkReference) { + $hasWixSdk = $true + } + } + + if ($hasWixSdk) { + return "Configured in $wixProj (restored during build)" + } + + throw "WixToolset.Sdk not referenced in $wixProj" } # ---------------------------------------------------------------------------- # Optional Dependencies (for Serena MCP) # ---------------------------------------------------------------------------- if ($IncludeOptional) { - if ($Detailed) { - Write-Host "" - Write-Host "--- Optional Dependencies (Serena MCP) ---" -ForegroundColor Cyan - } - - # Python - $results += Test-Dependency -Name "Python" -Required "Optional" -Check { - $python = Get-Command python.exe -ErrorAction SilentlyContinue - if (-not $python) { $python = Get-Command python3.exe -ErrorAction SilentlyContinue } - if ($python) { - $version = (& $python.Source --version 2>&1) - return $version - } - throw "python.exe not found in PATH" - } - - # uv (Python package manager) - $results += Test-Dependency -Name "uv (Python package manager)" -Required "Optional" -Check { - $uv = Get-Command uv -ErrorAction SilentlyContinue - if ($uv) { - $version = (& uv --version 2>&1) - return $version - } - throw "uv not found - install with: winget install astral-sh.uv" - } - - # clangd (C++ language server) - $results += Test-Dependency -Name "clangd (C++ language server)" -Required "Optional" -Check { - $clangd = Get-Command clangd -ErrorAction SilentlyContinue - if ($clangd) { - $version = (& clangd --version 2>&1 | Select-Object -First 1) - return $version - } - throw "clangd not found - Serena will auto-download if needed" - } - - # Serena project config - $results += Test-Dependency -Name "Serena project config" -Required "Optional" -Check { - $configPath = ".serena/project.yml" - if (Test-Path $configPath) { - return "Found at $configPath" - } - throw "No .serena/project.yml - Serena not configured for this repo" - } + if ($Detailed) { + Write-Host "" + Write-Host "--- Optional Dependencies (Serena MCP) ---" -ForegroundColor Cyan + } + + # Python + $results += Test-Dependency -Name "Python" -Required "Optional" -Check { + $python = Get-Command python.exe -ErrorAction SilentlyContinue + if (-not $python) { $python = Get-Command python3.exe -ErrorAction SilentlyContinue } + if ($python) { + $version = (& $python.Source --version 2>&1) + return $version + } + throw "python.exe not found in PATH" + } + + # uv (Python package manager) + $results += Test-Dependency -Name "uv (Python package manager)" -Required "Optional" -Check { + $uv = Get-Command uv -ErrorAction SilentlyContinue + if ($uv) { + $version = (& uv --version 2>&1) + return $version + } + throw "uv not found - install with: winget install astral-sh.uv" + } + + # clangd (C++ language server) + $results += Test-Dependency -Name "clangd (C++ language server)" -Required "Optional" -Check { + $clangd = Get-Command clangd -ErrorAction SilentlyContinue + if ($clangd) { + $version = (& clangd --version 2>&1 | Select-Object -First 1) + return $version + } + throw "clangd not found - Serena will auto-download if needed" + } + + # Serena project config + $results += Test-Dependency -Name "Serena project config" -Required "Optional" -Check { + $configPath = ".serena/project.yml" + if (Test-Path $configPath) { + return "Found at $configPath" + } + throw "No .serena/project.yml - Serena not configured for this repo" + } } # ---------------------------------------------------------------------------- # Summary # ---------------------------------------------------------------------------- if ($Detailed) { - Write-Host "" - Write-Host "=== Summary ===" -ForegroundColor Cyan + Write-Host "" + Write-Host "=== Summary ===" -ForegroundColor Cyan } $required = $results | Where-Object { $_.IsRequired -ne $false } @@ -314,28 +314,28 @@ $foundRequired = ($required | Where-Object { $_.Found } | Measure-Object).Count Write-Host "Dependency preflight: required $foundRequired/$totalRequired found" if ($IncludeOptional) { - $totalOptional = ($optional | Measure-Object).Count - $foundOptional = ($optional | Where-Object { $_.Found } | Measure-Object).Count - Write-Host "Dependency preflight: optional $foundOptional/$totalOptional found" + $totalOptional = ($optional | Measure-Object).Count + $foundOptional = ($optional | Where-Object { $_.Found } | Measure-Object).Count + Write-Host "Dependency preflight: optional $foundOptional/$totalOptional found" } if ($missing.Count -gt 0) { - Write-Host "" - Write-Host "Missing required dependencies:" -ForegroundColor Red - foreach ($m in $missing) { - Write-Host " - $($m.Name)" -ForegroundColor Red - } - - if ($FailOnMissing) { - Write-Host "" - Write-Host "Exiting with error (FailOnMissing specified)" -ForegroundColor Red - exit 1 - } + Write-Host "" + Write-Host "Missing required dependencies:" -ForegroundColor Red + foreach ($m in $missing) { + Write-Host " - $($m.Name)" -ForegroundColor Red + } + + if ($FailOnMissing) { + Write-Host "" + Write-Host "Exiting with error (FailOnMissing specified)" -ForegroundColor Red + exit 1 + } } else { - Write-Host "Dependency preflight: all required dependencies are available" -ForegroundColor Green + Write-Host "Dependency preflight: all required dependencies are available" -ForegroundColor Green } if ($PassThru) { - return $results + return $results }