From 1c9d80c06621274f3c3db9315678695de9c4514e Mon Sep 17 00:00:00 2001 From: 5an7y Date: Thu, 19 Mar 2026 15:36:29 -0700 Subject: Add Build-Samples.ps1, ListAllSamples.ps1, fix Build-Sample retry bug - ListAllSamples.ps1: Enumerates .sln files and writes Samples.txt - Build-Samples.ps1: Unified build script reading Samples.txt with exclusion support, parallel execution, and report generation - Build-Sample.ps1: Fix retry bug where all-fail exits 0 instead of 1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Build-Samples.ps1 | 407 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 407 insertions(+) create mode 100644 Build-Samples.ps1 (limited to 'Build-Samples.ps1') diff --git a/Build-Samples.ps1 b/Build-Samples.ps1 new file mode 100644 index 00000000..c8ad72f5 --- /dev/null +++ b/Build-Samples.ps1 @@ -0,0 +1,407 @@ +<# +.SYNOPSIS + Builds driver samples from a sample list file with parallel execution and exclusion support. + +.DESCRIPTION + Reads sample names from Samples.txt (or a specified file), processes exclusions from + exclusions.csv, determines the build environment, and builds all non-excluded samples + in parallel using Build-Sample.ps1. Generates CSV and HTML overview reports. + + Requires PowerShell 7+ (uses ForEach-Object -Parallel and ternary operators). + +.PARAMETER SampleListPath + Path to the file containing sample names (one per line). Defaults to Samples.txt in the + current directory. + +.PARAMETER Samples + Optional array of specific sample names to build. When provided, overrides SampleListPath. + +.PARAMETER Configurations + Build configurations (e.g. 'Debug','Release'). Defaults to $env:WDS_Configuration or + ('Debug','Release'). + +.PARAMETER Platforms + Build platforms (e.g. 'x64','arm64'). Defaults to $env:WDS_Platform or ('x64','arm64'). + +.PARAMETER LogFilesDirectory + Directory for build log files. Defaults to _logs in the current directory. + +.PARAMETER ReportFileName + Base name for the report files (without extension). Defaults to $env:WDS_ReportFileName + or '_overview'. + +.PARAMETER InfOptions + Additional InfVerif options. If not provided, determined automatically based on build number. + +.PARAMETER ThrottleLimit + Maximum parallel build jobs. Defaults to 5 x logical processors. + +.EXAMPLE + .\Build-Samples + +.EXAMPLE + .\Build-Samples -Samples 'audio.acx.samples.audiocodec.driver','usb.kmdf_fx2' -Configurations 'Debug' -Platforms 'x64' + +.EXAMPLE + .\Build-Samples -SampleListPath .\MySamples.txt -ThrottleLimit 8 +#> + +[CmdletBinding()] +param( + [string]$SampleListPath = (Join-Path (Get-Location) "Samples.txt"), + [string[]]$Samples, + [string[]]$Configurations = @(if ([string]::IsNullOrEmpty($env:WDS_Configuration)) { ('Debug', 'Release') } else { $env:WDS_Configuration }), + [string[]]$Platforms = @(if ([string]::IsNullOrEmpty($env:WDS_Platform)) { ('x64', 'arm64') } else { $env:WDS_Platform }), + [string]$LogFilesDirectory = (Join-Path (Get-Location) "_logs"), + [string]$ReportFileName = $(if ([string]::IsNullOrEmpty($env:WDS_ReportFileName)) { "_overview" } else { $env:WDS_ReportFileName }), + [string]$InfOptions, + [int]$ThrottleLimit = 0 +) + +$root = (Get-Location).Path + +# Launch developer PowerShell if necessary +if (-not $env:VSCMD_VER) { + Import-Module (Resolve-Path "$env:ProgramFiles\Microsoft Visual Studio\*\*\Common7\Tools\Microsoft.VisualStudio.DevShell.dll") + Enter-VsDevShell -VsInstallPath (Resolve-Path "$env:ProgramFiles\Microsoft Visual Studio\*\*") + Set-Location $root +} + +$ThrottleFactor = 5 +$LogicalProcessors = (Get-CIMInstance -Class 'CIM_Processor' -Verbose:$false).NumberOfLogicalProcessors + +if ($ThrottleLimit -eq 0) { + $ThrottleLimit = $ThrottleFactor * $LogicalProcessors +} + +$Verbose = $false +if ($PSBoundParameters.ContainsKey('Verbose')) { + $Verbose = $PsBoundParameters.Get_Item('Verbose') +} + +# Prepare log directory +Remove-Item -Recurse -Path $LogFilesDirectory 2>&1 | Out-Null +New-Item -ItemType Directory -Force -Path $LogFilesDirectory | Out-Null + +$reportFilePath = Join-Path $LogFilesDirectory "$ReportFileName.htm" +$reportCsvFilePath = Join-Path $LogFilesDirectory "$ReportFileName.csv" + +# Verify msbuild is available +$oldPreference = $ErrorActionPreference +$ErrorActionPreference = "stop" +try { + Get-Command "msbuild" | Out-Null +} +catch { + Write-Host "`u{274C} msbuild cannot be called from current environment. Check that msbuild is set in current path (for example, that it is called from a Visual Studio developer command)." + Write-Error "msbuild cannot be called from current environment." + exit 1 +} +finally { + $ErrorActionPreference = $oldPreference +} + +# +# Load sample list +# +if ($Samples) { + $sampleNames = $Samples +} +else { + if (-not (Test-Path $SampleListPath)) { + Write-Error "Sample list file not found: $SampleListPath. Run .\ListAllSamples.ps1 first to generate Samples.txt." + exit 1 + } + $sampleNames = Get-Content $SampleListPath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } +} + +# Build the sampleSet hashtable: sampleName -> directory path +$sampleSet = @{} +foreach ($name in $sampleNames) { + $relativePath = $name.Replace('.', '\') + $fullPath = Join-Path $root $relativePath + $sampleSet[$name] = $fullPath +} + +# +# Determine build environment +# +$build_environment = "" +$build_number = 0 +$nuget_package_version = 0 + +if ($env:GITHUB_REPOSITORY) { + $build_environment = "GitHub" + $nuget_package_version = ([regex]'(?<=x64\.)(\d+\.)(\d+\.)(\d+\.)(\d+)').Matches((Get-ChildItem .\packages\*WDK.x64* -Name)).Value + $build_number = $nuget_package_version.split('.')[2] +} +elseif (Test-Path(".\packages\*")) { + $build_environment = "NuGet" + $nuget_package_version = ([regex]'(?<=x64\.)(\d+\.)(\d+\.)(\d+\.)(\d+)').Matches((Get-ChildItem .\packages\*WDK.x64* -Name)).Value + $build_number = $nuget_package_version.split('.')[2] +} +elseif ($env:BuildLab -match '(?[^.]*).(?[^.]*).(?[^.]*)') { + $build_environment = "EWDK." + $Matches.branch + "." + $Matches.build + "." + $Matches.qfe + $build_number = $Matches.build +} +elseif ($env:UCRTVersion -match '10.0.(?.*).0') { + $build_environment = "WDK" + $build_number = $Matches.build +} +else { + Write-Output "Environment variables {" + Get-ChildItem env:* | Sort-Object name + Write-Output "Environment variables }" + Write-Error "Could not determine build environment." + exit 1 +} + +# Determine WDK Visual Studio Component version +if ($build_environment -match '^EWDK') { + $wdk_vs_component_ver = "(WDK Visual Studio Component Version is not included for EWDK builds)" +} +else { + $wdk_vs_component_ver = Get-ChildItem "${env:ProgramData}\Microsoft\VisualStudio\Packages\Microsoft.Windows.DriverKit,version=*" -ErrorAction SilentlyContinue + if (-not $wdk_vs_component_ver) { + Write-Error "WDK Visual Studio Component version not found. Please ensure the WDK Component is installed." + exit 1 + } + $wdk_vs_component_ver = [regex]::Match($wdk_vs_component_ver.Name, '(\d+\.){3}\d+').Value +} + +# +# InfVerif_AdditionalOptions +# +if ($InfOptions) { + $InfVerif_AdditionalOptions = $InfOptions +} +else { + $InfVerif_AdditionalOptions = ($build_number -le 22621 ? "/sw1284 /sw1285 /sw1293 /sw2083 /sw2086" : "/samples") +} + +# +# Load exclusions from exclusions.csv +# +$exclusionConfigurations = @{} +$exclusionReasons = @{} +Import-Csv 'exclusions.csv' | ForEach-Object { + $excluded_driver = $_.Path.Replace($root, '').Trim('\').Replace('\', '.').ToLower() + $excluded_configurations = ($_.configurations -eq '' ? '*' : $_.configurations) + $excluded_minbuild = ($_.MinBuild -eq '' ? 00000 : $_.MinBuild) + $excluded_maxbuild = ($_.MaxBuild -eq '' ? 99999 : $_.MaxBuild) + if (($excluded_minbuild -le $build_number) -and ($build_number -le $excluded_maxbuild)) { + $exclusionConfigurations[$excluded_driver] = $excluded_configurations + $exclusionReasons[$excluded_driver] = $_.Reason + Write-Verbose "Exclusion.csv entry applied for '$excluded_driver' for configuration '$excluded_configurations'." + } + else { + Write-Verbose "Exclusion.csv entry not applied for '$excluded_driver' due to build number." + } +} + +# +# Build all samples +# +$jresult = @{ + SolutionsBuilt = 0 + SolutionsSucceeded = 0 + SolutionsExcluded = 0 + SolutionsUnsupported = 0 + SolutionsFailed = 0 + SolutionsSporadic = 0 + Results = @() + FailSet = @() + SporadicSet = @() + lock = [System.Threading.Mutex]::new($false) +} + +$SolutionsTotal = $sampleSet.Count * $Configurations.Count * $Platforms.Count + +Write-Output "WDK Build Environment: $build_environment" +Write-Output "WDK Build Number: $build_number" +if (($build_environment -eq "GitHub") -or ($build_environment -eq "NuGet")) { + Write-Output "WDK Nuget Version: $nuget_package_version" +} +Write-Output "WDK Visual Studio Component Version: $wdk_vs_component_ver" +Write-Output "Samples: $($sampleSet.Count)" +Write-Output "Configurations: $($Configurations.Count) ($Configurations)" +Write-Output "Platforms: $($Platforms.Count) ($Platforms)" +Write-Output "InfVerif_AdditionalOptions: $InfVerif_AdditionalOptions" +Write-Output "Combinations: $SolutionsTotal" +Write-Output "LogicalProcessors: $LogicalProcessors" +Write-Output "ThrottleFactor: $ThrottleFactor" +Write-Output "ThrottleLimit: $ThrottleLimit" +Write-Output "WDS_WipeOutputs: $env:WDS_WipeOutputs" +Write-Output "Disk Remaining (GB): $(((Get-Volume ((Get-Item ".").PSDrive.Name)).SizeRemaining) / 1GB)" +Write-Output "" +Write-Output "T: Combinations" +Write-Output "B: Built" +Write-Output "R: Build is running currently" +Write-Output "P: Build is pending an available build slot" +Write-Output "" +Write-Output "S: Built and result was 'Succeeded'" +Write-Output "E: Built and result was 'Excluded'" +Write-Output "U: Built and result was 'Unsupported' (Platform and Configuration combination)" +Write-Output "F: Built and result was 'Failed'" +Write-Output "O: Built and result was 'Sporadic'" +Write-Output "" +Write-Output "Building all combinations..." + +$sw = [Diagnostics.Stopwatch]::StartNew() + +$sampleSet.GetEnumerator() | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel { + $LogFilesDirectory = $using:LogFilesDirectory + $exclusionConfigurations = $using:exclusionConfigurations + $exclusionReasons = $using:exclusionReasons + $Configurations = $using:Configurations + $Platforms = $using:Platforms + $InfVerif_AdditionalOptions = $using:InfVerif_AdditionalOptions + $Verbose = $using:Verbose + + $sampleName = $_.Key + $directory = $_.Value + + $ResultElement = New-Object psobject + Add-Member -InputObject $ResultElement -MemberType NoteProperty -Name Sample -Value "$sampleName" + + foreach ($configuration in $Configurations) { + foreach ($platform in $Platforms) { + $thisunsupported = 0 + $thisfailed = 0 + $thissporadic = 0 + $thisexcluded = 0 + $thissucceeded = 0 + $thisresult = "Not run" + $thisfailset = @() + $thissporadicset = @() + + if ($exclusionConfigurations.ContainsKey($sampleName) -and ($exclusionConfigurations[$sampleName].Split(';') | Where-Object { "$configuration|$platform" -like $_ })) { + Write-Verbose "[$sampleName $configuration|$platform] `u{23E9} Excluded and skipped. Reason: $($exclusionReasons[$sampleName])" + $thisexcluded += 1 + $thisresult = "Excluded" + } + else { + .\Build-Sample -Directory $directory -SampleName $sampleName -LogFilesDirectory $LogFilesDirectory -Configuration $configuration -Platform $platform -InfVerif_AdditionalOptions $InfVerif_AdditionalOptions -Verbose:$Verbose + if ($LASTEXITCODE -eq 0) { + $thissucceeded += 1 + $thisresult = "Succeeded" + } + elseif ($LASTEXITCODE -eq 1) { + $thisfailset += "$sampleName $configuration|$platform" + $thisfailed += 1 + $thisresult = "Failed" + } + elseif ($LASTEXITCODE -eq 2) { + $thissporadicset += "$sampleName $configuration|$platform" + $thissporadic += 1 + $thisresult = "Sporadic" + } + else { + $thisunsupported += 1 + $thisresult = "Unsupported" + } + } + Add-Member -InputObject $ResultElement -MemberType NoteProperty -Name "$configuration|$platform" -Value "$thisresult" + + $null = ($using:jresult).lock.WaitOne() + try { + ($using:jresult).SolutionsBuilt += 1 + ($using:jresult).SolutionsSucceeded += $thissucceeded + ($using:jresult).SolutionsExcluded += $thisexcluded + ($using:jresult).SolutionsUnsupported += $thisunsupported + ($using:jresult).SolutionsFailed += $thisfailed + ($using:jresult).SolutionsSporadic += $thissporadic + ($using:jresult).FailSet += $thisfailset + ($using:jresult).SporadicSet += $thissporadicset + $SolutionsTotal = $using:SolutionsTotal + $ThrottleLimit = $using:ThrottleLimit + $SolutionsBuilt = ($using:jresult).SolutionsBuilt + $SolutionsRemaining = $SolutionsTotal - $SolutionsBuilt + $SolutionsRunning = if ($SolutionsRemaining -ge $ThrottleLimit) { $ThrottleLimit } else { $SolutionsRemaining } + $SolutionsPending = if ($SolutionsRemaining -ge $ThrottleLimit) { ($SolutionsRemaining - $ThrottleLimit) } else { 0 } + $SolutionsBuiltPercent = [Math]::Round(100 * ($SolutionsBuilt / $using:SolutionsTotal)) + $TBRP = "T:" + ($SolutionsTotal) + "; B:" + (($using:jresult).SolutionsBuilt) + "; R:" + ($SolutionsRunning) + "; P:" + ($SolutionsPending) + $rstr = "S:" + (($using:jresult).SolutionsSucceeded) + "; E:" + (($using:jresult).SolutionsExcluded) + "; U:" + (($using:jresult).SolutionsUnsupported) + "; F:" + (($using:jresult).SolutionsFailed) + "; O:" + (($using:jresult).SolutionsSporadic) + Write-Progress -Activity "Building combinations" -Status "$SolutionsBuilt of $using:SolutionsTotal combinations built ($SolutionsBuiltPercent%) | $TBRP | $rstr" -PercentComplete $SolutionsBuiltPercent + } + finally { + ($using:jresult).lock.ReleaseMutex() + } + } + } + $null = ($using:jresult).lock.WaitOne() + try { + ($using:jresult).Results += $ResultElement + } + finally { + ($using:jresult).lock.ReleaseMutex() + } +} + +$sw.Stop() + +Write-Output "" + +if ($jresult.FailSet.Count -gt 0) { + Write-Output "Some combinations were built with errors:" + $jresult.FailSet = $jresult.FailSet | Sort-Object + foreach ($failedSample in $jresult.FailSet) { + $failedSample -match "^(.*) (\w*)\|(\w*)$" | Out-Null + $failName = $Matches[1] + $failConfiguration = $Matches[2] + $failPlatform = $Matches[3] + Write-Output "Build errors in Sample $failName; Configuration: $failConfiguration; Platform: $failPlatform {" + Get-Content "$LogFilesDirectory\$failName.$failConfiguration.$failPlatform.0.err" | Write-Output + Write-Output "} $failedSample" + } + Write-Error "Some combinations were built with errors." + Write-Output "" +} + +if ($jresult.SporadicSet.Count -gt 0) { + Write-Output "Some combinations were built with sporadic error:" + $jresult.SporadicSet = $jresult.SporadicSet | Sort-Object + foreach ($sporadicSample in $jresult.SporadicSet) { + $sporadicSample -match "^(.*) (\w*)\|(\w*)$" | Out-Null + $sporadicName = $Matches[1] + $sporadicConfiguration = $Matches[2] + $sporadicPlatform = $Matches[3] + Write-Output "Build sporadic errors in Sample $sporadicName; Configuration: $sporadicConfiguration; Platform: $sporadicPlatform {" + Get-Content "$LogFilesDirectory\$sporadicName.$sporadicConfiguration.$sporadicPlatform.0.err" | Write-Output + Write-Output "} $sporadicSample" + } + Write-Error "Some combinations were built with sporadic errors." + Write-Output "" +} + +# Display timer statistics +$min = $sw.Elapsed.Minutes +$seconds = $sw.Elapsed.Seconds + +$SolutionsSucceeded = $jresult.SolutionsSucceeded +$SolutionsExcluded = $jresult.SolutionsExcluded +$SolutionsUnsupported = $jresult.SolutionsUnsupported +$SolutionsFailed = $jresult.SolutionsFailed +$SolutionsSporadic = $jresult.SolutionsSporadic +$Results = $jresult.Results + +Write-Output "Built all combinations." +Write-Output "" +Write-Output "Elapsed time: $min minutes, $seconds seconds." +Write-Output ("Disk Remaining (GB): " + (((Get-Volume (Get-Item ".").PSDrive.Name).SizeRemaining / 1GB))) +Write-Output ("Samples: " + $sampleSet.Count) +Write-Output ("Configurations: " + $Configurations.Count + " (" + $Configurations + ")") +Write-Output ("Platforms: " + $Platforms.Count + " (" + $Platforms + ")") +Write-Output "Combinations: $SolutionsTotal" +Write-Output "Succeeded: $SolutionsSucceeded" +Write-Output "Excluded: $SolutionsExcluded" +Write-Output "Unsupported: $SolutionsUnsupported" +Write-Output "Failed: $SolutionsFailed" +Write-Output "Sporadic: $SolutionsSporadic" +Write-Output "Log files directory: $LogFilesDirectory" +Write-Output "Overview report: $reportFilePath" +Write-Output "" + +$Results | Sort-Object { $_.Sample } | ConvertTo-Csv | Out-File $reportCsvFilePath +$Results | Sort-Object { $_.Sample } | ConvertTo-Html -Title "Overview" | Out-File $reportFilePath +Invoke-Item $reportFilePath -- cgit v1.3.1 From 9cd8d031aa48669e9e3cc11886d856ce52d6ce20 Mon Sep 17 00:00:00 2001 From: 5an7y Date: Fri, 20 Mar 2026 15:09:27 -0700 Subject: Refactor Build-Samples.ps1 and fix ListAllSamples NuGet filter Build-Samples.ps1: - Extract helper functions for readability (Resolve-BuildEnvironment, Import-SampleExclusions, Initialize-DevShell, Assert-MsBuildAvailable, Get-DiskFreeGB) - Add #Requires -Version 7.0 - Fix build_number to explicit [int] cast (was string from regex) - Fix multi-socket CPU handling (sum LogicalProcessors) - Fix NuGet path detection to use absolute repo root path - Support wildcard patterns in exclusion paths (-like vs ContainsKey) - Guard Invoke-Item for CI/automation (skip in non-interactive sessions) - Validate sample directories exist before building - Wrap Get-Volume in try/catch for UNC/network drives - Use Write-Host with carriage return for progress (Write-Progress unreliable in -Parallel runspaces) - Clean up variable names and add section comments - Use switch instead of if/elseif for exit codes ListAllSamples.ps1: - Fix NuGet packages filter regex to catch packages anywhere in path (was only matching paths starting with packages) Samples.txt: - Regenerated with fixed filter, remove BOM Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Build-Samples.ps1 | 730 +++++++++++++++++++++++++++++++++-------------------- ListAllSamples.ps1 | 2 +- Samples.txt | 4 +- 3 files changed, 461 insertions(+), 275 deletions(-) (limited to 'Build-Samples.ps1') diff --git a/Build-Samples.ps1 b/Build-Samples.ps1 index c8ad72f5..6fd86bd3 100644 --- a/Build-Samples.ps1 +++ b/Build-Samples.ps1 @@ -3,15 +3,24 @@ Builds driver samples from a sample list file with parallel execution and exclusion support. .DESCRIPTION - Reads sample names from Samples.txt (or a specified file), processes exclusions from - exclusions.csv, determines the build environment, and builds all non-excluded samples - in parallel using Build-Sample.ps1. Generates CSV and HTML overview reports. + This is the main build orchestrator for driver samples. It performs these steps: - Requires PowerShell 7+ (uses ForEach-Object -Parallel and ternary operators). + 1. Ensures a developer build environment (VS DevShell or EWDK) is active + 2. Reads sample names from Samples.txt (or a specified file/array) + 3. Detects the build environment (GitHub CI, NuGet, EWDK, or WDK) and build number + 4. Loads exclusions from exclusions.csv (supports wildcard paths) + 5. Builds all non-excluded sample/configuration/platform combinations in parallel + 6. Generates CSV and HTML overview reports + + Requires PowerShell 7+ (uses ForEach-Object -Parallel). + + Typical call chain: + Build-AllSamples.ps1 -> ListAllSamples.ps1 -> Build-Samples.ps1 -> Build-Sample.ps1 + (this script) .PARAMETER SampleListPath - Path to the file containing sample names (one per line). Defaults to Samples.txt in the - current directory. + Path to the file containing sample names (one per line, dot-separated). + Defaults to Samples.txt in the current directory. .PARAMETER Samples Optional array of specific sample names to build. When provided, overrides SampleListPath. @@ -31,7 +40,8 @@ or '_overview'. .PARAMETER InfOptions - Additional InfVerif options. If not provided, determined automatically based on build number. + Additional InfVerif options (e.g. '/samples', '/msft'). If not provided, determined + automatically based on the WDK build number. .PARAMETER ThrottleLimit Maximum parallel build jobs. Defaults to 5 x logical processors. @@ -39,13 +49,21 @@ .EXAMPLE .\Build-Samples + Builds all samples from Samples.txt with default settings. + .EXAMPLE .\Build-Samples -Samples 'audio.acx.samples.audiocodec.driver','usb.kmdf_fx2' -Configurations 'Debug' -Platforms 'x64' + Builds specific samples for a single configuration and platform. + .EXAMPLE .\Build-Samples -SampleListPath .\MySamples.txt -ThrottleLimit 8 + + Builds samples from a custom list with limited parallelism. #> +#Requires -Version 7.0 + [CmdletBinding()] param( [string]$SampleListPath = (Join-Path (Get-Location) "Samples.txt"), @@ -58,350 +76,518 @@ param( [int]$ThrottleLimit = 0 ) -$root = (Get-Location).Path +# ============================================================================= +# Helper Functions +# ============================================================================= -# Launch developer PowerShell if necessary -if (-not $env:VSCMD_VER) { - Import-Module (Resolve-Path "$env:ProgramFiles\Microsoft Visual Studio\*\*\Common7\Tools\Microsoft.VisualStudio.DevShell.dll") - Enter-VsDevShell -VsInstallPath (Resolve-Path "$env:ProgramFiles\Microsoft Visual Studio\*\*") - Set-Location $root -} +function Initialize-DevShell { + <# + .SYNOPSIS Imports the Visual Studio Developer PowerShell if not already active. + #> + param([string]$ReturnToDirectory) -$ThrottleFactor = 5 -$LogicalProcessors = (Get-CIMInstance -Class 'CIM_Processor' -Verbose:$false).NumberOfLogicalProcessors + if ($env:VSCMD_VER) { + Write-Verbose "VS Developer Shell already active (VSCMD_VER=$env:VSCMD_VER)." + return + } -if ($ThrottleLimit -eq 0) { - $ThrottleLimit = $ThrottleFactor * $LogicalProcessors + $devShellDll = Resolve-Path "$env:ProgramFiles\Microsoft Visual Studio\*\*\Common7\Tools\Microsoft.VisualStudio.DevShell.dll" ` + -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $devShellDll) { + Write-Error "Visual Studio Developer Shell module not found." + exit 1 + } + + Import-Module $devShellDll.Path + $vsInstall = Resolve-Path "$env:ProgramFiles\Microsoft Visual Studio\*\*" | Select-Object -First 1 + Enter-VsDevShell -VsInstallPath $vsInstall.Path + Set-Location $ReturnToDirectory } -$Verbose = $false -if ($PSBoundParameters.ContainsKey('Verbose')) { - $Verbose = $PsBoundParameters.Get_Item('Verbose') +function Assert-MsBuildAvailable { + <# + .SYNOPSIS Verifies msbuild.exe is on PATH. Exits with error if not found. + #> + $savedPref = $ErrorActionPreference + $ErrorActionPreference = 'Stop' + try { + Get-Command 'msbuild' | Out-Null + } + catch { + Write-Error "msbuild cannot be called from current environment. Ensure it is on PATH (run from VS Developer Command Prompt or EWDK)." + exit 1 + } + finally { + $ErrorActionPreference = $savedPref + } } -# Prepare log directory -Remove-Item -Recurse -Path $LogFilesDirectory 2>&1 | Out-Null -New-Item -ItemType Directory -Force -Path $LogFilesDirectory | Out-Null +function Resolve-BuildEnvironment { + <# + .SYNOPSIS + Detects the active build environment and returns metadata. + .DESCRIPTION + Checks (in priority order) for GitHub CI, local NuGet packages, EWDK, or WDK. + Returns a hashtable: Name, BuildNumber (int), NuGetVersion, WdkVsComponentVersion. + #> + param([string]$RepoRoot) + + $result = @{ + Name = '' + BuildNumber = [int]0 + NuGetVersion = '' + WdkVsComponentVersion = '' + } -$reportFilePath = Join-Path $LogFilesDirectory "$ReportFileName.htm" -$reportCsvFilePath = Join-Path $LogFilesDirectory "$ReportFileName.csv" + if ($env:GITHUB_REPOSITORY) { + $result.Name = 'GitHub' + $wdkPackage = Get-ChildItem "$RepoRoot\packages\*WDK.x64*" -Name -ErrorAction SilentlyContinue + $result.NuGetVersion = ([regex]'(?<=x64\.)(\d+\.){3}\d+').Match($wdkPackage).Value + $result.BuildNumber = [int]($result.NuGetVersion.Split('.')[2]) + } + elseif (Test-Path "$RepoRoot\packages\*") { + $result.Name = 'NuGet' + $wdkPackage = Get-ChildItem "$RepoRoot\packages\*WDK.x64*" -Name -ErrorAction SilentlyContinue + $result.NuGetVersion = ([regex]'(?<=x64\.)(\d+\.){3}\d+').Match($wdkPackage).Value + $result.BuildNumber = [int]($result.NuGetVersion.Split('.')[2]) + } + elseif ($env:BuildLab -match '^(?[^.]+)\.(?\d+)\.(?[^.]+)$') { + $result.Name = "EWDK.$($Matches.branch).$($Matches.build).$($Matches.qfe)" + $result.BuildNumber = [int]$Matches.build + } + elseif ($env:UCRTVersion -match '10\.0\.(?\d+)\.0') { + $result.Name = 'WDK' + $result.BuildNumber = [int]$Matches.build + } + else { + Write-Output "Environment variables {" + Get-ChildItem env:* | Sort-Object Name + Write-Output "Environment variables }" + Write-Error "Could not determine build environment. Ensure EWDK, WDK, or NuGet packages are configured." + exit 1 + } -# Verify msbuild is available -$oldPreference = $ErrorActionPreference -$ErrorActionPreference = "stop" -try { - Get-Command "msbuild" | Out-Null + # WDK VS component version (EWDK does not ship this metadata) + if ($result.Name -match '^EWDK') { + $result.WdkVsComponentVersion = '(not available for EWDK builds)' + } + else { + $vsComponent = Get-ChildItem "${env:ProgramData}\Microsoft\VisualStudio\Packages\Microsoft.Windows.DriverKit,version=*" -ErrorAction SilentlyContinue + if (-not $vsComponent) { + Write-Error "WDK Visual Studio Component not found. Ensure the WDK Component is installed." + exit 1 + } + $result.WdkVsComponentVersion = [regex]::Match($vsComponent.Name, '(\d+\.){3}\d+').Value + } + + return $result } -catch { - Write-Host "`u{274C} msbuild cannot be called from current environment. Check that msbuild is set in current path (for example, that it is called from a Visual Studio developer command)." - Write-Error "msbuild cannot be called from current environment." - exit 1 + +function Import-SampleExclusions { + <# + .SYNOPSIS + Loads exclusions.csv and returns exclusion objects applicable to the current build. + .DESCRIPTION + Each returned exclusion has: + - Pattern: dot-separated path (may contain wildcards, e.g. 'general.*') + - Configurations: semicolon-separated config|platform patterns (or '*' for all) + - Reason: human-readable explanation + + Only exclusions whose [MinBuild, MaxBuild] range includes the given build number + are returned. Exclusions outside the range are silently skipped. + .NOTES + CSV format: Path,Configurations,MinBuild,MaxBuild,Reason + Example row: network\wlan\wdi,*,,27100,"failure introduced in VS17.14" + #> + param( + [string]$CsvPath, + [int]$BuildNumber + ) + + if (-not (Test-Path $CsvPath)) { + Write-Warning "Exclusions file not found: $CsvPath. No exclusions will be applied." + return @() + } + + $exclusions = [System.Collections.ArrayList]::new() + Import-Csv $CsvPath | ForEach-Object { + $pattern = $_.Path.Trim('\').Replace('\', '.').ToLower() + $configs = if ([string]::IsNullOrWhiteSpace($_.Configurations)) { '*' } else { $_.Configurations } + $minBuild = if ([string]::IsNullOrWhiteSpace($_.MinBuild)) { 0 } else { [int]$_.MinBuild } + $maxBuild = if ([string]::IsNullOrWhiteSpace($_.MaxBuild)) { 99999 } else { [int]$_.MaxBuild } + + if ($minBuild -le $BuildNumber -and $BuildNumber -le $maxBuild) { + [void]$exclusions.Add([PSCustomObject]@{ + Pattern = $pattern + Configurations = $configs + Reason = $_.Reason + }) + Write-Verbose "Exclusion applied: '$pattern' configs='$configs' reason='$($_.Reason)'" + } + else { + Write-Verbose "Exclusion skipped: '$pattern' - build $BuildNumber outside [$minBuild, $maxBuild]" + } + } + + return $exclusions.ToArray() +} + +function Get-DiskFreeGB { + <# + .SYNOPSIS Returns free disk space in GB for the current drive, or 'N/A' on error. + #> + try { + return [math]::Round((Get-Volume (Get-Item '.').PSDrive.Name).SizeRemaining / 1GB, 1) + } + catch { + return 'N/A' + } } -finally { - $ErrorActionPreference = $oldPreference + +# ============================================================================= +# Step 1 - Prepare Build Environment +# ============================================================================= + +$root = (Get-Location).Path + +Initialize-DevShell -ReturnToDirectory $root +Assert-MsBuildAvailable + +# ============================================================================= +# Step 2 - Calculate Parallelism +# ============================================================================= + +$throttleFactor = 5 +# Sum across all CPU sockets (Get-CimInstance returns an array on multi-socket systems) +$logicalProcessors = ((Get-CimInstance -Class CIM_Processor -Verbose:$false).NumberOfLogicalProcessors | Measure-Object -Sum).Sum + +if ($ThrottleLimit -eq 0) { + $ThrottleLimit = $throttleFactor * $logicalProcessors } -# -# Load sample list -# +$verbose = $PSBoundParameters.ContainsKey('Verbose') -and $PSBoundParameters['Verbose'] + +# ============================================================================= +# Step 3 - Prepare Log Directory +# ============================================================================= + +Remove-Item -Recurse -Path $LogFilesDirectory -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $LogFilesDirectory | Out-Null + +$reportHtmlPath = Join-Path $LogFilesDirectory "$ReportFileName.htm" +$reportCsvPath = Join-Path $LogFilesDirectory "$ReportFileName.csv" + +# ============================================================================= +# Step 4 - Load Sample List +# ============================================================================= + if ($Samples) { $sampleNames = $Samples } else { if (-not (Test-Path $SampleListPath)) { - Write-Error "Sample list file not found: $SampleListPath. Run .\ListAllSamples.ps1 first to generate Samples.txt." + Write-Error "Sample list not found: $SampleListPath. Run .\ListAllSamples.ps1 first." exit 1 } $sampleNames = Get-Content $SampleListPath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } } -# Build the sampleSet hashtable: sampleName -> directory path -$sampleSet = @{} +# Map sample names to directory paths, validating each exists +$sampleSet = [ordered]@{} +$skippedCount = 0 foreach ($name in $sampleNames) { - $relativePath = $name.Replace('.', '\') - $fullPath = Join-Path $root $relativePath - $sampleSet[$name] = $fullPath + $fullPath = Join-Path $root ($name.Replace('.', '\')) + if (Test-Path $fullPath -PathType Container) { + $sampleSet[$name] = $fullPath + } + else { + Write-Warning "Sample directory not found, skipping: $name" + $skippedCount++ + } } -# -# Determine build environment -# -$build_environment = "" -$build_number = 0 -$nuget_package_version = 0 - -if ($env:GITHUB_REPOSITORY) { - $build_environment = "GitHub" - $nuget_package_version = ([regex]'(?<=x64\.)(\d+\.)(\d+\.)(\d+\.)(\d+)').Matches((Get-ChildItem .\packages\*WDK.x64* -Name)).Value - $build_number = $nuget_package_version.split('.')[2] -} -elseif (Test-Path(".\packages\*")) { - $build_environment = "NuGet" - $nuget_package_version = ([regex]'(?<=x64\.)(\d+\.)(\d+\.)(\d+\.)(\d+)').Matches((Get-ChildItem .\packages\*WDK.x64* -Name)).Value - $build_number = $nuget_package_version.split('.')[2] -} -elseif ($env:BuildLab -match '(?[^.]*).(?[^.]*).(?[^.]*)') { - $build_environment = "EWDK." + $Matches.branch + "." + $Matches.build + "." + $Matches.qfe - $build_number = $Matches.build -} -elseif ($env:UCRTVersion -match '10.0.(?.*).0') { - $build_environment = "WDK" - $build_number = $Matches.build -} -else { - Write-Output "Environment variables {" - Get-ChildItem env:* | Sort-Object name - Write-Output "Environment variables }" - Write-Error "Could not determine build environment." +if ($sampleSet.Count -eq 0) { + Write-Error "No valid sample directories found. Verify Samples.txt and current directory." exit 1 } -# Determine WDK Visual Studio Component version -if ($build_environment -match '^EWDK') { - $wdk_vs_component_ver = "(WDK Visual Studio Component Version is not included for EWDK builds)" -} -else { - $wdk_vs_component_ver = Get-ChildItem "${env:ProgramData}\Microsoft\VisualStudio\Packages\Microsoft.Windows.DriverKit,version=*" -ErrorAction SilentlyContinue - if (-not $wdk_vs_component_ver) { - Write-Error "WDK Visual Studio Component version not found. Please ensure the WDK Component is installed." - exit 1 - } - $wdk_vs_component_ver = [regex]::Match($wdk_vs_component_ver.Name, '(\d+\.){3}\d+').Value -} +# ============================================================================= +# Step 5 - Detect Build Environment +# ============================================================================= + +$buildEnv = Resolve-BuildEnvironment -RepoRoot $root +$buildNumber = $buildEnv.BuildNumber +# ============================================================================= +# Step 6 - Determine InfVerif Options +# ============================================================================= # -# InfVerif_AdditionalOptions +# Samples must build cleanly, but certain InfVerif warnings are acceptable because +# they flag issues intentionally present in samples (to be fixed when productizing). +# <= 22621: suppress individual warnings /sw1284 /sw1285 /sw1293 /sw2083 /sw2086 +# > 22621: these are grouped under /samples # if ($InfOptions) { - $InfVerif_AdditionalOptions = $InfOptions + $infVerifOptions = $InfOptions } else { - $InfVerif_AdditionalOptions = ($build_number -le 22621 ? "/sw1284 /sw1285 /sw1293 /sw2083 /sw2086" : "/samples") + $infVerifOptions = if ($buildNumber -le 22621) { '/sw1284 /sw1285 /sw1293 /sw2083 /sw2086' } else { '/samples' } } -# -# Load exclusions from exclusions.csv -# -$exclusionConfigurations = @{} -$exclusionReasons = @{} -Import-Csv 'exclusions.csv' | ForEach-Object { - $excluded_driver = $_.Path.Replace($root, '').Trim('\').Replace('\', '.').ToLower() - $excluded_configurations = ($_.configurations -eq '' ? '*' : $_.configurations) - $excluded_minbuild = ($_.MinBuild -eq '' ? 00000 : $_.MinBuild) - $excluded_maxbuild = ($_.MaxBuild -eq '' ? 99999 : $_.MaxBuild) - if (($excluded_minbuild -le $build_number) -and ($build_number -le $excluded_maxbuild)) { - $exclusionConfigurations[$excluded_driver] = $excluded_configurations - $exclusionReasons[$excluded_driver] = $_.Reason - Write-Verbose "Exclusion.csv entry applied for '$excluded_driver' for configuration '$excluded_configurations'." - } - else { - Write-Verbose "Exclusion.csv entry not applied for '$excluded_driver' due to build number." - } -} +# ============================================================================= +# Step 7 - Load Exclusions +# ============================================================================= -# -# Build all samples -# -$jresult = @{ - SolutionsBuilt = 0 - SolutionsSucceeded = 0 - SolutionsExcluded = 0 - SolutionsUnsupported = 0 - SolutionsFailed = 0 - SolutionsSporadic = 0 - Results = @() - FailSet = @() - SporadicSet = @() - lock = [System.Threading.Mutex]::new($false) -} +$exclusions = Import-SampleExclusions -CsvPath (Join-Path $root 'exclusions.csv') -BuildNumber $buildNumber -$SolutionsTotal = $sampleSet.Count * $Configurations.Count * $Platforms.Count +# ============================================================================= +# Step 8 - Print Build Plan +# ============================================================================= -Write-Output "WDK Build Environment: $build_environment" -Write-Output "WDK Build Number: $build_number" -if (($build_environment -eq "GitHub") -or ($build_environment -eq "NuGet")) { - Write-Output "WDK Nuget Version: $nuget_package_version" +$combinationsTotal = $sampleSet.Count * $Configurations.Count * $Platforms.Count + +Write-Output "" +Write-Output "--- WDK Sample Build Plan ------------------------------------------" +Write-Output " Environment: $($buildEnv.Name)" +Write-Output " Build Number: $buildNumber" +if ($buildEnv.NuGetVersion) { + Write-Output " NuGet Version: $($buildEnv.NuGetVersion)" } -Write-Output "WDK Visual Studio Component Version: $wdk_vs_component_ver" -Write-Output "Samples: $($sampleSet.Count)" -Write-Output "Configurations: $($Configurations.Count) ($Configurations)" -Write-Output "Platforms: $($Platforms.Count) ($Platforms)" -Write-Output "InfVerif_AdditionalOptions: $InfVerif_AdditionalOptions" -Write-Output "Combinations: $SolutionsTotal" -Write-Output "LogicalProcessors: $LogicalProcessors" -Write-Output "ThrottleFactor: $ThrottleFactor" -Write-Output "ThrottleLimit: $ThrottleLimit" -Write-Output "WDS_WipeOutputs: $env:WDS_WipeOutputs" -Write-Output "Disk Remaining (GB): $(((Get-Volume ((Get-Item ".").PSDrive.Name)).SizeRemaining) / 1GB)" +Write-Output " WDK VS Component: $($buildEnv.WdkVsComponentVersion)" +Write-Output " InfVerif Options: $infVerifOptions" Write-Output "" -Write-Output "T: Combinations" -Write-Output "B: Built" -Write-Output "R: Build is running currently" -Write-Output "P: Build is pending an available build slot" +Write-Output " Samples: $($sampleSet.Count) ($skippedCount skipped)" +Write-Output " Configurations: $($Configurations -join ', ')" +Write-Output " Platforms: $($Platforms -join ', ')" +Write-Output " Combinations: $combinationsTotal" +Write-Output " Exclusions: $($exclusions.Count)" Write-Output "" -Write-Output "S: Built and result was 'Succeeded'" -Write-Output "E: Built and result was 'Excluded'" -Write-Output "U: Built and result was 'Unsupported' (Platform and Configuration combination)" -Write-Output "F: Built and result was 'Failed'" -Write-Output "O: Built and result was 'Sporadic'" +Write-Output " Parallelism: $ThrottleLimit jobs ($logicalProcessors cores x $throttleFactor)" +Write-Output " Disk Free (GB): $(Get-DiskFreeGB)" +Write-Output " Wipe Outputs: $(-not [string]::IsNullOrEmpty($env:WDS_WipeOutputs))" +Write-Output "--------------------------------------------------------------------" +Write-Output "" +Write-Output "Progress legend:" +Write-Output " T=Total B=Built R=Running P=Pending" +Write-Output " S=Succeeded E=Excluded U=Unsupported F=Failed O=Sporadic" Write-Output "" Write-Output "Building all combinations..." -$sw = [Diagnostics.Stopwatch]::StartNew() +# ============================================================================= +# Step 9 - Execute Parallel Builds +# ============================================================================= + +# Shared mutable state protected by a Mutex. This is required because +# ForEach-Object -Parallel runs each iteration in a separate runspace, +# so standard .NET locks (Monitor/lock) do not work across runspaces. +$buildState = @{ + Built = 0 + Succeeded = 0 + Excluded = 0 + Unsupported = 0 + Failed = 0 + Sporadic = 0 + Results = @() + FailSet = @() + SporadicSet = @() + Lock = [System.Threading.Mutex]::new($false) +} + +$stopwatch = [Diagnostics.Stopwatch]::StartNew() $sampleSet.GetEnumerator() | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel { - $LogFilesDirectory = $using:LogFilesDirectory - $exclusionConfigurations = $using:exclusionConfigurations - $exclusionReasons = $using:exclusionReasons - $Configurations = $using:Configurations - $Platforms = $using:Platforms - $InfVerif_AdditionalOptions = $using:InfVerif_AdditionalOptions - $Verbose = $using:Verbose + # --- Import shared state from parent scope --- + $logDir = $using:LogFilesDirectory + $exclusions = $using:exclusions + $configs = $using:Configurations + $platforms = $using:Platforms + $infOpts = $using:infVerifOptions + $isVerbose = $using:verbose + $state = $using:buildState + $total = $using:combinationsTotal + $throttle = $using:ThrottleLimit $sampleName = $_.Key - $directory = $_.Value - - $ResultElement = New-Object psobject - Add-Member -InputObject $ResultElement -MemberType NoteProperty -Name Sample -Value "$sampleName" - - foreach ($configuration in $Configurations) { - foreach ($platform in $Platforms) { - $thisunsupported = 0 - $thisfailed = 0 - $thissporadic = 0 - $thisexcluded = 0 - $thissucceeded = 0 - $thisresult = "Not run" - $thisfailset = @() - $thissporadicset = @() - - if ($exclusionConfigurations.ContainsKey($sampleName) -and ($exclusionConfigurations[$sampleName].Split(';') | Where-Object { "$configuration|$platform" -like $_ })) { - Write-Verbose "[$sampleName $configuration|$platform] `u{23E9} Excluded and skipped. Reason: $($exclusionReasons[$sampleName])" - $thisexcluded += 1 - $thisresult = "Excluded" + $directory = $_.Value + + # Build a result row: one column per configuration|platform combination + $resultRow = [PSCustomObject]@{ Sample = $sampleName } + + foreach ($configuration in $configs) { + foreach ($platform in $platforms) { + $result = 'Not run' + $succeededDelta = 0 + $excludedDelta = 0 + $unsupportedDelta = 0 + $failedDelta = 0 + $sporadicDelta = 0 + $failEntry = $null + $sporadicEntry = $null + + # -- Check exclusions (supports wildcard paths like 'general.*') -- + $exclusionReason = $null + foreach ($excl in $exclusions) { + if ($sampleName -like $excl.Pattern) { + $configKey = "$configuration|$platform" + foreach ($cfgPattern in $excl.Configurations.Split(';')) { + if ($configKey -like $cfgPattern) { + $exclusionReason = $excl.Reason + break + } + } + if ($exclusionReason) { break } + } + } + + if ($exclusionReason) { + Write-Verbose "[$sampleName $configuration|$platform] Excluded: $exclusionReason" + $excludedDelta = 1 + $result = 'Excluded' } else { - .\Build-Sample -Directory $directory -SampleName $sampleName -LogFilesDirectory $LogFilesDirectory -Configuration $configuration -Platform $platform -InfVerif_AdditionalOptions $InfVerif_AdditionalOptions -Verbose:$Verbose - if ($LASTEXITCODE -eq 0) { - $thissucceeded += 1 - $thisresult = "Succeeded" - } - elseif ($LASTEXITCODE -eq 1) { - $thisfailset += "$sampleName $configuration|$platform" - $thisfailed += 1 - $thisresult = "Failed" - } - elseif ($LASTEXITCODE -eq 2) { - $thissporadicset += "$sampleName $configuration|$platform" - $thissporadic += 1 - $thisresult = "Sporadic" - } - else { - $thisunsupported += 1 - $thisresult = "Unsupported" + # -- Build the sample -- + .\Build-Sample -Directory $directory -SampleName $sampleName ` + -LogFilesDirectory $logDir -Configuration $configuration ` + -Platform $platform -InfVerif_AdditionalOptions $infOpts ` + -Verbose:$isVerbose + + # Exit codes from Build-Sample.ps1: + # 0 = succeeded on first attempt + # 1 = failed after all retries + # 2 = sporadic (failed first, succeeded on retry) + # 3 = unsupported configuration/platform + switch ($LASTEXITCODE) { + 0 { $succeededDelta = 1; $result = 'Succeeded' } + 1 { $failedDelta = 1; $result = 'Failed'; $failEntry = "$sampleName $configuration|$platform" } + 2 { $sporadicDelta = 1; $result = 'Sporadic'; $sporadicEntry = "$sampleName $configuration|$platform" } + default { $unsupportedDelta = 1; $result = 'Unsupported' } } } - Add-Member -InputObject $ResultElement -MemberType NoteProperty -Name "$configuration|$platform" -Value "$thisresult" - $null = ($using:jresult).lock.WaitOne() + $resultRow | Add-Member -MemberType NoteProperty -Name "$configuration|$platform" -Value $result + + # -- Update shared counters (under lock) -- + $null = $state.Lock.WaitOne() try { - ($using:jresult).SolutionsBuilt += 1 - ($using:jresult).SolutionsSucceeded += $thissucceeded - ($using:jresult).SolutionsExcluded += $thisexcluded - ($using:jresult).SolutionsUnsupported += $thisunsupported - ($using:jresult).SolutionsFailed += $thisfailed - ($using:jresult).SolutionsSporadic += $thissporadic - ($using:jresult).FailSet += $thisfailset - ($using:jresult).SporadicSet += $thissporadicset - $SolutionsTotal = $using:SolutionsTotal - $ThrottleLimit = $using:ThrottleLimit - $SolutionsBuilt = ($using:jresult).SolutionsBuilt - $SolutionsRemaining = $SolutionsTotal - $SolutionsBuilt - $SolutionsRunning = if ($SolutionsRemaining -ge $ThrottleLimit) { $ThrottleLimit } else { $SolutionsRemaining } - $SolutionsPending = if ($SolutionsRemaining -ge $ThrottleLimit) { ($SolutionsRemaining - $ThrottleLimit) } else { 0 } - $SolutionsBuiltPercent = [Math]::Round(100 * ($SolutionsBuilt / $using:SolutionsTotal)) - $TBRP = "T:" + ($SolutionsTotal) + "; B:" + (($using:jresult).SolutionsBuilt) + "; R:" + ($SolutionsRunning) + "; P:" + ($SolutionsPending) - $rstr = "S:" + (($using:jresult).SolutionsSucceeded) + "; E:" + (($using:jresult).SolutionsExcluded) + "; U:" + (($using:jresult).SolutionsUnsupported) + "; F:" + (($using:jresult).SolutionsFailed) + "; O:" + (($using:jresult).SolutionsSporadic) - Write-Progress -Activity "Building combinations" -Status "$SolutionsBuilt of $using:SolutionsTotal combinations built ($SolutionsBuiltPercent%) | $TBRP | $rstr" -PercentComplete $SolutionsBuiltPercent + $state.Built += 1 + $state.Succeeded += $succeededDelta + $state.Excluded += $excludedDelta + $state.Unsupported += $unsupportedDelta + $state.Failed += $failedDelta + $state.Sporadic += $sporadicDelta + if ($failEntry) { $state.FailSet += $failEntry } + if ($sporadicEntry) { $state.SporadicSet += $sporadicEntry } + + # Update progress bar + $built = $state.Built + $remaining = $total - $built + $running = [Math]::Min($remaining, $throttle) + $pending = [Math]::Max($remaining - $throttle, 0) + $pct = [Math]::Round(100 * $built / $total) + + $statusLine = "$built of $total combinations built ($pct%) | " + + "T:$total; B:$built; R:$running; P:$pending | " + + "S:$($state.Succeeded); E:$($state.Excluded); U:$($state.Unsupported); F:$($state.Failed); O:$($state.Sporadic)" + + # Write-Host with carriage return for a single-line progress indicator. + # Write-Progress does not reliably render from -Parallel runspaces. + Write-Host "`rBuilding combinations [$statusLine]" -NoNewline } finally { - ($using:jresult).lock.ReleaseMutex() + $state.Lock.ReleaseMutex() } } } - $null = ($using:jresult).lock.WaitOne() + + # Append the completed result row + $null = $state.Lock.WaitOne() try { - ($using:jresult).Results += $ResultElement + $state.Results += $resultRow } finally { - ($using:jresult).lock.ReleaseMutex() + $state.Lock.ReleaseMutex() } } -$sw.Stop() +$stopwatch.Stop() + +# End the progress line +Write-Host "" + +# ============================================================================= +# Step 10 - Report Failures +# ============================================================================= Write-Output "" -if ($jresult.FailSet.Count -gt 0) { - Write-Output "Some combinations were built with errors:" - $jresult.FailSet = $jresult.FailSet | Sort-Object - foreach ($failedSample in $jresult.FailSet) { - $failedSample -match "^(.*) (\w*)\|(\w*)$" | Out-Null - $failName = $Matches[1] - $failConfiguration = $Matches[2] - $failPlatform = $Matches[3] - Write-Output "Build errors in Sample $failName; Configuration: $failConfiguration; Platform: $failPlatform {" - Get-Content "$LogFilesDirectory\$failName.$failConfiguration.$failPlatform.0.err" | Write-Output - Write-Output "} $failedSample" +if ($buildState.FailSet.Count -gt 0) { + Write-Output "--- Build Failures -------------------------------------------------" + foreach ($entry in ($buildState.FailSet | Sort-Object)) { + if ($entry -match '^(?.*)\s+(?\w+)\|(?\w+)$') { + $errLog = Join-Path $LogFilesDirectory "$($Matches.name).$($Matches.config).$($Matches.platform).0.err" + Write-Output " [FAIL] $($Matches.name) ($($Matches.config)|$($Matches.platform)):" + if (Test-Path $errLog) { + Get-Content $errLog | ForEach-Object { Write-Output " $_" } + } + else { + Write-Output " (error log not found: $errLog)" + } + } } - Write-Error "Some combinations were built with errors." Write-Output "" + Write-Error "Some combinations were built with errors." } -if ($jresult.SporadicSet.Count -gt 0) { - Write-Output "Some combinations were built with sporadic error:" - $jresult.SporadicSet = $jresult.SporadicSet | Sort-Object - foreach ($sporadicSample in $jresult.SporadicSet) { - $sporadicSample -match "^(.*) (\w*)\|(\w*)$" | Out-Null - $sporadicName = $Matches[1] - $sporadicConfiguration = $Matches[2] - $sporadicPlatform = $Matches[3] - Write-Output "Build sporadic errors in Sample $sporadicName; Configuration: $sporadicConfiguration; Platform: $sporadicPlatform {" - Get-Content "$LogFilesDirectory\$sporadicName.$sporadicConfiguration.$sporadicPlatform.0.err" | Write-Output - Write-Output "} $sporadicSample" +if ($buildState.SporadicSet.Count -gt 0) { + Write-Output "--- Sporadic Failures (succeeded on retry) -------------------------" + foreach ($entry in ($buildState.SporadicSet | Sort-Object)) { + if ($entry -match '^(?.*)\s+(?\w+)\|(?\w+)$') { + $errLog = Join-Path $LogFilesDirectory "$($Matches.name).$($Matches.config).$($Matches.platform).0.err" + Write-Output " [SPORADIC] $($Matches.name) ($($Matches.config)|$($Matches.platform)):" + if (Test-Path $errLog) { + Get-Content $errLog | ForEach-Object { Write-Output " $_" } + } + } } - Write-Error "Some combinations were built with sporadic errors." Write-Output "" + Write-Error "Some combinations had sporadic build failures." } -# Display timer statistics -$min = $sw.Elapsed.Minutes -$seconds = $sw.Elapsed.Seconds +# ============================================================================= +# Step 11 - Final Summary +# ============================================================================= -$SolutionsSucceeded = $jresult.SolutionsSucceeded -$SolutionsExcluded = $jresult.SolutionsExcluded -$SolutionsUnsupported = $jresult.SolutionsUnsupported -$SolutionsFailed = $jresult.SolutionsFailed -$SolutionsSporadic = $jresult.SolutionsSporadic -$Results = $jresult.Results +$elapsed = $stopwatch.Elapsed -Write-Output "Built all combinations." +Write-Output "--- Build Complete -------------------------------------------------" +Write-Output " Elapsed: $($elapsed.Minutes)m $($elapsed.Seconds)s" +Write-Output " Disk Free (GB): $(Get-DiskFreeGB)" Write-Output "" -Write-Output "Elapsed time: $min minutes, $seconds seconds." -Write-Output ("Disk Remaining (GB): " + (((Get-Volume (Get-Item ".").PSDrive.Name).SizeRemaining / 1GB))) -Write-Output ("Samples: " + $sampleSet.Count) -Write-Output ("Configurations: " + $Configurations.Count + " (" + $Configurations + ")") -Write-Output ("Platforms: " + $Platforms.Count + " (" + $Platforms + ")") -Write-Output "Combinations: $SolutionsTotal" -Write-Output "Succeeded: $SolutionsSucceeded" -Write-Output "Excluded: $SolutionsExcluded" -Write-Output "Unsupported: $SolutionsUnsupported" -Write-Output "Failed: $SolutionsFailed" -Write-Output "Sporadic: $SolutionsSporadic" -Write-Output "Log files directory: $LogFilesDirectory" -Write-Output "Overview report: $reportFilePath" +Write-Output " Samples: $($sampleSet.Count)" +Write-Output " Configurations: $($Configurations -join ', ')" +Write-Output " Platforms: $($Platforms -join ', ')" +Write-Output " Combinations: $combinationsTotal" Write-Output "" - -$Results | Sort-Object { $_.Sample } | ConvertTo-Csv | Out-File $reportCsvFilePath -$Results | Sort-Object { $_.Sample } | ConvertTo-Html -Title "Overview" | Out-File $reportFilePath -Invoke-Item $reportFilePath +Write-Output " Succeeded: $($buildState.Succeeded)" +Write-Output " Excluded: $($buildState.Excluded)" +Write-Output " Unsupported: $($buildState.Unsupported)" +Write-Output " Failed: $($buildState.Failed)" +Write-Output " Sporadic: $($buildState.Sporadic)" +Write-Output "" +Write-Output " Log directory: $LogFilesDirectory" +Write-Output " CSV report: $reportCsvPath" +Write-Output " HTML report: $reportHtmlPath" +Write-Output "--------------------------------------------------------------------" + +# ============================================================================= +# Step 12 - Generate Reports +# ============================================================================= + +$sortedResults = $buildState.Results | Sort-Object { $_.Sample } +$sortedResults | ConvertTo-Csv | Out-File $reportCsvPath +$sortedResults | ConvertTo-Html -Title "WDK Sample Build Overview" | Out-File $reportHtmlPath + +# Only open the HTML report interactively (not in CI/automation) +if (-not $env:GITHUB_REPOSITORY -and -not $env:BUILD_BUILDID -and [Environment]::UserInteractive) { + Invoke-Item $reportHtmlPath +} diff --git a/ListAllSamples.ps1 b/ListAllSamples.ps1 index ed87fc83..f533c080 100644 --- a/ListAllSamples.ps1 +++ b/ListAllSamples.ps1 @@ -4,7 +4,7 @@ .DESCRIPTION Searches for all .sln files recursively from the repo root, excludes NuGet package directories - (paths starting with 'packages.'), computes a normalized sample name for each, and writes + (paths containing 'packages' as a segment), computes a normalized sample name for each, and writes the sorted list to Samples.txt (one sample name per line). The sample name is derived from the relative directory path: backslashes are replaced with dots diff --git a/Samples.txt b/Samples.txt index 7998f395..571b6b4c 100644 --- a/Samples.txt +++ b/Samples.txt @@ -1,4 +1,4 @@ -audio.acx.samples.audiocodec.driver +audio.acx.samples.audiocodec.driver audio.simpleaudiosample audio.soundwire.samples.sdcavad audio.sysvad @@ -61,8 +61,8 @@ network.modem.fakemodem network.ndis.extension network.ndis.filter network.ndis.mux -network.ndis.ndisprot.6x network.ndis.ndisprot_kmdf +network.ndis.ndisprot.6x network.ndis.netvmini.6x network.radio.radiomanagersample network.trans.ddproxy -- cgit v1.3.1 From 302d18984142a22d5195105fe0347eb5b145d859 Mon Sep 17 00:00:00 2001 From: 5an7y Date: Fri, 27 Mar 2026 16:04:47 -0700 Subject: Remove Samples.txt: discover samples dynamically via ListAllSamples - ListAllSamples.ps1: output sorted sample names to stdout instead of writing Samples.txt. The file output is replaced with Write-Output so callers can capture results via pipeline or command substitution. - Build-Samples.ps1: remove SampleListPath parameter. Default discovery now calls ListAllSamples.ps1 at runtime and consumes its stdout. Explicit -Samples array still works and is sorted alphabetically. Both paths guarantee a consistent alphabetical sample ordering. - Samples.txt: deleted. No longer needed as a pre-generated artifact. WDS_TestPass.cs is unaffected - it only passes -LogFilesDirectory and -InfOptions, neither of which changed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Build-Samples.ps1 | 33 ++++++------- ListAllSamples.ps1 | 14 +++--- Samples.txt | 134 ----------------------------------------------------- 3 files changed, 21 insertions(+), 160 deletions(-) delete mode 100644 Samples.txt (limited to 'Build-Samples.ps1') diff --git a/Build-Samples.ps1 b/Build-Samples.ps1 index 6fd86bd3..aad2d3e5 100644 --- a/Build-Samples.ps1 +++ b/Build-Samples.ps1 @@ -6,7 +6,7 @@ This is the main build orchestrator for driver samples. It performs these steps: 1. Ensures a developer build environment (VS DevShell or EWDK) is active - 2. Reads sample names from Samples.txt (or a specified file/array) + 2. Discovers samples via ListAllSamples.ps1 (or uses the -Samples parameter if provided) 3. Detects the build environment (GitHub CI, NuGet, EWDK, or WDK) and build number 4. Loads exclusions from exclusions.csv (supports wildcard paths) 5. Builds all non-excluded sample/configuration/platform combinations in parallel @@ -15,15 +15,12 @@ Requires PowerShell 7+ (uses ForEach-Object -Parallel). Typical call chain: - Build-AllSamples.ps1 -> ListAllSamples.ps1 -> Build-Samples.ps1 -> Build-Sample.ps1 - (this script) + Build-AllSamples.ps1 -> Build-Samples.ps1 -> ListAllSamples.ps1 (for discovery) + (this script) -> Build-Sample.ps1 (per sample) -.PARAMETER SampleListPath - Path to the file containing sample names (one per line, dot-separated). - Defaults to Samples.txt in the current directory. .PARAMETER Samples - Optional array of specific sample names to build. When provided, overrides SampleListPath. + Optional array of specific sample names to build. When omitted, all samples are discovered dynamically via ListAllSamples.ps1. .PARAMETER Configurations Build configurations (e.g. 'Debug','Release'). Defaults to $env:WDS_Configuration or @@ -49,7 +46,7 @@ .EXAMPLE .\Build-Samples - Builds all samples from Samples.txt with default settings. + Discovers all samples via ListAllSamples.ps1 and builds them with default settings. .EXAMPLE .\Build-Samples -Samples 'audio.acx.samples.audiocodec.driver','usb.kmdf_fx2' -Configurations 'Debug' -Platforms 'x64' @@ -57,16 +54,15 @@ Builds specific samples for a single configuration and platform. .EXAMPLE - .\Build-Samples -SampleListPath .\MySamples.txt -ThrottleLimit 8 + .\Build-Samples -ThrottleLimit 8 - Builds samples from a custom list with limited parallelism. + Discovers all samples and builds them with limited parallelism. #> #Requires -Version 7.0 [CmdletBinding()] param( - [string]$SampleListPath = (Join-Path (Get-Location) "Samples.txt"), [string[]]$Samples, [string[]]$Configurations = @(if ([string]::IsNullOrEmpty($env:WDS_Configuration)) { ('Debug', 'Release') } else { $env:WDS_Configuration }), [string[]]$Platforms = @(if ([string]::IsNullOrEmpty($env:WDS_Platform)) { ('x64', 'arm64') } else { $env:WDS_Platform }), @@ -282,14 +278,15 @@ $reportCsvPath = Join-Path $LogFilesDirectory "$ReportFileName.csv" # ============================================================================= if ($Samples) { - $sampleNames = $Samples + # Explicit list passed by the caller — use as-is, sorted alphabetically. + $sampleNames = $Samples | Sort-Object } else { - if (-not (Test-Path $SampleListPath)) { - Write-Error "Sample list not found: $SampleListPath. Run .\ListAllSamples.ps1 first." - exit 1 - } - $sampleNames = Get-Content $SampleListPath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + # Default: discover samples dynamically via ListAllSamples.ps1. + # ListAllSamples outputs one alphabetically-sorted sample name per line to stdout. + Write-Verbose "No -Samples provided. Discovering samples via ListAllSamples.ps1..." + $sampleNames = & (Join-Path $PSScriptRoot 'ListAllSamples.ps1') -Verbose:$verbose | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } } # Map sample names to directory paths, validating each exists @@ -307,7 +304,7 @@ foreach ($name in $sampleNames) { } if ($sampleSet.Count -eq 0) { - Write-Error "No valid sample directories found. Verify Samples.txt and current directory." + Write-Error "No valid sample directories found. Ensure ListAllSamples.ps1 is available in the repo root." exit 1 } diff --git a/ListAllSamples.ps1 b/ListAllSamples.ps1 index f533c080..1a1a4a78 100644 --- a/ListAllSamples.ps1 +++ b/ListAllSamples.ps1 @@ -1,11 +1,11 @@ <# .SYNOPSIS - Enumerates all available sample solutions in the repository and writes them to Samples.txt. + Enumerates all available sample solutions in the repository and writes them to the console. .DESCRIPTION Searches for all .sln files recursively from the repo root, excludes NuGet package directories (paths containing 'packages' as a segment), computes a normalized sample name for each, and writes - the sorted list to Samples.txt (one sample name per line). + the sorted list to stdout (one sample name per line). The sample name is derived from the relative directory path: backslashes are replaced with dots and the result is lowercased. @@ -13,10 +13,10 @@ .EXAMPLE .\ListAllSamples - Discovers all samples and writes Samples.txt to the repo root. + Discovers all samples and writes the sorted names to the console. .OUTPUTS - Samples.txt in the current working directory. + Sorted sample names written to stdout, one per line. #> [CmdletBinding()] @@ -45,7 +45,5 @@ foreach ($file in $solutionFiles) { $sortedNames = $sampleNames.Keys | Sort-Object -$outputPath = Join-Path $root "Samples.txt" -$sortedNames | Out-File -FilePath $outputPath -Encoding utf8 - -Write-Output "Found $($sortedNames.Count) samples. Written to Samples.txt" +Write-Verbose "Found $($sortedNames.Count) samples." +$sortedNames | Write-Output diff --git a/Samples.txt b/Samples.txt deleted file mode 100644 index 571b6b4c..00000000 --- a/Samples.txt +++ /dev/null @@ -1,134 +0,0 @@ -audio.acx.samples.audiocodec.driver -audio.simpleaudiosample -audio.soundwire.samples.sdcavad -audio.sysvad -avstream.avscamera -avstream.avshws -avstream.avssamp -avstream.sampledevicemft -avstream.samplemft0 -bluetooth.bthecho -bluetooth.serialhcibus -filesys.cdfs -filesys.fastfat -filesys.minifilter.avscan -filesys.minifilter.cancelsafe -filesys.minifilter.cdo -filesys.minifilter.change -filesys.minifilter.ctx -filesys.minifilter.delete -filesys.minifilter.metadatamanager -filesys.minifilter.minispy -filesys.minifilter.namechanger -filesys.minifilter.nullfilter -filesys.minifilter.passthrough -filesys.minifilter.scanner -filesys.minifilter.simrep -filesys.minifilter.swapbuffers -general.cancel -general.dchu.osrfx2_dchu_base -general.dchu.osrfx2_dchu_extension_loose -general.dchu.osrfx2_dchu_extension_tight -general.echo.kmdf -general.echo.umdf2 -general.event -general.ioctl.kmdf -general.ioctl.wdm -general.obcallback -general.pcidrv -general.perfcounters.kcs -general.plx9x5x -general.registry.regfltr -general.simplemediasource -general.systemdma.wdm -general.toaster.toastdrv -general.toaster.toastpkg -general.toaster.umdf2 -general.tracing.evntdrv -general.tracing.systemtracecontrol -general.tracing.tracedriver -gnss -gpio.samples -hid.firefly -hid.hclient -hid.hidusbfx2 -hid.vhidmini2 -input.kbfiltr -input.layout -input.moufiltr -network.config.bindview -network.modem.fakemodem -network.ndis.extension -network.ndis.filter -network.ndis.mux -network.ndis.ndisprot_kmdf -network.ndis.ndisprot.6x -network.ndis.netvmini.6x -network.radio.radiomanagersample -network.trans.ddproxy -network.trans.inspect -network.trans.msnmntr -network.trans.stmedit -network.trans.wfpsampler -network.wlan.wdi -network.wsk.echosrv -network.wwan.cxwmbclass -nfc.nfccxsample -nfp.net -pofx.pep -pofx.umdf2 -pofx.wdf -pos.drivers.barcodescanner -pos.drivers.magneticstripereader -powerlimit.plclient -powerlimit.plpolicy -prm -sd.miniport.sdhc -security.elam -sensors.activity -sensors.adxl345acc -sensors.customsensors -sensors.fusion -sensors.pedometer -sensors.sensorscombodriver -sensors.simpledeviceorientationsensor -serial.serenum -serial.serial -serial.virtualserial2 -setup.devcon -simbatt.func -smartcrd -spb.skeletoni2c -spb.spbtesttool -storage.class.classpnp -storage.class.disk -storage.iscsi -storage.miniports.lsi_u3 -storage.miniports.storahci -storage.msdsm -storage.tools.spti -thermal.simsensor -thermal.thermalclient -tools.dv.samples.dv-faildriver-wdm.driver -tools.kasan.samples.kasandemo-wdm -tools.sdv.samples.sdv-faildriver-kmdf -tools.sdv.samples.sdv-faildriver-ndis -tools.sdv.samples.sdv-faildriver-storport -tools.sdv.samples.sdv-faildriver-wdm -tree -usb.kmdf_enumswitches -usb.kmdf_fx2 -usb.ucmcxucsi -usb.ucmtcpcicxclientsample -usb.ucmucsiacpisample -usb.ufxclientsample -usb.umdf2_fx2 -usb.usbsamp -usb.usbview -usb.wdf_osrfx2_lab -video.archive.pixlib -video.indirectdisplay -video.kmdod -wia -wmi.wmiacpi -wmi.wmisamp -- cgit v1.3.1 From bc6b2708d9d762dde716a2af7c8fa08534c1afcc Mon Sep 17 00:00:00 2001 From: 5an7y Date: Mon, 30 Mar 2026 15:32:35 -0700 Subject: Merge Build-Sample.ps1 into Build-Samples.ps1 as internal function - Inline Build-Sample.ps1 logic as Build-SingleSample function in Build-Samples.ps1 - Pass function definition into parallel runspaces via $using: pattern - Replace exit codes with return values for in-process execution - Delete Build-Sample.ps1 (no longer needed as a separate script) - Update Build-ChangedSamples.ps1 trigger file list --- .github/scripts/Build-ChangedSamples.ps1 | 2 +- Build-Sample.ps1 | 211 ------------------------------- Build-Samples.ps1 | 152 +++++++++++++++++++++- 3 files changed, 146 insertions(+), 219 deletions(-) delete mode 100644 Build-Sample.ps1 (limited to 'Build-Samples.ps1') diff --git a/.github/scripts/Build-ChangedSamples.ps1 b/.github/scripts/Build-ChangedSamples.ps1 index 29f99061..3e91eb88 100644 --- a/.github/scripts/Build-ChangedSamples.ps1 +++ b/.github/scripts/Build-ChangedSamples.ps1 @@ -22,7 +22,7 @@ foreach ($file in $ChangedFiles) { $filename = Split-Path $file -Leaf # Files that can affect how every sample is built should trigger a full build - if ($filename -eq "Build-Samples.ps1" -or $filename -eq "Build-Sample.ps1" -or $filename -eq "exclusions.csv" -or $filename -eq "Directory.Build.props" -or $filename -eq "packages.config") { + if ($filename -eq "Build-Samples.ps1" -or $filename -eq "exclusions.csv" -or $filename -eq "Directory.Build.props" -or $filename -eq "packages.config") { $buildAll = $true } if ($dir -like "$root\.github\scripts" -or $dir -like "$root\.github\scripts\*") { diff --git a/Build-Sample.ps1 b/Build-Sample.ps1 deleted file mode 100644 index c09b7694..00000000 --- a/Build-Sample.ps1 +++ /dev/null @@ -1,211 +0,0 @@ -<# -.SYNOPSIS -Builds an specific directory containing a sample solution. - -.DESCRIPTION -This script attempts to build a directory containing a driver sample Solution for the specified configurations and platforms. - -.PARAMETER Directory -Path to a directory containing a valid Visual Studio Solution (.sln file). This is the solution that will be built. - -.PARAMETER SampleName -A friendly name to refer to the sample. Is unspecified, a name will be automatically generated one from the sample path. - -.PARAMETER Configuration -Configuration name that will be used to build the solution. Common available values are "Debug" and "Release". - -.PARAMETER Platform -Platform to build the solution for (e.g. "x64", "arm64"). - -.PARAMETER InfVerif_AdditionalOptions -Additional options for infverif (e.g. "/samples"). - -.PARAMETER LogFilesDirectoy -Path to a directory where the log files will be written to. If not provided, outputs will be logged to the current working directory. - -.INPUTS -None. - -.OUTPUTS -Verbose output about the execution of this script will be provided only if -Verbose is provided. Otherwise, no output will be generated. - -.EXAMPLE -.\Build-Sample -Directory .\usb\kmdf_fx2 - -.EXAMPLE -.\Build-Sample -Directory .\usb\kmdf_fx2 -Configuration 'Release' -Platform 'x64' -Verbose -LogFilesDirectory .\_logs - -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory = $true, - HelpMessage = 'Enter one directory path', - Position = 0)] - [string]$Directory, - [string]$SampleName, - [string]$Configuration = "Debug", - [string]$Platform = "x64", - [string]$InfVerif_AdditionalOptions = "/samples", - $LogFilesDirectory = (Get-Location) -) - -$Verbose = $false -if ($PSBoundParameters.ContainsKey('Verbose')) { - $Verbose = $PsBoundParameters.Get_Item('Verbose') -} - -$oldPreference = $ErrorActionPreference -$ErrorActionPreference = "stop" -try -{ - # Check that msbuild can be called before trying anything. - Get-Command "msbuild" | Out-Null -} -catch -{ - Write-Verbose "`u{274C} msbuild cannot be called from current environment. Check that msbuild is set in current path (for example, that it is called from a Visual Studio developer command)." - Write-Error "msbuild cannot be called from current environment." - exit 1 -} -finally -{ - $ErrorActionPreference = $oldPreference -} - -if (-not (Test-Path -Path $Directory -PathType Container)) -{ - Write-Warning "`u{274C} A valid directory could not be found under $Directory" - exit 1 -} - -New-Item -ItemType Directory -Force -Path $LogFilesDirectory | Out-Null - -if (-not (Test-Path -Path $LogFilesDirectory -PathType Container)) -{ - Write-Warning "`u{274C} A valid directory for storing log files could not be created under $LogFilesDirectory" - # No exit here: process will continue but logs won't be available. -} - -if ([string]::IsNullOrWhitespace($SampleName)) -{ - $SampleName = (Resolve-Path $Directory).Path.Replace((Get-Location), '').Replace('\', '.').Trim('.').ToLower() -} - -$solutionFile = Get-ChildItem -Path $Directory -Filter *.sln | Select-Object -ExpandProperty FullName -First 1 - -if ($null -eq $solutionFile) -{ - Write-Warning "`u{274C} A solution could not be found under $Directory" - exit 1 -} - -$configurationIsSupported = $false -$inSolutionConfigurationPlatformsSection = $false -foreach ($line in Get-Content -Path $solutionFile) -{ - if (-not $inSolutionConfigurationPlatformsSection -and $line -match "\s*GlobalSection\(SolutionConfigurationPlatforms\).*") - { - $inSolutionConfigurationPlatformsSection = $true; - continue; - } - elseif ($line -match "\s*EndGlobalSection.*") - { - $inSolutionConfigurationPlatformsSection = $false; - continue; - } - - if ($inSolutionConfigurationPlatformsSection) - { - [regex]$regex = ".*=\s*(?(?.*)\|(?.*))\s*" - $match = $regex.Match($line) - if ([string]::IsNullOrWhiteSpace($match.Groups["ConfigString"].Value) -or [string]::IsNullOrWhiteSpace($match.Groups["Platform"].Value)) - { - Write-Warning "Could not parse configuration entry $line from file $solutionFile." - continue; - } - if ($match.Groups["Configuration"].Value.Trim() -eq $Configuration -and $match.Groups["Platform"].Value.Trim() -eq $Platform) - { - $configurationIsSupported = $true; - } - } -} - -if (-not $configurationIsSupported) -{ - Write-Verbose "[$SampleName] `u{23E9} Skipped. Configuration $Configuration|$Platform not supported." - exit 3 -} - -Write-Verbose "Building Sample: $SampleName; Configuration: $Configuration; Platform: $Platform {" - -$myexit=1 - -# -# Let us build up to three times (0th, 1st, and 2nd attempt). -# If we succeed at first, then it is a success. -# If we fail at first, but succeed at either of next two attempts, then it is a sporadic failure. -# If we even at third attempt fail, then it is a true failure. -# -for ($i = 0; $i -lt 3; $i++) -{ - $binLogFilePath = "$LogFilesDirectory\$SampleName.$Configuration.$Platform.$i.binlog" - $errorLogFilePath = "$LogFilesDirectory\$SampleName.$Configuration.$Platform.$i.err" - $warnLogFilePath = "$LogFilesDirectory\$SampleName.$Configuration.$Platform.$i.wrn" - $OutLogFilePath = "$LogFilesDirectory\$SampleName.$Configuration.$Platform.$i.out" - - msbuild $solutionFile -clp:Verbosity=m -t:rebuild -property:Configuration=$Configuration -property:Platform=$Platform -p:TargetVersion=Windows10 -p:InfVerif_AdditionalOptions="$InfVerif_AdditionalOptions" -warnaserror -binaryLogger:LogFile=$binLogFilePath`;ProjectImports=None -flp1:errorsonly`;logfile=$errorLogFilePath -flp2:WarningsOnly`;logfile=$warnLogFilePath -noLogo > $OutLogFilePath - if ($null -ne $env:WDS_WipeOutputs) - { - Write-Verbose ("WipeOutputs: " + $Directory + " " + (((Get-Volume (Get-Item ".").PSDrive.Name).SizeRemaining / 1GB))) - Get-ChildItem -path $Directory -Recurse -Include x64 | Remove-Item -Recurse - Get-ChildItem -path $Directory -Recurse -Include arm64 | Remove-Item -Recurse - } - if ($LASTEXITCODE -eq 0) - { - # We succeeded building. - # If it was at a later attempt, let the caller know with a different exit code. - if ($i -eq 0) - { - $myexit = 0 - } - else - { - $myexit = 2 - } - # Remove binlog on success to save space; keep otherwise to diagnose issues. - Remove-Item $binLogFilePath - break; - } - else - { - # We failed building. - # Let us sleep for a bit. - # Then let the while loop do its thing and re-run. - Start-Sleep 1 - if ($Verbose) - { - Write-Warning "`u{274C} Build failed. Retrying to see if sporadic..." - } - } -} - -if ($myexit -eq 1) -{ - if ($Verbose) - { - Write-Warning "`u{274C} Build failed. Log available at $errorLogFilePath" - } - exit 1 -} - -if ($myexit -eq 2) -{ - if ($Verbose) - { - Write-Warning "`u{274C} Build sporadically failed. Log available at $errorLogFilePath" - } - exit 2 -} - -Write-Verbose "Building Sample: $SampleName; Configuration: $Configuration; Platform: $Platform }" diff --git a/Build-Samples.ps1 b/Build-Samples.ps1 index aad2d3e5..4242d428 100644 --- a/Build-Samples.ps1 +++ b/Build-Samples.ps1 @@ -14,10 +14,6 @@ Requires PowerShell 7+ (uses ForEach-Object -Parallel). - Typical call chain: - Build-AllSamples.ps1 -> Build-Samples.ps1 -> ListAllSamples.ps1 (for discovery) - (this script) -> Build-Sample.ps1 (per sample) - .PARAMETER Samples Optional array of specific sample names to build. When omitted, all samples are discovered dynamically via ListAllSamples.ps1. @@ -240,6 +236,141 @@ function Get-DiskFreeGB { } } +function Build-SingleSample { + <# + .SYNOPSIS + Builds a single sample directory for one configuration/platform combination. + .DESCRIPTION + Locates the .sln in the given directory, verifies the configuration|platform is + supported, then invokes msbuild with up to 3 attempts (to detect sporadic failures). + .OUTPUTS + Returns an integer exit code: + 0 = succeeded on first attempt + 1 = failed after all retries + 2 = sporadic (failed first, succeeded on retry) + 3 = unsupported configuration/platform + #> + param( + [string]$Directory, + [string]$SampleName, + [string]$Configuration = 'Debug', + [string]$Platform = 'x64', + [string]$InfVerif_AdditionalOptions = '/samples', + [string]$LogFilesDirectory = (Get-Location), + [bool]$Verbose = $false + ) + + if (-not (Test-Path -Path $Directory -PathType Container)) { + Write-Warning "`u{274C} A valid directory could not be found under $Directory" + return 1 + } + + New-Item -ItemType Directory -Force -Path $LogFilesDirectory | Out-Null + + if ([string]::IsNullOrWhiteSpace($SampleName)) { + $SampleName = (Resolve-Path $Directory).Path.Replace((Get-Location), '').Replace('\', '.').Trim('.').ToLower() + } + + $solutionFile = Get-ChildItem -Path $Directory -Filter *.sln | + Select-Object -ExpandProperty FullName -First 1 + + if ($null -eq $solutionFile) { + Write-Warning "`u{274C} A solution could not be found under $Directory" + return 1 + } + + # --- Check whether the solution supports the requested configuration|platform --- + $configurationIsSupported = $false + $inSolutionConfigurationPlatformsSection = $false + foreach ($line in Get-Content -Path $solutionFile) { + if (-not $inSolutionConfigurationPlatformsSection -and + $line -match '\s*GlobalSection\(SolutionConfigurationPlatforms\).*') { + $inSolutionConfigurationPlatformsSection = $true + continue + } + elseif ($line -match '\s*EndGlobalSection.*') { + $inSolutionConfigurationPlatformsSection = $false + continue + } + + if ($inSolutionConfigurationPlatformsSection) { + [regex]$regex = '.*=\s*(?(?.*)\|(?.*))\s*' + $match = $regex.Match($line) + if ([string]::IsNullOrWhiteSpace($match.Groups['ConfigString'].Value) -or + [string]::IsNullOrWhiteSpace($match.Groups['Platform'].Value)) { + Write-Warning "Could not parse configuration entry $line from file $solutionFile." + continue + } + if ($match.Groups['Configuration'].Value.Trim() -eq $Configuration -and + $match.Groups['Platform'].Value.Trim() -eq $Platform) { + $configurationIsSupported = $true + } + } + } + + if (-not $configurationIsSupported) { + Write-Verbose "[$SampleName] `u{23E9} Skipped. Configuration $Configuration|$Platform not supported." + return 3 + } + + Write-Verbose "Building Sample: $SampleName; Configuration: $Configuration; Platform: $Platform {" + + $myexit = 1 + + # Build up to three times (0th, 1st, and 2nd attempt). + # Succeed on 1st -> success (0) + # Fail 1st, succeed on retry -> sporadic (2) + # Fail all three -> failure (1) + for ($i = 0; $i -lt 3; $i++) { + $binLogFilePath = "$LogFilesDirectory\$SampleName.$Configuration.$Platform.$i.binlog" + $errorLogFilePath = "$LogFilesDirectory\$SampleName.$Configuration.$Platform.$i.err" + $warnLogFilePath = "$LogFilesDirectory\$SampleName.$Configuration.$Platform.$i.wrn" + $outLogFilePath = "$LogFilesDirectory\$SampleName.$Configuration.$Platform.$i.out" + + msbuild $solutionFile ` + -clp:Verbosity=m -t:rebuild ` + -property:Configuration=$Configuration ` + -property:Platform=$Platform ` + -p:TargetVersion=Windows10 ` + -p:InfVerif_AdditionalOptions="$InfVerif_AdditionalOptions" ` + -warnaserror ` + -binaryLogger:LogFile=$binLogFilePath`;ProjectImports=None ` + -flp1:errorsonly`;logfile=$errorLogFilePath ` + -flp2:WarningsOnly`;logfile=$warnLogFilePath ` + -noLogo > $outLogFilePath + + if ($null -ne $env:WDS_WipeOutputs) { + Write-Verbose ("WipeOutputs: $Directory " + (((Get-Volume (Get-Item '.').PSDrive.Name).SizeRemaining / 1GB))) + Get-ChildItem -Path $Directory -Recurse -Include x64 | Remove-Item -Recurse + Get-ChildItem -Path $Directory -Recurse -Include arm64 | Remove-Item -Recurse + } + + if ($LASTEXITCODE -eq 0) { + $myexit = if ($i -eq 0) { 0 } else { 2 } + # Remove binlog on success to save space; keep otherwise to diagnose issues. + Remove-Item $binLogFilePath + break + } + else { + Start-Sleep 1 + if ($Verbose) { + Write-Warning "`u{274C} Build failed. Retrying to see if sporadic..." + } + } + } + + if ($myexit -eq 1 -and $Verbose) { + Write-Warning "`u{274C} Build failed. Log available at $errorLogFilePath" + } + if ($myexit -eq 2 -and $Verbose) { + Write-Warning "`u{274C} Build sporadically failed. Log available at $errorLogFilePath" + } + + Write-Verbose "Building Sample: $SampleName; Configuration: $Configuration; Platform: $Platform }" + + return $myexit +} + # ============================================================================= # Step 1 - Prepare Build Environment # ============================================================================= @@ -392,6 +523,9 @@ $buildState = @{ $stopwatch = [Diagnostics.Stopwatch]::StartNew() +# Capture function definition so it can be reconstructed inside each parallel runspace. +$buildSingleSampleDef = ${function:Build-SingleSample}.ToString() + $sampleSet.GetEnumerator() | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel { # --- Import shared state from parent scope --- $logDir = $using:LogFilesDirectory @@ -404,6 +538,9 @@ $sampleSet.GetEnumerator() | ForEach-Object -ThrottleLimit $ThrottleLimit -Paral $total = $using:combinationsTotal $throttle = $using:ThrottleLimit + # Reconstruct the function inside this parallel runspace + ${function:Build-SingleSample} = $using:buildSingleSampleDef + $sampleName = $_.Key $directory = $_.Value @@ -443,17 +580,18 @@ $sampleSet.GetEnumerator() | ForEach-Object -ThrottleLimit $ThrottleLimit -Paral } else { # -- Build the sample -- - .\Build-Sample -Directory $directory -SampleName $sampleName ` + $buildResult = Build-SingleSample ` + -Directory $directory -SampleName $sampleName ` -LogFilesDirectory $logDir -Configuration $configuration ` -Platform $platform -InfVerif_AdditionalOptions $infOpts ` -Verbose:$isVerbose - # Exit codes from Build-Sample.ps1: + # Return codes from Build-SingleSample: # 0 = succeeded on first attempt # 1 = failed after all retries # 2 = sporadic (failed first, succeeded on retry) # 3 = unsupported configuration/platform - switch ($LASTEXITCODE) { + switch ($buildResult) { 0 { $succeededDelta = 1; $result = 'Succeeded' } 1 { $failedDelta = 1; $result = 'Failed'; $failEntry = "$sampleName $configuration|$platform" } 2 { $sporadicDelta = 1; $result = 'Sporadic'; $sporadicEntry = "$sampleName $configuration|$platform" } -- cgit v1.3.1 From 83cfa72cc265ab052100563cfc2e8c9d9b6020ba Mon Sep 17 00:00:00 2001 From: 5an7y Date: Mon, 30 Mar 2026 15:43:16 -0700 Subject: Support wildcard patterns in -Samples parameter - When -Samples contains wildcards (* or ?), discover all samples first then filter with -like (same pattern style as exclusions.csv) - Exact sample names still work as before without discovery overhead - Update parameter docs, examples, and Building-Locally.md --- Build-Samples.ps1 | 43 +++++++++++++++++++++++++++++++++++-------- Building-Locally.md | 3 +++ 2 files changed, 38 insertions(+), 8 deletions(-) (limited to 'Build-Samples.ps1') diff --git a/Build-Samples.ps1 b/Build-Samples.ps1 index 4242d428..7eafb54d 100644 --- a/Build-Samples.ps1 +++ b/Build-Samples.ps1 @@ -16,7 +16,9 @@ .PARAMETER Samples - Optional array of specific sample names to build. When omitted, all samples are discovered dynamically via ListAllSamples.ps1. + Optional array of specific sample names or wildcard patterns to build. Supports + wildcards (e.g. 'tools.*', 'audio.*') which are matched against all discovered + samples. When omitted, all samples are discovered dynamically via ListAllSamples.ps1. .PARAMETER Configurations Build configurations (e.g. 'Debug','Release'). Defaults to $env:WDS_Configuration or @@ -49,6 +51,11 @@ Builds specific samples for a single configuration and platform. +.EXAMPLE + .\Build-Samples -Samples 'tools.*' + + Builds all samples whose name matches the wildcard pattern 'tools.*'. + .EXAMPLE .\Build-Samples -ThrottleLimit 8 @@ -408,17 +415,37 @@ $reportCsvPath = Join-Path $LogFilesDirectory "$ReportFileName.csv" # Step 4 - Load Sample List # ============================================================================= -if ($Samples) { - # Explicit list passed by the caller — use as-is, sorted alphabetically. - $sampleNames = $Samples | Sort-Object -} -else { - # Default: discover samples dynamically via ListAllSamples.ps1. - # ListAllSamples outputs one alphabetically-sorted sample name per line to stdout. +# Always discover the full sample list when patterns contain wildcards, +# or when no -Samples were provided at all. +$hasWildcards = $Samples | Where-Object { $_ -match '[*?]' } + +if (-not $Samples) { + # No filter: discover and build everything. Write-Verbose "No -Samples provided. Discovering samples via ListAllSamples.ps1..." $sampleNames = & (Join-Path $PSScriptRoot 'ListAllSamples.ps1') -Verbose:$verbose | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } } +elseif ($hasWildcards) { + # One or more entries contain wildcards — discover all, then filter with -like. + Write-Verbose "Wildcard detected in -Samples. Discovering all samples to match patterns..." + $allSamples = & (Join-Path $PSScriptRoot 'ListAllSamples.ps1') -Verbose:$verbose | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + $sampleNames = @() + foreach ($pattern in $Samples) { + $matched = $allSamples | Where-Object { $_ -like $pattern } + if ($matched) { + $sampleNames += $matched + } + else { + Write-Warning "Pattern '$pattern' did not match any samples." + } + } + $sampleNames = $sampleNames | Sort-Object -Unique +} +else { + # Exact list passed by the caller — use as-is, sorted alphabetically. + $sampleNames = $Samples | Sort-Object +} # Map sample names to directory paths, validating each exists $sampleSet = [ordered]@{} diff --git a/Building-Locally.md b/Building-Locally.md index d988a330..45110098 100644 --- a/Building-Locally.md +++ b/Building-Locally.md @@ -155,6 +155,9 @@ Get-Help .\Build-Samples # Build specific samples for all flavors: .\Build-Samples -Samples 'tools.sdv.samples.sampledriver','usb.kmdf_fx2' +# Build all samples inside the 'tools' folder using wildcards: +.\Build-Samples -Samples 'tools.*' + # Build specific samples for only 'Debug|x64': .\Build-Samples -Samples 'tools.sdv.samples.sampledriver' -Configurations 'Debug' -Platforms 'x64' ``` -- cgit v1.3.1