diff --git a/.vscode/settings.json b/.vscode/settings.json index 91244032f3..a70510a40a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -85,7 +85,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, @@ -132,4 +131,4 @@ // - Agent: Apply change-log (modifies files) // - Installer: Setup for patch (downloads artifacts) // ============================================================================ -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8e14bcddb2..c906460e45 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -334,7 +334,7 @@ { "label": "Test (native)", "type": "shell", - "command": "./test.ps1 -Native -Configuration ${input:testConfiguration}", + "command": "./test.ps1 -SkipManaged -Configuration ${input:testConfiguration}", "group": "test", "detail": "Run native tests via test.ps1 (dispatches to Build/scripts/Invoke-CppTest.ps1)", "options": { @@ -348,7 +348,7 @@ { "label": "Test (native, no build)", "type": "shell", - "command": "./test.ps1 -Native -NoBuild -Configuration ${input:testConfiguration}", + "command": "./test.ps1 -SkipManaged -NoBuild -Configuration ${input:testConfiguration}", "group": "test", "detail": "Run native tests via test.ps1 without rebuilding", "options": { @@ -530,4 +530,3 @@ } ] } - diff --git a/Build/Agent/FwBuildEnvironment.psm1 b/Build/Agent/FwBuildEnvironment.psm1 index 5b73699100..3f6b881818 100644 --- a/Build/Agent/FwBuildEnvironment.psm1 +++ b/Build/Agent/FwBuildEnvironment.psm1 @@ -1,148 +1,441 @@ <# .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-VsWherePath { + <# + .SYNOPSIS + Returns the path to the Microsoft-provided vswhere executable. + #> + $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-FwToolchainPolicy { + <# + .SYNOPSIS + Returns the repo-controlled FieldWorks toolchain policy. + #> + if ($script:FwToolchainPolicy) { + return $script:FwToolchainPolicy + } + + $policyPath = Join-Path (Split-Path -Parent $PSScriptRoot) 'FieldWorks.Toolchain.props' + $defaults = [ordered]@{ + VisualStudioMajor = '17' + VisualStudioVersionRange = '[17.0,18.0)' + VCTargetsVersion = 'v170' + PlatformToolset = 'v143' + DotNetFrameworkSdkVisualStudioVersion = '17.0' + } + + if (-not (Test-Path $policyPath)) { + $script:FwToolchainPolicy = [pscustomobject]$defaults + return $script:FwToolchainPolicy + } + + [xml]$policyXml = Get-Content -LiteralPath $policyPath -Raw + $propertyGroups = @($policyXml.Project.PropertyGroup) + + function Get-PolicyValue { + param( + [string]$Name, + [string]$DefaultValue + ) + + foreach ($propertyGroup in $propertyGroups) { + $node = $propertyGroup.$Name + if (-not $node) { + continue + } + + $value = $node.'#text' + if (-not [string]::IsNullOrWhiteSpace($value)) { + return $value.Trim() + } + } + + return $DefaultValue + } + + $script:FwToolchainPolicy = [pscustomobject]@{ + VisualStudioMajor = Get-PolicyValue -Name 'FwVisualStudioMajor' -DefaultValue $defaults.VisualStudioMajor + VisualStudioVersionRange = Get-PolicyValue -Name 'FwVisualStudioVersionRange' -DefaultValue $defaults.VisualStudioVersionRange + VCTargetsVersion = Get-PolicyValue -Name 'FwVCTargetsVersion' -DefaultValue $defaults.VCTargetsVersion + PlatformToolset = Get-PolicyValue -Name 'FwPlatformToolset' -DefaultValue $defaults.PlatformToolset + DotNetFrameworkSdkVisualStudioVersion = Get-PolicyValue -Name 'FwDotNetFrameworkSdkVisualStudioVersion' -DefaultValue $defaults.DotNetFrameworkSdkVisualStudioVersion + } + + return $script:FwToolchainPolicy +} + +function Get-VsInstallationInfo { + <# + .SYNOPSIS + Returns installation metadata for the latest matching Visual Studio instance. + #> + param( + [string[]]$Requires = @(), + [string]$VersionRange = '' + ) + + $vsWhere = Get-VsWherePath + if (-not $vsWhere) { + return $null + } + + if ([string]::IsNullOrWhiteSpace($VersionRange)) { + $VersionRange = (Get-FwToolchainPolicy).VisualStudioVersionRange + } + + $vsWhereArgs = @('-latest', '-products', '*') + if (-not [string]::IsNullOrWhiteSpace($VersionRange)) { + $vsWhereArgs += '-version' + $vsWhereArgs += $VersionRange + } + + if ($Requires -and $Requires.Count -gt 0) { + $vsWhereArgs += '-requires' + $vsWhereArgs += $Requires + } + + $installationPath = & $vsWhere @vsWhereArgs -property installationPath + if (-not $installationPath) { + return $null + } + + $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 + } + + $toolchainPolicy = Get-FwToolchainPolicy + + $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 + + $vsTestPath = Join-Path $installationPath 'Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe' + if (-not (Test-Path $vsTestPath)) { + $vsTestPath = $null + } + + $vcInstallDir = Join-Path $installationPath 'VC' + if (-not (Test-Path $vcInstallDir)) { + $vcInstallDir = $null + } + + $vcTargetsPath = $null + if (-not [string]::IsNullOrWhiteSpace($toolchainPolicy.VCTargetsVersion)) { + $vcTargetsPath = Join-Path $installationPath (Join-Path 'MSBuild\Microsoft\VC' $toolchainPolicy.VCTargetsVersion) + if (-not (Test-Path $vcTargetsPath)) { + $vcTargetsPath = $null + } + } + + return [pscustomobject]@{ + VsWherePath = $vsInfo.VsWherePath + InstallationPath = $installationPath + DisplayVersion = $vsInfo.DisplayVersion + VisualStudioVersionRange = $toolchainPolicy.VisualStudioVersionRange + VsDevCmdPath = $vsDevCmdPath + MSBuildPath = $msbuildPath + VSTestPath = $vsTestPath + VcInstallDir = $vcInstallDir + VCTargetsPath = $vcTargetsPath + PlatformToolset = $toolchainPolicy.PlatformToolset + DotNetFrameworkSdkVisualStudioVersion = $toolchainPolicy.DotNetFrameworkSdkVisualStudioVersion + } +} + +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') + ) + + $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 [string]::Equals($clDirectory, $normalizedToolPath, [System.StringComparison]::OrdinalIgnoreCase) -and + [string]::Equals($nmakeDirectory, $normalizedToolPath, [System.StringComparison]::OrdinalIgnoreCase) +} + +function Ensure-PreferredVcToolPath { + <# + .SYNOPSIS + Moves the active HostX64\x64 MSVC bin directory to the front of PATH. + #> + $preferred = Get-ActiveVcToolBinPath + 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 (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 + + $vsToolchain = Get-VsToolchainInfo -Requires @('Microsoft.Component.MSBuild', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64') + + if (-not $vsToolchain) { + $vsWhere = Get-VsWherePath + Write-Host '' + 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' + } + + 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' + } + + # x64-only build + $arch = 'amd64' + $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 + + $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 (Test-VsDevEnvironmentActive)) { + 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 +443,95 @@ 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 + } + + $toolchain = Get-VsToolchainInfo -Requires @('Microsoft.Component.MSBuild') + if ($toolchain -and $toolchain.MSBuildPath) { + return $toolchain.MSBuildPath + } + + 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 +539,25 @@ 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 + } + + $toolchain = Get-VsToolchainInfo -Requires @('Microsoft.Component.MSBuild') + if ($toolchain -and $toolchain.VSTestPath) { + return $toolchain.VSTestPath + } + + return $null } # ============================================================================= @@ -283,9 +565,14 @@ function Get-VSTestPath { # ============================================================================= Export-ModuleMember -Function @( - 'Initialize-VsDevEnvironment', + 'Get-VsWherePath', + 'Get-VsInstallationInfo', + 'Get-VsToolchainInfo', + 'Get-VsDevEnvironmentVariables', + 'Test-VsDevEnvironmentActive', + 'Initialize-VsDevEnvironment', 'Test-CvtresCompatibility', - 'Get-MSBuildPath', - 'Invoke-MSBuild', - 'Get-VSTestPath' + 'Get-MSBuildPath', + 'Invoke-MSBuild', + 'Get-VSTestPath' ) diff --git a/Build/Agent/Run-VsTests.ps1 b/Build/Agent/Run-VsTests.ps1 index 64ed1c1540..9de229e6b0 100644 --- a/Build/Agent/Run-VsTests.ps1 +++ b/Build/Agent/Run-VsTests.ps1 @@ -54,6 +54,7 @@ param( ) $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 @@ -71,12 +72,7 @@ if (-not $OutputDir) { } $runSettings = Join-Path $repoRoot "Test.runsettings" -$vsTestPath = "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe" - -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" -} +$vsTestPath = Get-VSTestPath if (-not (Test-Path $vsTestPath)) { Write-Error "vstest.console.exe not found. Install Visual Studio 2022 or Build Tools." diff --git a/Build/Agent/Setup-FwBuildEnv.ps1 b/Build/Agent/Setup-FwBuildEnv.ps1 index a95fe4c6f5..f6f4784d3a 100644 --- a/Build/Agent/Setup-FwBuildEnv.ps1 +++ b/Build/Agent/Setup-FwBuildEnv.ps1 @@ -35,6 +35,8 @@ param( ) $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") @@ -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 @@ -109,6 +110,7 @@ Set-EnvVar -Name "FW_ROOT_DATA_DIR" -Value $distFiles $results = @{ 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 - } +$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 not found via vswhere" -Status "FAIL" - $results.Errors += "Visual Studio not found" + 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,25 +159,11 @@ 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) { +else { Write-Status "MSBuild not found" -Status "FAIL" $results.Errors += "MSBuild not found" } @@ -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" } # ---------------------------------------------------------------------------- @@ -246,6 +216,7 @@ Write-Host "=== Setup Complete ===" -ForegroundColor Cyan 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 } # Return results object for programmatic use diff --git a/Build/Agent/Setup-InstallerBuild.ps1 b/Build/Agent/Setup-InstallerBuild.ps1 index 32f6e4ae92..5d076a60b3 100644 --- a/Build/Agent/Setup-InstallerBuild.ps1 +++ b/Build/Agent/Setup-InstallerBuild.ps1 @@ -47,6 +47,43 @@ 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 + +function Get-InstallerBuildCommand { + param( + [Parameter(Mandatory)] + [ValidateSet('RestorePackages', 'BuildInstaller', 'BuildPatchInstaller')] + [string]$Target + ) + + $arguments = @( + 'msbuild Build/InstallerBuild.proj', + "/t:$Target", + '/p:Configuration=Release', + '/p:Platform=x64' + ) + + if ($Target -ne 'RestorePackages') { + $arguments += @('/m', '/v:n') + } + + return $arguments -join ' ' +} + +function Get-WrappedInstallerBuildCommand { + param( + [Parameter(Mandatory)] + [string]$VsDevCmdPath, + [Parameter(Mandatory)] + [string]$Command + ) + + return 'cmd /c "call ""{0}"" -arch=amd64 >nul && {1}"' -f $VsDevCmdPath, $Command +} + +$restoreCommand = Get-InstallerBuildCommand -Target 'RestorePackages' +$buildCommand = Get-InstallerBuildCommand -Target 'BuildInstaller' +$patchCommand = Get-InstallerBuildCommand -Target 'BuildPatchInstaller' Write-Host "========================================" -ForegroundColor Cyan Write-Host " FieldWorks Installer Build Setup" -ForegroundColor Cyan @@ -55,6 +92,10 @@ Write-Host "" $issues = @() $warnings = @() +$vsDevEnvActive = $false +$restoreWrappedCommand = $null +$buildWrappedCommand = $null +$patchWrappedCommand = $null #region WiX Toolset Validation @@ -90,57 +131,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 = Get-WrappedInstallerBuildCommand -VsDevCmdPath $toolchain.VsDevCmdPath -Command $restoreCommand + $buildWrappedCommand = Get-WrappedInstallerBuildCommand -VsDevCmdPath $toolchain.VsDevCmdPath -Command $buildCommand + $patchWrappedCommand = Get-WrappedInstallerBuildCommand -VsDevCmdPath $toolchain.VsDevCmdPath -Command $patchCommand + } 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 @@ -327,15 +352,15 @@ if ($issues.Count -eq 0) { # VS Developer environment is active, show simple commands Write-Host "" Write-Host " # Restore packages" -ForegroundColor Gray - Write-Host " msbuild Build/InstallerBuild.proj /t:RestorePackages /p:Configuration=Release /p:Platform=x64" -ForegroundColor Cyan + Write-Host " $restoreCommand" -ForegroundColor Cyan Write-Host "" Write-Host " # Build base installer" -ForegroundColor Gray - Write-Host " msbuild Build/InstallerBuild.proj /t:BuildInstaller /p:Configuration=Release /p:Platform=x64 /p:config=release /m /v:n" -ForegroundColor Cyan + Write-Host " $buildCommand" -ForegroundColor Cyan Write-Host "" if ($SetupPatch) { Write-Host " # Build patch installer" -ForegroundColor Gray - Write-Host " msbuild Build/InstallerBuild.proj /t:BuildPatchInstaller /p:Configuration=Release /p:Platform=x64 /p:config=release /m /v:n" -ForegroundColor Cyan + Write-Host " $patchCommand" -ForegroundColor Cyan Write-Host "" } } else { @@ -344,17 +369,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 10c943dc16..60250ad7a4 100644 --- a/Build/Agent/Verify-FwDependencies.ps1 +++ b/Build/Agent/Verify-FwDependencies.ps1 @@ -5,6 +5,8 @@ .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 @@ -24,7 +26,8 @@ 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 @@ -45,6 +48,9 @@ .EXAMPLE # 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()] @@ -56,6 +62,7 @@ param( ) $ErrorActionPreference = 'Stop' +Import-Module (Join-Path $PSScriptRoot 'FwBuildEnvironment.psm1') -Force function Test-Dependency { param( @@ -158,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 @@ -173,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)" } @@ -217,7 +224,7 @@ $results += Test-Dependency -Name "WiX Toolset (v6 via NuGet)" -Required "Option throw "Installer project not found: $wixProj" } - [xml]$wixProjXml = Get-Content -LiteralPath $wixProj + [xml]$wixProjXml = Get-Content -LiteralPath $wixProj -Raw $projectNode = $wixProjXml.Project $hasWixSdk = $false diff --git a/Build/Agent/fix-whitespace.ps1 b/Build/Agent/fix-whitespace.ps1 index 324a254a2f..1933489889 100644 --- a/Build/Agent/fix-whitespace.ps1 +++ b/Build/Agent/fix-whitespace.ps1 @@ -78,7 +78,7 @@ if (Test-Path -LiteralPath 'check-results.log') { if (-not $fixFiles -or $fixFiles.Count -eq 0) { $base = Get-BaseRef Write-Host "Fixing whitespace for files changed since $base..HEAD" - $fixFiles = git diff --name-only "$base"..HEAD + $fixFiles = git diff --name-only "$base..HEAD" } $files = $fixFiles | Where-Object { $_ -and (Test-Path $_) } diff --git a/Build/FieldWorks.Toolchain.props b/Build/FieldWorks.Toolchain.props new file mode 100644 index 0000000000..e1b0a2e65e --- /dev/null +++ b/Build/FieldWorks.Toolchain.props @@ -0,0 +1,13 @@ + + + + 17 + [17.0,18.0) + v170 + v143 + 17.0 + + diff --git a/Build/Src/FwBuildTasks/BuildUtils.cs b/Build/Src/FwBuildTasks/BuildUtils.cs index 0484dd09d2..cf69ecd8b5 100644 --- a/Build/Src/FwBuildTasks/BuildUtils.cs +++ b/Build/Src/FwBuildTasks/BuildUtils.cs @@ -11,6 +11,7 @@ using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Xml.Xsl; +using System.Xml.Linq; using System.Text; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; @@ -24,6 +25,7 @@ public static class BuildUtils { public static bool IsUnix => Environment.OSVersion.Platform == PlatformID.Unix; private const string NetFxLegacyVersionFolder = "v4.0.30319"; + private const string ToolchainPolicyRelativePath = "Build\\FieldWorks.Toolchain.props"; private static readonly string[] NetFxFrameworkFolders = { "Framework64", "Framework" }; /// @@ -267,19 +269,92 @@ public static string ResolveDotNetFrameworkAssemblyPath(string assemblyFileName) $"Probed: {string.Join("; ", probeLog.Distinct())}"); } - private static IEnumerable GetToolLocationHelperCandidates(string toolName, ICollection probeLog) + private static string FindToolchainPolicyFile() { - var candidates = new List(); + var repoRoot = Environment.GetEnvironmentVariable("FW_ROOT_CODE_DIR"); + if (!String.IsNullOrWhiteSpace(repoRoot)) + { + var repoCandidate = Path.Combine(repoRoot, ToolchainPolicyRelativePath); + if (File.Exists(repoCandidate)) + return repoCandidate; + } + + for (var directory = new DirectoryInfo(GetAssemblyFolder()); directory != null; directory = directory.Parent) + { + var candidate = Path.Combine(directory.FullName, ToolchainPolicyRelativePath); + if (File.Exists(candidate)) + return candidate; + } + + return null; + } + + private static string GetToolchainPolicyProperty(string propertyName) + { + var policyFile = FindToolchainPolicyFile(); + if (String.IsNullOrEmpty(policyFile)) + return null; + try { - candidates.Add(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile( - toolName, - TargetDotNetFrameworkVersion.Version48, - VisualStudioVersion.Version170, - DotNetFrameworkArchitecture.Bitness64)); + var policyDocument = XDocument.Load(policyFile); + if (policyDocument.Root == null) + return null; + + return policyDocument.Root + .Elements("PropertyGroup") + .Elements(propertyName) + .Select(element => element.Value == null ? null : element.Value.Trim()) + .FirstOrDefault(value => !String.IsNullOrEmpty(value)); } catch { + return null; + } + } + + private static VisualStudioVersion? GetConfiguredDotNetFrameworkSdkVisualStudioVersion() + { + switch (GetToolchainPolicyProperty("FwDotNetFrameworkSdkVisualStudioVersion")) + { + case "10.0": + return VisualStudioVersion.Version100; + case "11.0": + return VisualStudioVersion.Version110; + case "12.0": + return VisualStudioVersion.Version120; + case "14.0": + return VisualStudioVersion.Version140; + case "15.0": + return VisualStudioVersion.Version150; + case "16.0": + return VisualStudioVersion.Version160; + case "17.0": + return VisualStudioVersion.Version170; + case "18.0": + return VisualStudioVersion.Version180; + default: + return null; + } + } + + private static IEnumerable GetToolLocationHelperCandidates(string toolName, ICollection probeLog) + { + var candidates = new List(); + var configuredVisualStudioVersion = GetConfiguredDotNetFrameworkSdkVisualStudioVersion(); + if (configuredVisualStudioVersion.HasValue) + { + try + { + candidates.Add(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile( + toolName, + TargetDotNetFrameworkVersion.Version48, + configuredVisualStudioVersion.Value, + DotNetFrameworkArchitecture.Bitness64)); + } + catch + { + } } try diff --git a/Build/Src/FwBuildTasks/Make.cs b/Build/Src/FwBuildTasks/Make.cs index 55458e919d..4a91985688 100644 --- a/Build/Src/FwBuildTasks/Make.cs +++ b/Build/Src/FwBuildTasks/Make.cs @@ -111,9 +111,21 @@ protected override string ToolName } } + private static string FindToolInDirectory(string directory, string toolName) + { + if (String.IsNullOrEmpty(directory) || !Directory.Exists(directory)) + return null; + + if (File.Exists(Path.Combine(directory, toolName))) + return directory; + + return null; + } + 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(); @@ -128,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))) @@ -137,10 +149,27 @@ private void CheckToolPath() return; } } - // Fall Back to the install directory (if VCINSTALLDIR is set) + if (!String.IsNullOrEmpty(vcToolsInstallDir)) + { + string activeToolPath = FindToolInDirectory(Path.Combine(vcToolsInstallDir, "bin", "Hostx64", "x64"), ToolName); + if (!String.IsNullOrEmpty(activeToolPath)) + { + ToolPath = activeToolPath; + return; + } + } + + // Fall back to the legacy VC install directory (if VCINSTALLDIR is set) if (!String.IsNullOrEmpty(vcInstallDir)) { - ToolPath = Path.Combine(vcInstallDir, "bin"); + string legacyToolPath = FindToolInDirectory(Path.Combine(vcInstallDir, "bin"), ToolName); + if (!String.IsNullOrEmpty(legacyToolPath)) + { + ToolPath = legacyToolPath; + return; + } + + ToolPath = String.Empty; } else { diff --git a/Directory.Build.props b/Directory.Build.props index 7495546d05..58dd34ad0e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,4 +1,8 @@ + + + true diff --git a/Lib/src/unit++/VS/unit++.vcxproj b/Lib/src/unit++/VS/unit++.vcxproj index 29bfc46673..f82f49fc28 100644 --- a/Lib/src/unit++/VS/unit++.vcxproj +++ b/Lib/src/unit++/VS/unit++.vcxproj @@ -19,12 +19,12 @@ StaticLibrary MultiByte - v143 + $(FwPlatformToolset) StaticLibrary MultiByte - v143 + $(FwPlatformToolset) @@ -96,4 +96,4 @@ - \ No newline at end of file + 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 diff --git a/Src/DebugProcs/DebugProcs.vcxproj b/Src/DebugProcs/DebugProcs.vcxproj index ddb0c867e2..a2debafda4 100644 --- a/Src/DebugProcs/DebugProcs.vcxproj +++ b/Src/DebugProcs/DebugProcs.vcxproj @@ -23,11 +23,11 @@ Makefile - v143 + $(FwPlatformToolset) Makefile - v143 + $(FwPlatformToolset) @@ -84,4 +84,4 @@ - \ No newline at end of file + diff --git a/Src/Generic/Generic.vcxproj b/Src/Generic/Generic.vcxproj index 6685a478b5..dc847d87c2 100644 --- a/Src/Generic/Generic.vcxproj +++ b/Src/Generic/Generic.vcxproj @@ -30,15 +30,15 @@ Makefile - v143 + $(FwPlatformToolset) Makefile - v143 + $(FwPlatformToolset) Makefile - v143 + $(FwPlatformToolset) diff --git a/Src/Generic/Test/TestGeneric.vcxproj b/Src/Generic/Test/TestGeneric.vcxproj index 895df81cbe..17eb8439d6 100644 --- a/Src/Generic/Test/TestGeneric.vcxproj +++ b/Src/Generic/Test/TestGeneric.vcxproj @@ -43,13 +43,13 @@ Application - v143 + $(FwPlatformToolset) NotSet true Application - v143 + $(FwPlatformToolset) NotSet false true diff --git a/Src/Kernel/Kernel.vcxproj b/Src/Kernel/Kernel.vcxproj index 438e89476f..1e5e3c0a62 100644 --- a/Src/Kernel/Kernel.vcxproj +++ b/Src/Kernel/Kernel.vcxproj @@ -26,11 +26,11 @@ Makefile - v143 + $(FwPlatformToolset) Makefile - v143 + $(FwPlatformToolset) @@ -84,4 +84,3 @@ - diff --git a/Src/LexText/ParserCore/XAmpleCOMWrapper/XAmpleCOMWrapper.vcxproj b/Src/LexText/ParserCore/XAmpleCOMWrapper/XAmpleCOMWrapper.vcxproj index 67bc3bdd13..4378e22621 100644 --- a/Src/LexText/ParserCore/XAmpleCOMWrapper/XAmpleCOMWrapper.vcxproj +++ b/Src/LexText/ParserCore/XAmpleCOMWrapper/XAmpleCOMWrapper.vcxproj @@ -24,19 +24,19 @@ DynamicLibrary Static MultiByte - v143 + $(FwPlatformToolset) DynamicLibrary Static MultiByte - v143 + $(FwPlatformToolset) DynamicLibrary Static MultiByte - v143 + $(FwPlatformToolset) @@ -224,4 +224,4 @@ - \ No newline at end of file + diff --git a/Src/Utilities/pcpatrflex/DisambiguateInFLExDB/DisambiguateInFLExDBTests/ToneParsInvokerTests.cs b/Src/Utilities/pcpatrflex/DisambiguateInFLExDB/DisambiguateInFLExDBTests/ToneParsInvokerTests.cs index 898a185dee..a41d47ef96 100644 --- a/Src/Utilities/pcpatrflex/DisambiguateInFLExDB/DisambiguateInFLExDBTests/ToneParsInvokerTests.cs +++ b/Src/Utilities/pcpatrflex/DisambiguateInFLExDB/DisambiguateInFLExDBTests/ToneParsInvokerTests.cs @@ -192,18 +192,25 @@ private string NormalizeContent(string input) private static string NormalizeViaIndex(string input, string match, string change) { - // I tried to use regular expressions but never got them to match... - int iAppData = input.IndexOf(match); - if (iAppData != -1) + int searchStart = 0; + while (searchStart < input.Length) { - int iColon = input.IndexOf(":"); - if (iColon != -1) + int matchIndex = input.IndexOf(match, searchStart, StringComparison.OrdinalIgnoreCase); + if (matchIndex == -1) + return input; + + int colonIndex = input.LastIndexOf(':', matchIndex); + if (colonIndex > 0) { - iColon--; // skip the drive letter, too - string appdataPath = input.Substring(iColon, iAppData - iColon); - input = input.Replace(appdataPath, change); + int pathStart = colonIndex - 1; + input = input.Remove(pathStart, matchIndex - pathStart).Insert(pathStart, change); + searchStart = pathStart + change.Length + match.Length; + continue; } + + searchStart = matchIndex + match.Length; } + return input; } diff --git a/Src/views/Test/TestViews.vcxproj b/Src/views/Test/TestViews.vcxproj index 033270aa47..16b1166024 100644 --- a/Src/views/Test/TestViews.vcxproj +++ b/Src/views/Test/TestViews.vcxproj @@ -51,13 +51,13 @@ Application - v143 + $(FwPlatformToolset) NotSet true Application - v143 + $(FwPlatformToolset) NotSet false true diff --git a/Src/views/views.vcxproj b/Src/views/views.vcxproj index bc15e8bf27..1b846d0c43 100644 --- a/Src/views/views.vcxproj +++ b/Src/views/views.vcxproj @@ -30,17 +30,17 @@ Makefile - v143 + $(FwPlatformToolset) false Makefile - v143 + $(FwPlatformToolset) false Makefile - v143 + $(FwPlatformToolset) false @@ -217,4 +217,3 @@ -