diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 770ecda..e000ab4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,32 +14,23 @@ on: jobs: build: runs-on: windows-latest + environment: + name: General steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Invoke-Pester shell: pwsh run: | - Import-Module Pester - $Configuration = [PesterConfiguration]@{ - Run = @{ - Path = '*' - Passthru = $true - } - CodeCoverage = @{ - Enabled = $true - OutputFormat = 'JaCoCo' - OutputPath = '${{ env.GITHUB_WORKSPACE }}/Pester-Coverage.xml' - } - } - Invoke-Pester -Configuration $Configuration - - uses: codecov/codecov-action@v3 + ${{ github.workspace }}/Build/RunTests.ps1 + - uses: codecov/codecov-action@v5 with: - files: '${{ env.GITHUB_WORKSPACE }}/Pester-Coverage.xml' + files: '${{ github.workspace }}/__output/coverage.xml' flags: unittests name: codecov-umbrella + token: '${{ secrets.CODECOV_TOKEN }}' fail_ci_if_error: true verbose: true - name: Build @@ -47,7 +38,7 @@ jobs: run: | & .\Build\Publish.ps1 -Verbose - name: Upload __packages - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: __packages path: __packages @@ -59,7 +50,7 @@ jobs: steps: - name: Download __packages id: download - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v5 with: name: __packages path: __packages diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 4d4b1f7..aef6467 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -5,33 +5,23 @@ on: jobs: build: runs-on: windows-latest + environment: + name: General steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Invoke-Pester shell: pwsh run: | - Import-Module Pester - $Configuration = [PesterConfiguration]@{ - Run = @{ - Path = '*' - Passthru = $true - } - CodeCoverage = @{ - Enabled = $true - OutputFormat = 'JaCoCo' - OutputPath = '${{ env.GITHUB_WORKSPACE }}/Pester-Coverage.xml' - } - } - Invoke-Pester -Configuration $Configuration + ${{ github.workspace }}/Build/RunTests.ps1 - name: Build shell: pwsh run: | & .\Build\Publish.ps1 -Verbose - name: Upload __packages - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: __packages path: __packages diff --git a/.vscode/PSScriptAnalyzerSettings.psd1 b/.vscode/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000..355e88a --- /dev/null +++ b/.vscode/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,13 @@ +@{ + Rules = @{ + PSReviewUnusedParameter = @{ + CommandsToTraverse = @( + 'SearchAncestors' + 'Using-Location' + ) + } + } + IncludeRules = @('*') + ExcludeRules = @('PSUseApprovedVerbs') +} + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9a725c2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "coverage-gutters.coverageBaseDir": "__output", + "editor.renderWhitespace": "all", + "powershell.codeFormatting.newLineAfterCloseBrace": false, + "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", + "powershell.scriptAnalysis.enable": true, + "powershell.scriptAnalysis.settingsPath": ".vscode/PSScriptAnalyzerSettings.psd1" +} diff --git a/Build/RunScriptAnalyzer.ps1 b/Build/RunScriptAnalyzer.ps1 new file mode 100644 index 0000000..f0bff31 --- /dev/null +++ b/Build/RunScriptAnalyzer.ps1 @@ -0,0 +1,18 @@ +#Requires -PSEdition Core + +[CmdletBinding()] +param( +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$WorkingDirectory = $PSScriptRoot +$RepositoryRoot = Resolve-Path -Path (& git -C $WorkingDirectory rev-parse '--show-toplevel') +$SettingsFile = Join-Path -Path $RepositoryRoot -ChildPath '.vscode\PSScriptAnalyzerSettings.psd1' +& git -C $RepositoryRoot ls-files *.ps1 *.psm1 *.psd1 | + ForEach-Object { Get-Item -Path (Join-Path -Path $RepositoryRoot -ChildPath $_) } | + ForEach-Object { Invoke-ScriptAnalyzer -Path $_ -Settings $SettingsFile } | + ForEach-Object { + "$($_.ScriptPath):$($_.Line):$($_.Column) [$($_.RuleName)] $($_.Message)" + } diff --git a/Build/RunTests.ps1 b/Build/RunTests.ps1 new file mode 100644 index 0000000..bad0c94 --- /dev/null +++ b/Build/RunTests.ps1 @@ -0,0 +1,27 @@ +#Requires -PSEdition Core + +[CmdletBinding()] +param( +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +Import-Module Pester + +$WorkingDirectory = $PSScriptRoot +$RepositoryRoot = Resolve-Path -Path (& git -C $WorkingDirectory rev-parse '--show-toplevel') +$Configuration = [PesterConfiguration]@{ + Run = @{ + Path = $RepositoryRoot + Passthru = $true + } + CodeCoverage = @{ + Enabled = $true + Path = "$RepositoryRoot/PSCMake" + OutputFormat = 'JaCoCo' + OutputPath = "$RepositoryRoot/__output/coverage.xml" + } +} + +Invoke-Pester -Configuration $Configuration diff --git a/LICENSE b/LICENSE index 74afde0..21c1e11 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Mark Schofield +Copyright (c) 2025 Mark Schofield Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/PSCMake/Common/CMake.ps1 b/PSCMake/Common/CMake.ps1 index 936fdd6..f45faa4 100644 --- a/PSCMake/Common/CMake.ps1 +++ b/PSCMake/Common/CMake.ps1 @@ -1,7 +1,7 @@ #---------------------------------------------------------------------------------------------------------------------- # MIT License # -# Copyright (c) 2021 Mark Schofield +# Copyright (c) 2025 Mark Schofield # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -30,7 +30,6 @@ $ErrorActionPreference = 'Stop' $PEnv = Get-ChildItem env: | ToHashTable -$PreviousLocation = $null $CMakeCandidates = @( (Get-Command 'cmake' -ErrorAction SilentlyContinue) if ($IsWindows) { @@ -39,33 +38,27 @@ $CMakeCandidates = @( ) <# - .Synopsis - Finds the root of the CMake build - the current or ancestral folder containing a 'CMakePresets.json' file. + .Synopsis + Finds the root of the CMake build - the current or ancestral folder containing a 'CMakePresets.json' file. #> function FindCMakeRoot { $CurrentLocation = (Get-Location).Path - if ($CurrentLocation -ne $script:PreviousLocation) { - Write-Verbose "PreviousLocation = $script:PreviousLocation" - Write-Verbose "CurrentLocation = $CurrentLocation" - $script:PreviousLocation = $CurrentLocation - $script:CMakeRoot = GetPathOfFileAbove $CurrentLocation 'CMakePresets.json' - } - $script:CMakeRoot + GetPathOfFileAbove $CurrentLocation 'CMakePresets.json' } $script:CMakePresetsPath = $null <# - .Synopsis - Gets the path that the most recently loaded CMakePresets.json was loaded from. + .Synopsis + Gets the path that the most recently loaded CMakePresets.json was loaded from. #> function GetCMakePresetsPath { $script:CMakePresetsPath } <# - .Synopsis - Loads the CMakePresets.json into a PowerShell representation. + .Synopsis + Loads the CMakePresets.json into a PowerShell representation. #> function GetCMakePresets { param( @@ -84,8 +77,8 @@ function GetCMakePresets { } <# - .Synopsis - Gets names of the 'buildPresets' in the specified CMakePresets.json object. + .Synopsis + Gets names of the 'buildPresets' in the specified CMakePresets.json object. #> function GetBuildPresetNames { param( @@ -103,18 +96,18 @@ function GetBuildPresetNames { # Filter presets that have configure presets that have conditions that evaluate to $false $Presets = $Presets | Where-Object { $BuildPresetJson = $_ - $ConfigurePresetJson = $CMakePresetsJson.configurePresets | - Where-Object { $_.name -eq $BuildPresetJson.configurePreset } | - Where-Object { EvaluatePresetCondition $_ $CMakePresetsJson.configurePresets } + $ConfigurePresetJson = $CMakePresetsJson.configurePresets | Where-Object { $_.name -eq $BuildPresetJson.configurePreset } | Where-Object { EvaluatePresetCondition $_ $CMakePresetsJson.configurePresets } + $null -ne $ConfigurePresetJson } + $Presets.name } } <# - .Synopsis - Gets names of the 'configurePresets' in the specified CMakePresets.json object. + .Synopsis + Gets names of the 'configurePresets' in the specified CMakePresets.json object. #> function GetConfigurePresetNames { param( @@ -124,26 +117,28 @@ function GetConfigurePresetNames { $Presets = $CMakePresetsJson.configurePresets # Filter presets that have '"hidden":true' - $Presets = $Presets | Where-Object { -not (Get-MemberValue -InputObject $_ -Name 'hidden' -Or $false) } + $Presets = $Presets | + Where-Object { -not (Get-MemberValue -InputObject $_ -Name 'hidden' -Or $false) } # Filter presets that have (or their ancestors have) conditions that evaluate to $false - $Presets = $Presets | Where-Object { EvaluatePresetCondition $_ $CMakePresetsJson.configurePresets } + $Presets = $Presets | + Where-Object { EvaluatePresetCondition $_ $CMakePresetsJson.configurePresets } $Presets.name } } <# - .Synopsis - Finds the 'CMake' command. + .Synopsis + Finds the 'CMake' command. #> function GetCMake { - $CMake = Get-Variable -Name 'CMake' -ValueOnly -Scope global -ErrorAction SilentlyContinue + $CMake = Get-Variable -Name 'CMake' -ValueOnly -Scope script -ErrorAction SilentlyContinue if (-not $CMake) { foreach ($CMakeCandidate in $CMakeCandidates) { $CMake = Get-Command $CMakeCandidate -ErrorAction SilentlyContinue if ($CMake) { - $global:CMake = $CMake + $script:CMake = $CMake break } } @@ -176,48 +171,72 @@ function ResolvePresets { $PresetJson, $ConfigurePresetJson } -function ResolvePresetProperty { - param( - $CMakePresetsJson, - $ConfigurePreset, - $PropertyName - ) +<# + .Synopsis + Searches the specified preset and its ancestors, invoking the specified action for each preset. - for ($Preset = $ConfigurePreset; $Preset; ) { - $PropertyValue = Get-MemberValue -InputObject $Preset -Name $PropertyName - if ($PropertyValue) { - return $PropertyValue - } + .Parameter Preset + The preset to start searching from. + + .Parameter Presets + The collection of presets to search for 'inherit' references. - $BasePreset = Get-MemberValue $Preset 'inherits' - if (-not $BasePreset) { - break + .Description + The action should return $null to continue searching, or a non-$null value to stop searching and return that value. + + When searching multiple preset 'inherit' values, the presets will be search in order. +#> +function SearchAncestors { + param( + $Preset, + $Presets, + [scriptblock] $Action + ) + if ($null -eq $Preset) { + return $null + } + [array] $PendingPresets = @($Preset) + for (; ($null -ne $PendingPresets) -and ($PendingPresets.Count -gt 0); ) { + $Preset, $PendingPresets = $PendingPresets + $Result = & $Action $Preset + if ($null -ne $Result) { + return $Result } + [array] $BasePresets = Get-MemberValue $Preset 'inherits' -Or @() | + ForEach-Object { + $BaseParentName = $_ + $Presets | Where-Object { $_.name -eq $BaseParentName } | Select-Object -First 1 + } + $PendingPresets = $BasePresets + $PendingPresets + } +} - $Preset = $CMakePresetsJson.configurePresets | Where-Object { $_.name -eq $BasePreset } | Select-Object -First 1 +function ResolvePresetProperty { + param( + $Preset, + $Presets, + $PropertyName + ) + SearchAncestors -Preset $Preset -Presets $Presets { + param($CurrentPreset) + Get-MemberValue -InputObject $CurrentPreset -Name $PropertyName } } function EvaluatePresetCondition { param( - $PresetJson, - $PresetsJson + $Preset, + $Presets ) - - $PresetConditionJson = Get-MemberValue $PresetJson 'condition' - if ($PresetConditionJson) { - if (-not (EvaluateCondition $PresetConditionJson $PresetJson)) { + $Result = SearchAncestors -Preset $Preset -Presets $Presets { + param($CurrentPreset) + $PresetConditionJson = Get-MemberValue $CurrentPreset 'condition' + if (($PresetConditionJson) -and + (-not (EvaluateCondition $PresetConditionJson $CurrentPreset))) { return $false } } - - $BasePresetName = Get-MemberValue $PresetJson 'inherits' - if (-not $BasePresetName) { - return $true - } - - $BasePreset = $PresetsJson | Where-Object { $_.name -eq $BasePresetName } | Select-Object -First 1 - EvaluatePresetCondition $BasePreset $PresetsJson + $Result -ne $false } function EvaluateCondition { @@ -225,8 +244,7 @@ function EvaluateCondition { $ConditionJson, $PresetJson ) - switch ($ConditionJson.type) - { + switch ($ConditionJson.type) { 'equals' { return (MacroReplacement $ConditionJson.lhs $PresetJson) -eq (MacroReplacement $ConditionJson.rhs $PresetJson) } @@ -288,7 +306,7 @@ function GetBinaryDirectory { $CMakePresetsJson, $ConfigurePreset ) - $BinaryDirectory = ResolvePresetProperty $CMakePresetsJson $ConfigurePreset 'binaryDir' + $BinaryDirectory = ResolvePresetProperty -Preset $ConfigurePreset -Presets $CMakePresetsJson.configurePresets -PropertyName 'binaryDir' # Perform macro-replacement $Result = MacroReplacement $BinaryDirectory $ConfigurePreset @@ -309,8 +327,8 @@ function GetMacroConstants { } @{ - '${hostSystemName}'=$HostSystemName - '$vendor{PSCMake}'='true' + '${hostSystemName}' = $HostSystemName + '$vendor{PSCMake}' = 'true' } } @@ -391,6 +409,27 @@ function Enable-CMakeBuildQuery { } } +# For the 'code model' JSON that was found, load the full 'target' JSON to be able to find 'EXECUTABLE' targets. +# +function FilterExecutableTargets { + param ( + $CodeModelDirectory, + $TargetTuplesCodeModel + ) + $TargetJsons = $TargetTuplesCodeModel | + ForEach-Object { + Join-Path -Path $CodeModelDirectory -ChildPath $_.jsonFile | + Get-Item | + Get-Content | + ConvertFrom-Json + } + + $TargetJsons | + Where-Object { + $_.type -eq 'EXECUTABLE' + } +} + function Get-CMakeBuildCodeModelDirectory { param( [string] $BinaryDirectory @@ -399,28 +438,33 @@ function Get-CMakeBuildCodeModelDirectory { } <# - .Synopsis - Gets PowerShell representation of the CodeModel JSON for the given binary directory. + .Synopsis + Gets PowerShell representation of the CodeModel JSON for the given binary directory. - .Outputs - The PowerShell representation of the CodeModel JSON for the given binary directory, or `$null` if it can't be found. + .Outputs + The PowerShell representation of the CodeModel JSON for the given binary directory, or `$null` if it can't be found. #> function Get-CMakeBuildCodeModel { param( [string] $BinaryDirectory ) - Get-ChildItem -Path (Get-CMakeBuildCodeModelDirectory $BinaryDirectory) -File -Filter 'codemodel-v2-*' -ErrorAction SilentlyContinue | + + # Since BinaryDirectory may contain characters that are valid for the file-system, but are used by PowerShell's + # wildcard syntax (i.e. '[' and ']'), escape the characters before passing to Get-ChildItem. + $EscapedBinaryDirectory = $BinaryDirectory.Replace('[', '`[').Replace(']', '`]') + + Get-ChildItem -Path (Get-CMakeBuildCodeModelDirectory $EscapedBinaryDirectory) -File -Filter 'codemodel-v2-*' -ErrorAction SilentlyContinue | Select-Object -First 1 | Get-Content | ConvertFrom-Json } <# - .Synopsis - Gets the target with the given name, for the given configuration from the specified code model. + .Synopsis + Gets the target with the given name, for the given configuration from the specified code model. - .Outputs - The PowerShell representation of the target from the CodeModel JSON. + .Outputs + The PowerShell representation of the target from the CodeModel JSON. #> function GetNamedTarget { param( @@ -440,11 +484,11 @@ function GetNamedTarget { } <# - .Synopsis - Gets all targets within the given folder scope, for the given configuration from the specified code model. + .Synopsis + Gets all targets within the given folder scope, for the given configuration from the specified code model. - .Outputs - The PowerShell representation of the target(s) from the CodeModel JSON. + .Outputs + The PowerShell representation of the target(s) from the CodeModel JSON. #> function GetScopedTargets { param( @@ -452,20 +496,26 @@ function GetScopedTargets { $Configuration, $ScopeLocation ) + function CanonicalizeDirectoryPath($Path) { + Resolve-Path -Path (Join-Path -Path $Path -ChildPath '/') + } + $ScopeLocation = CanonicalizeDirectoryPath $ScopeLocation $CodeModelConfiguration = if ($Configuration) { $CodeModel.configurations | Where-Object { $_.name -eq $Configuration } } else { $CodeModel.configurations[0] } + $SourceDir = $CodeModel.paths.source $CodeModelConfiguration.targets | Where-Object { $Folder = $CodeModelConfiguration.directories[$_.directoryIndex].build $Folder = if ($Folder -eq '.') { - $CMakeRoot + $SourceDir } else { - Join-Path -Path $CMakeRoot -ChildPath $Folder + Join-Path -Path $SourceDir -ChildPath $Folder } - $Folder.StartsWith($ScopeLocation) + $Folder = CanonicalizeDirectoryPath $Folder + $Folder.Path.StartsWith($ScopeLocation.Path) } } @@ -500,17 +550,33 @@ function WriteDgml { $Targets = @{} '' '' + '' + '' + '' + '' + '' + '' + '' + $SourcePath = $CodeModel.paths.source '' ($CodeModel.configurations | Where-Object { $_.name -eq $Configuration }).targets | ForEach-Object { + $TargetJson = Get-Content (Join-Path -Path $CodeModelDirectory -ChildPath $_.jsonFile) | + ConvertFrom-Json + + $ReferenceFileIndex = $TargetJson.backtraceGraph.nodes[0].file + $Definition = Join-Path -Path $SourcePath -ChildPath $TargetJson.backtraceGraph.files[$ReferenceFileIndex] + '' - $TargetJson = Get-Content (Join-Path -Path $CodeModelDirectory -ChildPath $_.jsonFile) | - ConvertFrom-Json - Get-MemberValue -InputObject $TargetJson -Name artifacts -Or @() | ForEach-Object { ' -function ColorInterpolation{ +function ColorInterpolation { param( [System.Drawing.Color]$FromColor, [System.Drawing.Color]$ToColor, @@ -56,8 +56,8 @@ function ColorInterpolation{ } <# - .Synopsis - Checks whether the given file is newer than any subsequently specified files. + .Synopsis + Checks whether the given file is newer than any subsequently specified files. #> function IsUpToDate($Target) { $Dependencies = $args @@ -96,8 +96,8 @@ function DownloadFile([string] $Url, [string] $DownloadPath) { } <# - .Synopsis - Searches the given location and parent folders looking for the given file. + .Synopsis + Searches the given location and parent folders looking for the given file. #> function GetPathOfFileAbove([string]$Location, [string]$File) { for (; $Location.Length -ne 0; $Location = Split-Path $Location) { @@ -109,8 +109,8 @@ function GetPathOfFileAbove([string]$Location, [string]$File) { } <# - .Synopsis - Converts named items on a Pipeline into a hash table. + .Synopsis + Converts named items on a Pipeline into a hash table. #> filter ToHashTable { begin { $Result = @{} } diff --git a/PSCMake/Common/Console.ps1 b/PSCMake/Common/Console.ps1 index d97a389..9fa465d 100644 --- a/PSCMake/Common/Console.ps1 +++ b/PSCMake/Common/Console.ps1 @@ -1,7 +1,7 @@ #---------------------------------------------------------------------------------------------------------------------- # MIT License # -# Copyright (c) 2021 Mark Schofield +# Copyright (c) 2025 Mark Schofield # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -74,11 +74,11 @@ function GetConsole { } <# - .Synopsis - Returns whether virtual terminal processing is enabled for the current console. + .Synopsis + Returns whether virtual terminal processing is enabled for the current console. - .Outputs - `$true` if virtual terminal processing is enabled, `$false` otherwise. + .Outputs + `$true` if virtual terminal processing is enabled, `$false` otherwise. #> function IsVirtualTerminalProcessingEnabled { if ($null -eq $script:IsVirtualTerminalProcessingEnabled) { @@ -101,10 +101,10 @@ function IsVirtualTerminalProcessingEnabled { } <# - .Synopsis - Returns the control codes to set the foreground color to the specified value. + .Synopsis + Returns the control codes to set the foreground color to the specified value. #> -function ColorToControlCode{ +function ColorToControlCode { param( [System.Drawing.Color]$Color ) @@ -114,10 +114,10 @@ function ColorToControlCode{ } <# - .Synopsis - Returns the control codes to reset foreground attributes, if virtual terminal processing is enable. + .Synopsis + Returns the control codes to reset foreground attributes, if virtual terminal processing is enable. #> -function ResetForegroundControlCode{ +function ResetForegroundControlCode { if (IsVirtualTerminalProcessingEnabled) { "`e[39m" } diff --git a/PSCMake/Common/Ninja.ps1 b/PSCMake/Common/Ninja.ps1 index 4bdcdee..7177414 100644 --- a/PSCMake/Common/Ninja.ps1 +++ b/PSCMake/Common/Ninja.ps1 @@ -1,7 +1,7 @@ #---------------------------------------------------------------------------------------------------------------------- # MIT License # -# Copyright (c) 2021 Mark Schofield +# Copyright (c) 2025 Mark Schofield # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -30,11 +30,11 @@ $ErrorActionPreference = 'Stop' . $PSScriptRoot/Console.ps1 <# - .Synopsis - Tries to parse the specified Ninja log. + .Synopsis + Tries to parse the specified Ninja log. - .Outputs - The entries from the Ninja log. + .Outputs + The entries from the Ninja log. #> function TryParseNinjaLog { [CmdletBinding()] @@ -43,18 +43,18 @@ function TryParseNinjaLog { ) if (Test-Path -Path $NinjaLogPath -PathType Leaf) { Get-Content $NinjaLogPath | - Where-Object {$_[0] -ne '#'} | + Where-Object { $_[0] -ne '#' } | ForEach-Object { $Tokens = $_ -split "\t" [int]$StartTime = $Tokens[0] [int]$EndTime = $Tokens[1] [pscustomobject]@{ - StartTime=$StartTime - EndTime=$EndTime - WriteTime=([long]$Tokens[2]) - File=$Tokens[3] - CommandHash=$Tokens[4] - Duration=($EndTime - $StartTime) + StartTime = $StartTime + EndTime = $EndTime + WriteTime = ([long]$Tokens[2]) + File = $Tokens[3] + CommandHash = $Tokens[4] + Duration = ($EndTime - $StartTime) } } } @@ -63,11 +63,11 @@ function TryParseNinjaLog { $FileTimeOffset = [long]12622770400 * [long]10000000 <# - .Synopsis - Converts the specified Ninja log time representation into a [datetime]. + .Synopsis + Converts the specified Ninja log time representation into a [datetime]. - .Notes - This function is currently Windows-only. + .Notes + This function is currently Windows-only. #> function ConvertFrom-NinjaTime { param( @@ -77,11 +77,11 @@ function ConvertFrom-NinjaTime { } <# - .Synopsis - Converts the specified [datetime] into Ninja log time representation. + .Synopsis + Converts the specified [datetime] into Ninja log time representation. - .Notes - This function is currently Windows-only. + .Notes + This function is currently Windows-only. #> function ConvertTo-NinjaTime { param( @@ -97,7 +97,7 @@ function Report-NinjaBuild { ) $BuildStartNinjaTime = ConvertTo-NinjaTime $BuildStartTime $Entries = (TryParseNinjaLog $NinjaLogPath) | - Where-Object {$_.WriteTime -ge $BuildStartNinjaTime} + Where-Object { $_.WriteTime -ge $BuildStartNinjaTime } $Statistics = $Entries | Measure-Object -Property Duration -Maximum if (-not $Statistics) { return @@ -116,8 +116,8 @@ function Report-NinjaBuild { function Download-Ninja { param( [string] $OutputPath, - $NinjaVersion = '1.11.1', - $NinjaArchiveSha256Hash = '524B344A1A9A55005EAF868D991E090AB8CE07FA109F1820D40E74642E289ABC' + $NinjaVersion = '1.13.1', + $NinjaArchiveSha256Hash = '26a40fa8595694dec2fad4911e62d29e10525d2133c9a4230b66397774ae25bf' ) $NinjaArchiveUrl = "https://github.com/ninja-build/ninja/releases/download/v$NinjaVersion/ninja-win.zip" $NinjaArchivePath = Join-Path -Path $OutputPath -ChildPath 'ninja-win.zip' diff --git a/PSCMake/PSCMake.psm1 b/PSCMake/PSCMake.psm1 index ab49438..331861b 100644 --- a/PSCMake/PSCMake.psm1 +++ b/PSCMake/PSCMake.psm1 @@ -1,7 +1,7 @@ #---------------------------------------------------------------------------------------------------------------------- # MIT License # -# Copyright (c) 2021 Mark Schofield +# Copyright (c) 2025 Mark Schofield # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -31,8 +31,24 @@ $ErrorActionPreference = 'Stop' . $PSScriptRoot/Common/Ninja.ps1 <# - .Synopsis - An argument-completer for `Build-CMakeBuild`'s `-Preset` parameter. + .Synopsis + Invokes an executable. + + .Description + A function wrapping calls to '&', allowing the calls to be mocked for testing. +#> +function InvokeExecutable { + param( + [string] $Path, + [string[]] $Arguments + ) + Write-Verbose "Invoking: $Path $Arguments" + & $Path @Arguments +} + +<# + .Synopsis + An argument-completer for `Build-CMakeBuild`'s `-Preset` parameter. #> function BuildPresetsCompleter { param( @@ -42,13 +58,17 @@ function BuildPresetsCompleter { $CommandAst, $FakeBoundParameters ) + $null = $CommandName + $null = $ParameterName + $null = $CommandAst + $null = $FakeBoundParameters $CMakePresetsJson = GetCMakePresets -Silent GetBuildPresetNames $CMakePresetsJson | Where-Object { $_ -ilike "$WordToComplete*" } } <# - .Synopsis - An argument-completer for `Build-CMakeBuild`'s `-Configurations` parameter. + .Synopsis + An argument-completer for `Build-CMakeBuild`'s `-Configurations` parameter. #> function BuildConfigurationsCompleter { param( @@ -58,6 +78,11 @@ function BuildConfigurationsCompleter { $CommandAst, $FakeBoundParameters ) + $null = $CommandName + $null = $ParameterName + $null = $CommandAst + $null = $FakeBoundParameters + # TODO: A meaningful implementation would: # * If a buildPreset can be resolved, see if it has a `configuration` and use that. # * If not, look for a code model and use that. @@ -72,8 +97,8 @@ function BuildConfigurationsCompleter { } <# - .Synopsis - An argument-completer for `Build-CMakeBuild`'s `-Targets` parameter. + .Synopsis + An argument-completer for `Build-CMakeBuild`'s `-Targets` parameter. #> function BuildTargetsCompleter { param( @@ -83,27 +108,81 @@ function BuildTargetsCompleter { $CommandAst, $FakeBoundParameters ) + $null = $CommandName + $null = $ParameterName + $null = $CommandAst $CMakePresetsJson = GetCMakePresets -Silent $PresetNames = GetBuildPresetNames $CMakePresetsJson - $PresetName = $FakeBoundParameters['Presets'] ?? $PresetNames | + $PresetName = $FakeBoundParameters['Preset'] ?? $PresetNames | Select-Object -First 1 $BuildPreset, $ConfigurePreset = ResolvePresets $CMakePresetsJson 'buildPresets' $PresetName $BinaryDirectory = GetBinaryDirectory $CMakePresetsJson $ConfigurePreset $CMakeCodeModel = Get-CMakeBuildCodeModel $BinaryDirectory # TODO: See if the $BuildPreset has a configuration. - $ConfigurationName = $FakeBoundParameters['Configurations'] ?? $CMakeCodeModel.configurations.Name | + $ConfigurationName = $FakeBoundParameters['Configuration'] ?? $CMakeCodeModel.configurations.Name | Select-Object -First 1 $ConfigurationsJson = $CMakeCodeModel.configurations | Where-Object -Property 'name' -EQ $ConfigurationName $TargetNames = $ConfigurationsJson.targets.name + + # Add standard CMake targets 'all', 'clean', 'install' + $TargetNames += @( + 'all' + 'clean' + 'install' + ) + + # Add standard CMake target 'test' if 'CTestTestfile.cmake' exists in the binary directory. + $CTestFilePath = Join-Path -Path $BinaryDirectory -ChildPath 'CTestTestfile.cmake' + if (Test-Path -Path $CTestFilePath -PathType Leaf -ErrorAction SilentlyContinue) { + $TargetNames += 'test' + } + $TargetNames | Where-Object { $_ -ilike "$WordToComplete*" } } <# - .Synopsis - An argument-completer for `Configure-CMakeBuild`'s `-Presets` parameter. + .Synopsis + An argument-completer for `Invoke-CMakeOutput`'s `-Target` parameter. +#> +function ExecutableTargetsCompleter { + param( + $CommandName, + $ParameterName, + $WordToComplete, + $CommandAst, + $FakeBoundParameters + ) + $null = $CommandName + $null = $ParameterName + $null = $CommandAst + $CMakePresetsJson = GetCMakePresets -Silent + $PresetNames = GetBuildPresetNames $CMakePresetsJson + $PresetName = $FakeBoundParameters['Presets'] ?? $PresetNames | + Select-Object -First 1 + $BuildPreset, $ConfigurePreset = ResolvePresets $CMakePresetsJson 'buildPresets' $PresetName + $BinaryDirectory = GetBinaryDirectory $CMakePresetsJson $ConfigurePreset + $CMakeCodeModel = Get-CMakeBuildCodeModel $BinaryDirectory + + # TODO: See if the $BuildPreset has a configuration. + $ConfigurationName = $FakeBoundParameters['Configurations'] ?? $CMakeCodeModel.configurations.Name | + Select-Object -First 1 + $ConfigurationsJson = $CMakeCodeModel.configurations | + Where-Object -Property 'name' -EQ $ConfigurationName + + $TargetTuplesCodeModel = $ConfigurationsJson.targets | + Where-Object { $_.name -ilike "$WordToComplete*" } + + # Use the 'code model' JSON to load the target-specific JSON to filter to targets with 'type' equal to 'EXECUTABLE' + $TargetTuples = FilterExecutableTargets (Get-CMakeBuildCodeModelDirectory $BinaryDirectory) $TargetTuplesCodeModel + $TargetTuples.name +} + +<# + .Synopsis + An argument-completer for `Configure-CMakeBuild`'s `-Preset` parameter. #> function ConfigurePresetsCompleter { param( @@ -113,6 +192,10 @@ function ConfigurePresetsCompleter { $CommandAst, $FakeBoundParameters ) + $null = $CommandName + $null = $ParameterName + $null = $CommandAst + $null = $FakeBoundParameters $CMakePresetsJson = GetCMakePresets -Silent GetConfigurePresetNames $CMakePresetsJson | Where-Object { $_ -ilike "$WordToComplete*" } } @@ -142,108 +225,118 @@ function ConfigureCMake { '--log-level=VERBOSE' } ) - Write-Verbose "CMake Arguments: $CMakeArguments" - & $CMake @CMakeArguments + InvokeExecutable $CMake $CMakeArguments if ($LASTEXITCODE -ne 0) { Write-Error "Configuration failed. Command line: '$($CMake.Source)' $($CMakeArguments -join ' ')" } } <# - .Synopsis - Configures a CMake build. + .Synopsis + Configures a CMake build. - .Description - Configures the specified 'configurePresets' entries from a CMakePresets.json file in the current-or-higher folder. + .Description + Configures the specified 'configurePresets' entries from a CMakePresets.json file in the current-or-higher folder. - .Parameter Presets - The configure preset names to use. + .Parameter Preset + The configure preset name to use. Multiple presets can be specified. -.Parameter Fresh - A switch specifying whether a 'fresh' configuration is performed - removing any existing cache. + .Parameter Fresh + A switch specifying whether a 'fresh' configuration is performed - removing any existing cache. - .Example - # Configure the 'windows-x64' and 'windows-x86' CMake builds. - Configure-CMakeBuild -Presets windows-x64,windows-x86 + .Example + # Configure the 'windows-x64' and 'windows-x86' CMake builds. + Configure-CMakeBuild -Preset windows-x64,windows-x86 #> function Configure-CMakeBuild { [CmdletBinding()] param( + [Alias('Presets')] + [SupportsWildcards()] [Parameter()] - [string[]] $Presets, + [string[]] $Preset, [Parameter()] [switch] $Fresh ) $CMakeRoot = FindCMakeRoot $CMakePresetsJson = GetCMakePresets - $PresetNames = GetConfigurePresetNames $CMakePresetsJson - if (-not $Presets) { - $Presets = $PresetNames | Select-Object -First 1 - Write-Information "No preset specified, defaulting to: $Presets" + $ConfigurePresetNames = GetConfigurePresetNames $CMakePresetsJson + $ConfigurePresetNames = if (-not $Preset) { + $ConfigurePresetNames | Select-Object -First 1 + } else { + foreach ($CandidatePreset in $Preset) { + $ExpandedPresets = $ConfigurePresetNames | Where-Object { $_ -like $CandidatePreset } + $ExpandedPresets ?? $CandidatePreset + } } $CMake = GetCMake Using-Location $CMakeRoot { - foreach ($Preset in $Presets) { - Write-Output "Preset : $Preset" + foreach ($ConfigurePresetName in $ConfigurePresetNames) { + Write-Output "Preset : $ConfigurePresetName" - $ConfigurePreset = $CMakePresetsJson.configurePresets | Where-Object { $_.name -eq $Preset } + $ConfigurePreset = $CMakePresetsJson.configurePresets | Where-Object { $_.name -eq $ConfigurePresetName } if (-not $ConfigurePreset) { - Write-Error "Unable to find configuration preset '$Preset' in $script:CMakePresetsPath" + Write-Error "Unable to find configuration preset '$ConfigurePresetName' in $script:CMakePresetsPath" } - ConfigureCMake $CMake $CMakePresetsJson $ConfigurePreset -Fresh:$Fresh + ConfigureCMake -CMake $CMake $CMakePresetsJson $ConfigurePreset -Fresh:$Fresh } } } <# - .Synopsis - Builds a CMake build. + .Synopsis + Builds a CMake build. - .Description - Builds the specified 'buildPresets' entries from a CMakePresets.json file in the current-or-higher folder. + .Description + Builds the specified 'buildPresets' entries from a CMakePresets.json file in the current-or-higher folder. - .Parameter Presets + .Parameter Preset - .Parameter Configurations + .Parameter Configuration - .Parameter Targets - One or more + .Parameter Target + One or more - .Parameter Configure - A switch specifying whether the necessary configuration should be performed before the build is run. + .Parameter Configure + A switch specifying whether the necessary configuration should be performed before the build is run. - .Parameter Report - [Exploration] A switch specifying whether a report should be written of the command times of the build. Ninja builds only. + .Parameter Report + [Exploration] A switch specifying whether a report should be written of the command times of the build. Ninja builds only. - .Parameter Fresh - A switch specifying whether a 'fresh' configuration should be performed before the build is run. + .Parameter Fresh + A switch specifying whether a 'fresh' configuration should be performed before the build is run. - .Example - # Build the 'windows-x64' and 'windows-x86' CMake builds. - Build-CMakeBuild -Presets windows-x64,windows-x86 + .Example + # Build the 'windows-x64' and 'windows-x86' CMake builds. + Build-CMakeBuild -Preset windows-x64,windows-x86 - # Build the 'windows-x64' and 'windows-x86' CMake builds, with the 'Release' configuration. - Build-CMakeBuild -Presets windows-x64,windows-x86 -Configurations Release + # Build the 'windows-x64' and 'windows-x86' CMake builds, with the 'Release' configuration. + Build-CMakeBuild -Preset windows-x64,windows-x86 -Configuration Release - # Build the 'HelperLibrary' target, for the 'windows-x64' and 'windows-x86' CMake builds, with the 'Release' - # configuration. - Build-CMakeBuild -Presets windows-x64,windows-x86 -Configurations Release -Targets HelperLibrary + # Build the 'HelperLibrary' target, for the 'windows-x64' and 'windows-x86' CMake builds, with the 'Release' + # configuration. + Build-CMakeBuild -Preset windows-x64,windows-x86 -Configuration Release -Target HelperLibrary #> function Build-CMakeBuild { [CmdletBinding()] param( + [Alias('Presets')] + [SupportsWildcards()] [Parameter(Position = 0)] - [string[]] $Presets, + [string[]] $Preset, + [Alias('Configurations')] + [SupportsWildcards()] [Parameter(Position = 1)] - [string[]] $Configurations = @($null), + [string[]] $Configuration, + [Alias('Targets')] [Parameter(Position = 2)] - [string[]] $Targets, + [string[]] $Target, [Parameter()] [switch] $Configure, @@ -256,27 +349,31 @@ function Build-CMakeBuild { ) $CMakeRoot = FindCMakeRoot $CMakePresetsJson = GetCMakePresets - $PresetNames = GetBuildPresetNames $CMakePresetsJson - - if (-not $Presets) { - if (-not $PresetNames) { + $BuildPresetNames = GetBuildPresetNames $CMakePresetsJson + $BuildPresetNames = if (-not $Preset) { + if (-not $BuildPresetNames) { Write-Error "No Presets values specified, and one could not be inferred." } - $Presets = $PresetNames | Select-Object -First 1 + $BuildPresetNames | Select-Object -First 1 + } else { + foreach ($CandidatePreset in $Preset) { + $ExpandedPresets = $BuildPresetNames | Where-Object { $_ -like $CandidatePreset } + $ExpandedPresets ?? $CandidatePreset + } } # If; # * no targets were specified, and # * the current location is different from the cmake root # Then we're a scoped build! - $ScopedBuild = (-not $Targets) -and ($CMakeRoot -ne ((Get-Location).Path)) + $ScopedBuild = (-not $Target) -and ($CMakeRoot -ne ((Get-Location).Path)) $ScopeLocation = (Get-Location).Path $CMake = GetCMake Using-Location $CMakeRoot { - foreach ($Preset in $Presets) { - Write-Output "Preset : $Preset" + foreach ($BuildPresetName in $BuildPresetNames) { + Write-Output "Preset : $BuildPresetName" - $BuildPreset, $ConfigurePreset = ResolvePresets $CMakePresetsJson 'buildPresets' $Preset + $BuildPreset, $ConfigurePreset = ResolvePresets $CMakePresetsJson 'buildPresets' $BuildPresetName $BinaryDirectory = GetBinaryDirectory $CMakePresetsJson $ConfigurePreset $CMakeCacheFile = Join-Path -Path $BinaryDirectory -ChildPath 'CMakeCache.txt' @@ -285,42 +382,51 @@ function Build-CMakeBuild { # 2) '-fresh' was specified # 3) "$BinaryDirectory/CMakeCache.txt" doesn't exist # 4) The "Get-CMakeBuildCodeModelDirectory" folder doesn't exist + # 5) "Get-CMakeBuildCodeModel" returns $null if ($Configure -or $Fresh -or (-not (Test-Path -Path $CMakeCacheFile -PathType Leaf)) -or - (-not (Test-Path -Path (Get-CMakeBuildCodeModelDirectory $BinaryDirectory) -PathType Container))) { - ConfigureCMake $CMake $CMakePresetsJson $ConfigurePreset -Fresh:$Fresh + (-not (Test-Path -Path (Get-CMakeBuildCodeModelDirectory $BinaryDirectory) -PathType Container)) -or + (-not ($CodeModel = Get-CMakeBuildCodeModel $BinaryDirectory)) + ) { + ConfigureCMake -CMake $CMake $CMakePresetsJson $ConfigurePreset -Fresh:$Fresh + $CodeModel = Get-CMakeBuildCodeModel $BinaryDirectory } - $CodeModel = Get-CMakeBuildCodeModel $BinaryDirectory + [string[]] $ConfigurationNames = @($null) + if ($Configuration) { + $ConfigurationNames = foreach ($CandidateConfigurationName in $Configuration) { + $ExpandedName = $CodeModel.configurations.name | Where-Object { $_ -like $CandidateConfigurationName } + $ExpandedName ?? $CandidateConfigurationName + } + } - foreach ($Configuration in $Configurations) { - Write-Output "Configuration : $Configuration" + foreach ($ConfigurationName in $ConfigurationNames) { + Write-Output "Configuration : $ConfigurationName" - if ($ScopedBuild) { - $TargetTuples = GetScopedTargets $CodeModel $Configuration $ScopeLocation - $Targets = if ($TargetTuples) { + $TargetNames = if ($ScopedBuild) { + $TargetTuples = GetScopedTargets $CodeModel $ConfigurationName $ScopeLocation + if ($TargetTuples) { $TargetTuples.name - } else { - @() } - Write-Output "Scoped Targets : $Targets" + } else { + $Target } $CMakeArguments = @( '--build' - '--preset', $Preset + '--preset', $BuildPresetName + if ($ConfigurationName) { + '--config', $ConfigurationName + } + if ($TargetNames) { + '--target' + $TargetNames + } ) - if ($Targets) { - $CMakeArguments += '--target' - $CMakeArguments += $Targets - } - - Write-Verbose "CMake Arguments: $CMakeArguments" - $StartTime = [datetime]::Now - & $CMake @CMakeArguments (($Configuration)?('--config', $Configuration):$null) + InvokeExecutable $CMake $CMakeArguments if ($LASTEXITCODE -ne 0) { Write-Error "Build failed. Command line: '$($CMake.Source)' $($CMakeArguments -join ' ')" } @@ -372,33 +478,32 @@ function Write-CMakeBuild { } <# - .Synopsis - Runs the output of a CMake build. - - .Description - `Invoke-CMakeOutput` runs the output of a CMake build. A {preset,configuration,target} can be specified, and `Invoke-CMakeOutput` - will build the target, use the CMake code-model to discover the path to the generated executable and run it, passing any - extra parameter specified. If `Invoke-CMakeOutput` is run from a folder that only contains a single executable target, - then that target will be built and run. + .Synopsis + Runs the output of a CMake build. - .Parameter Preset - The CMake preset to use. If none is specified, then the first valid preset from CMakePresets.json is used. + .Description + `Invoke-CMakeOutput` runs the output of a CMake build. A {preset,configuration,target} can be specified, and `Invoke-CMakeOutput` + will build the target, use the CMake code-model to discover the path to the generated executable and run it, passing any + extra parameter specified. If `Invoke-CMakeOutput` is run from a folder that only contains a single executable target, + then that target will be built and run. - .Parameter Configuration - The CMake configuration to use. If none is specified, then the first valid configuration is used. + .Parameter Preset + The CMake preset to use. If none is specified, then the first valid preset from CMakePresets.json is used. - .Parameter Target - The CMake target that produces an executable to run. + .Parameter Configuration + The CMake configuration to use. If none is specified, then the first valid configuration is used. - .Parameter SkipBuild - If specified, the build will be skipped, otherwise a build will be run before invoking the output. + .Parameter Target + The CMake target that produces an executable to run. - .Parameter Arguments - All other parameters will be passed to the discovered executable. + .Parameter SkipBuild + If specified, the build will be skipped, otherwise a build will be run before invoking the output. + .Parameter Arguments + All other parameters will be passed to the discovered executable. #> function Invoke-CMakeOutput { - [CmdletBinding(PositionalBinding=$false)] + [CmdletBinding(PositionalBinding = $false)] param( [Parameter()] [string] $Preset, @@ -432,7 +537,7 @@ function Invoke-CMakeOutput { # $CodeModel = Get-CMakeBuildCodeModel $BinaryDirectory if (-not $CodeModel) { - Configure-CMakeBuild -Presets $Preset + Configure-CMakeBuild -Preset $Preset $CodeModel = Get-CMakeBuildCodeModel $BinaryDirectory } @@ -446,19 +551,8 @@ function Invoke-CMakeOutput { GetScopedTargets $CodeModel $Configuration $ScopeLocation } - # For the 'code model' JSON that was found, load the full 'target' JSON to be able to find 'EXECUTABLE' targets. - # - $TargetTuples = $TargetTuplesCodeModel | - ForEach-Object { - Join-Path -Path (Get-CMakeBuildCodeModelDirectory $BinaryDirectory) -ChildPath $_.jsonFile | - Get-Item | - Get-Content | - ConvertFrom-Json - } - $ExecutableTargetTuples = $TargetTuples | - Where-Object { - $_.type -eq 'EXECUTABLE' - } + # Use the 'code model' JSON to load the target-specific JSON to filter to targets with 'type' equal to 'EXECUTABLE' + $ExecutableTargetTuples = FilterExecutableTargets (Get-CMakeBuildCodeModelDirectory $BinaryDirectory) $TargetTuplesCodeModel $Count = ($ExecutableTargetTuples | Measure-Object).Count if ($Count -eq 0) { Write-Error "No executable target in scope." @@ -485,18 +579,18 @@ function Invoke-CMakeOutput { Write-Output "Running: $TargetPath $Arguments" Write-Output '----' - & $TargetPath @Arguments + InvokeExecutable $TargetPath $Arguments } Register-ArgumentCompleter -CommandName Invoke-CMakeOutput -ParameterName Preset -ScriptBlock $function:BuildPresetsCompleter Register-ArgumentCompleter -CommandName Invoke-CMakeOutput -ParameterName Configuration -ScriptBlock $function:BuildConfigurationsCompleter -Register-ArgumentCompleter -CommandName Invoke-CMakeOutput -ParameterName Target -ScriptBlock $function:BuildTargetsCompleter +Register-ArgumentCompleter -CommandName Invoke-CMakeOutput -ParameterName Target -ScriptBlock $function:ExecutableTargetsCompleter -Register-ArgumentCompleter -CommandName Build-CMakeBuild -ParameterName Presets -ScriptBlock $function:BuildPresetsCompleter -Register-ArgumentCompleter -CommandName Build-CMakeBuild -ParameterName Configurations -ScriptBlock $function:BuildConfigurationsCompleter -Register-ArgumentCompleter -CommandName Build-CMakeBuild -ParameterName Targets -ScriptBlock $function:BuildTargetsCompleter +Register-ArgumentCompleter -CommandName Build-CMakeBuild -ParameterName Preset -ScriptBlock $function:BuildPresetsCompleter +Register-ArgumentCompleter -CommandName Build-CMakeBuild -ParameterName Configuration -ScriptBlock $function:BuildConfigurationsCompleter +Register-ArgumentCompleter -CommandName Build-CMakeBuild -ParameterName Target -ScriptBlock $function:BuildTargetsCompleter -Register-ArgumentCompleter -CommandName Configure-CMakeBuild -ParameterName Presets -ScriptBlock $function:ConfigurePresetsCompleter +Register-ArgumentCompleter -CommandName Configure-CMakeBuild -ParameterName Preset -ScriptBlock $function:ConfigurePresetsCompleter Register-ArgumentCompleter -CommandName Write-CMakeBuild -ParameterName Preset -ScriptBlock $function:BuildPresetsCompleter Register-ArgumentCompleter -CommandName Write-CMakeBuild -ParameterName Configuration -ScriptBlock $function:BuildConfigurationsCompleter diff --git a/README.md b/README.md index b69070a..34fecab 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ provide a declaration - through a JSON file - of the available builds, where the sorts of useful metadata that makes it easier to build standard tooling around the build. This PowerShell module is one attempt at such tooling. -The PSCMake PowerShell module supports; +The PSCMake PowerShell module supports: - 1. configuring and building a CMake build, - 2. generating a 'DGML'- or 'DOT'- file for a build, - 3. invoking executable CMake build outputs. +1. configuring and building a CMake build, +2. generating a 'DGML'- or 'DOT'- file for a build, +3. invoking executable CMake build outputs. By leveraging the CMake file API, the module is able to offer tab-completion for presets, configurations and targets, and can implicitly scope the build based on the current working directory. @@ -37,7 +37,7 @@ The module provides the following commands: 3. `Write-CMakeBuild` - To output the CMake build as a DOT or DGML graph. 4. `Invoke-CMakeOutput` - To run an executable output from a CMake build, by target name or implicitly by scope. -Running `Build-CMakeBuild` by itself would run the first `buildConfiguration`. Run either command with `-?` to get more +Running `Build-CMakeBuild` by itself would run the first `buildConfiguration`. Run any command with `-?` to get more details. [cmake-presets]: "CMake Presets" diff --git a/Tests/Build-CMakeBuild.Tests.ps1 b/Tests/Build-CMakeBuild.Tests.ps1 index e577244..8dcb341 100644 --- a/Tests/Build-CMakeBuild.Tests.ps1 +++ b/Tests/Build-CMakeBuild.Tests.ps1 @@ -7,18 +7,112 @@ BeforeAll { . $PSScriptRoot/TestUtilities.ps1 . $PSScriptRoot/ReferenceBuild.ps1 + # Configure the reference build so that there is reference content to work with. $Properties = PrepareReferenceBuild - $CMake = "$env:ProgramFiles/CMake/bin/cmake.exe" & $CMake @Properties Import-Module -Force $PSScriptRoot/../PSCMake/PSCMake.psd1 -DisableNameChecking + + # Mock subsequent calls to invoke CMake so that we don't actually try to build anything. + $script:CMakeCalls = @() + Mock -ModuleName PSCMake InvokeExecutable { + param( + [string] $CMakePath, + [string[]] $Arguments + ) + $null = $CMakePath + $script:CMakeCalls += , $Arguments + } } Describe 'Build-CMakeBuild' { + BeforeEach { + $script:CMakeCalls = @() + } + It 'Builds with no parameters' { Using-Location "$PSScriptRoot/ReferenceBuild" { Build-CMakeBuild } + + $script:CMakeCalls | Should -HaveCount 1 + $script:CMakeCalls[0] | Should -Be @('--build', '--preset', 'windows-x64') + } + + It 'Builds with a single preset' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + Build-CMakeBuild -Preset windows-x64 + } + + $script:CMakeCalls | Should -HaveCount 1 + $script:CMakeCalls[0] | Should -Be @('--build', '--preset', 'windows-x64') + } + + It 'Builds a specified target' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + Build-CMakeBuild -Preset windows-x64 -Target B_Library + } + + $script:CMakeCalls | Should -HaveCount 1 + $script:CMakeCalls[0] | Should -Be @('--build', '--preset', 'windows-x64', '--target', 'B_Library') + } + + It 'Builds a multiple targets' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + Build-CMakeBuild -Preset windows-x64 -Target A_Library,B_Library + } + + $script:CMakeCalls | Should -HaveCount 1 + $script:CMakeCalls[0] | Should -Be @('--build', '--preset', 'windows-x64', '--target', 'A_Library', 'B_Library') + } + + It 'Reruns configuration with -Configure' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + Build-CMakeBuild -Preset windows-x64 -Configure + } + + $script:CMakeCalls | Should -HaveCount 2 + $script:CMakeCalls[0] | Should -Be @('--preset', 'windows-x64') + $script:CMakeCalls[1] | Should -Be @('--build', '--preset', 'windows-x64') + } + + It 'Reruns configuration with -Fresh' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + Build-CMakeBuild -Preset windows-x64 -Fresh + } + + $script:CMakeCalls | Should -HaveCount 2 + $script:CMakeCalls[0] | Should -Be @('--preset', 'windows-x64', '--fresh') + $script:CMakeCalls[1] | Should -Be @('--build', '--preset', 'windows-x64') + } + + It 'Builds with wildcard presets' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + Build-CMakeBuild -Preset '*-x64' + } + + $CMakeCalls | Should -HaveCount 1 + $script:CMakeCalls[0] | Should -Be @('--build', '--preset', 'windows-x64') + } + + It 'Builds with wildcard configurations' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + Build-CMakeBuild -Preset 'windows-x64' -Configuration * + } + + $CMakeCalls | Should -HaveCount 3 + $script:CMakeCalls[0] | Should -Be @('--build', '--preset', 'windows-x64', '--config', 'Debug') + $script:CMakeCalls[1] | Should -Be @('--build', '--preset', 'windows-x64', '--config', 'Release') + $script:CMakeCalls[2] | Should -Be @('--build', '--preset', 'windows-x64', '--config', 'RelWithDebInfo') + } + + It 'Builds scoped targets' { + Using-Location "$PSScriptRoot/ReferenceBuild/SubDirectory" { + Build-CMakeBuild -Preset 'windows-x64' + } + + $CMakeCalls | Should -HaveCount 1 + $script:CMakeCalls[0] | Should -Be @('--build', '--preset', 'windows-x64', '--target', 'SubDirectory_Executable', 'SubDirectory_Library') } } diff --git a/Tests/BuildConfigurationsCompleter.Tests.ps1 b/Tests/BuildConfigurationsCompleter.Tests.ps1 index 0796e24..4f7fae0 100644 --- a/Tests/BuildConfigurationsCompleter.Tests.ps1 +++ b/Tests/BuildConfigurationsCompleter.Tests.ps1 @@ -11,8 +11,8 @@ BeforeAll { Describe 'BuildConfigurationsCompleter' { It 'Returns the default configurations when no preset is specified' { - $Completions = Get-CommandCompletions "Build-CMakeBuild -Configurations " - $Completions.CompletionMatches.Count | Should -Be 4 + $Completions = Get-CommandCompletion "Build-CMakeBuild -Configuration " + $Completions.CompletionMatches | Should -HaveCount 4 $Completions.CompletionMatches[0].CompletionText | Should -Be 'Release' $Completions.CompletionMatches[1].CompletionText | Should -Be 'Debug' $Completions.CompletionMatches[2].CompletionText | Should -Be 'RelWithDebInfo' @@ -20,8 +20,8 @@ Describe 'BuildConfigurationsCompleter' { } It 'Returns the default configurations when no preset is specified, filtered by the word to complete' { - $Completions = Get-CommandCompletions "Build-CMakeBuild -Configurations D" - $Completions.CompletionMatches.Count | Should -Be 1 + $Completions = Get-CommandCompletion "Build-CMakeBuild -Configuration D" + $Completions.CompletionMatches | Should -HaveCount 1 $Completions.CompletionMatches[0].CompletionText | Should -Be 'Debug' } } diff --git a/Tests/BuildPresetsCompleter.Tests.ps1 b/Tests/BuildPresetsCompleter.Tests.ps1 index 5a80768..5b1909c 100644 --- a/Tests/BuildPresetsCompleter.Tests.ps1 +++ b/Tests/BuildPresetsCompleter.Tests.ps1 @@ -11,15 +11,15 @@ BeforeAll { Describe 'BuildPresetsCompleter' { It 'Returns the presets from the discovered presets file, in the order that they are defined' { - $Completions = Get-CommandCompletions "Build-CMakeBuild -Preset " - $Completions.CompletionMatches.Count | Should -Be 2 + $Completions = Get-CommandCompletion "Build-CMakeBuild -Preset " + $Completions.CompletionMatches | Should -HaveCount 2 $Completions.CompletionMatches[0].CompletionText | Should -Be 'windows-x64' $Completions.CompletionMatches[1].CompletionText | Should -Be 'windows-arm' } It 'Returns the presets from the discovered presets file, filtered by the word to complete' { - $Completions = Get-CommandCompletions "Build-CMakeBuild -Preset windows-a" - $Completions.CompletionMatches.Count | Should -Be 1 + $Completions = Get-CommandCompletion "Build-CMakeBuild -Preset windows-a" + $Completions.CompletionMatches | Should -HaveCount 1 $Completions.CompletionMatches[0].CompletionText | Should -Be 'windows-arm' } } diff --git a/Tests/BuildTargetsCompleter.Tests.ps1 b/Tests/BuildTargetsCompleter.Tests.ps1 index 7ab50e5..779b819 100644 --- a/Tests/BuildTargetsCompleter.Tests.ps1 +++ b/Tests/BuildTargetsCompleter.Tests.ps1 @@ -18,12 +18,21 @@ BeforeAll { Describe 'BuildTargetsCompleter' { It 'Returns the targets of the default preset, default configuration when neither is specified' { Using-Location "$PSScriptRoot/ReferenceBuild" { - $Completions = Get-CommandCompletions "Build-CMakeBuild -Targets " + $Completions = Get-CommandCompletion "Build-CMakeBuild -Targets " - $Completions.CompletionMatches.Count | Should -Be 3 - $Completions.CompletionMatches[0].CompletionText | Should -Be 'A_Library' - $Completions.CompletionMatches[1].CompletionText | Should -Be 'B_Library' - $Completions.CompletionMatches[2].CompletionText | Should -Be 'C_Library' + $Completions.CompletionMatches | Should -HaveCount 10 + $Completions.CompletionMatches.CompletionText | Should -Be @( + 'A_Library' + 'B_Library' + 'C_Library' + 'SubDirectoryOther_Executable' + 'SubDirectoryOther_Library' + 'SubDirectory_Executable' + 'SubDirectory_Library' + 'all' + 'clean' + 'install' + ) } } } diff --git a/Tests/Configure-CMakeBuild.Tests.ps1 b/Tests/Configure-CMakeBuild.Tests.ps1 index c350566..a74ad90 100644 --- a/Tests/Configure-CMakeBuild.Tests.ps1 +++ b/Tests/Configure-CMakeBuild.Tests.ps1 @@ -13,12 +13,83 @@ BeforeAll { & $CMake @Properties Import-Module -Force $PSScriptRoot/../PSCMake/PSCMake.psd1 -DisableNameChecking + + # Mock subsequent calls to invoke CMake so that we don't actually try to build anything. + Mock -ModuleName PSCMake InvokeExecutable { + param( + [string] $CMakePath, + [string[]] $Arguments + ) + $null = $CMakePath + $script:CMakeCalls += , $Arguments + } } Describe 'Configure-CMakeBuild' { + BeforeEach { + $script:CMakeCalls = @() + } + It 'Configures the build with no parameters' { Using-Location "$PSScriptRoot/ReferenceBuild" { Configure-CMakeBuild + + $script:CMakeCalls | Should -HaveCount 1 + $script:CMakeCalls[0] | Should -Be @('--preset', 'windows-x64') + } + } + + It 'Configures with --fresh when specified' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + Configure-CMakeBuild -Fresh + + $script:CMakeCalls | Should -HaveCount 1 + $script:CMakeCalls[0] | Should -Be @('--preset', 'windows-x64', '--fresh') + } + } + + It 'Configures with --verbose when specified' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + Configure-CMakeBuild -Fresh -Verbose + + $script:CMakeCalls | Should -HaveCount 1 + $script:CMakeCalls[0] | Should -Be @('--preset', 'windows-x64', '--fresh', '--log-level=VERBOSE') + } + } + + It 'Configures with a specific preset' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + Configure-CMakeBuild -Preset windows-arm + + $script:CMakeCalls | Should -HaveCount 1 + $script:CMakeCalls[0] | Should -Be @('--preset', 'windows-arm') + } + } + + It 'Configures with a multiple presets' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + Configure-CMakeBuild -Preset windows-arm,windows-x64 + + $script:CMakeCalls | Should -HaveCount 2 + $script:CMakeCalls[0] | Should -Be @('--preset', 'windows-arm') + $script:CMakeCalls[1] | Should -Be @('--preset', 'windows-x64') + } + } + + It 'Configures with a wildcard presets' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + Configure-CMakeBuild -Preset windows-* + + $script:CMakeCalls | Should -HaveCount 2 + $script:CMakeCalls[0] | Should -Be @('--preset', 'windows-x64') + $script:CMakeCalls[1] | Should -Be @('--preset', 'windows-arm') + } + } + + It 'Reports invalid presets' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + { Configure-CMakeBuild -Preset linux-x64 } | + Should -Throw "Unable to find configuration preset 'linux-x64' in $PSScriptRoot\ReferenceBuild\CMakePresets.json" } } } diff --git a/Tests/EvaluateCondition.Tests.ps1 b/Tests/EvaluateCondition.Tests.ps1 index 45327b6..4f60401 100644 --- a/Tests/EvaluateCondition.Tests.ps1 +++ b/Tests/EvaluateCondition.Tests.ps1 @@ -4,9 +4,9 @@ BeforeAll { . $PSScriptRoot/../PSCMake/Common/CMake.ps1 Mock GetMacroConstants { @{ - '${hostSystemName}'='Linux' - '$vendor{PSCMake}'='true' - } } + '${hostSystemName}' = 'Linux' + '$vendor{PSCMake}' = 'true' + } } } Describe 'EvaluateCondition' { diff --git a/Tests/ExecutableTargetsCompleter.Tests.ps1 b/Tests/ExecutableTargetsCompleter.Tests.ps1 new file mode 100644 index 0000000..1950d66 --- /dev/null +++ b/Tests/ExecutableTargetsCompleter.Tests.ps1 @@ -0,0 +1,25 @@ +#Requires -PSEdition Core + +BeforeAll { + . $PSScriptRoot/TestUtilities.ps1 + . $PSScriptRoot/ReferenceBuild.ps1 + + $Properties = PrepareReferenceBuild + + $CMake = "$env:ProgramFiles/CMake/bin/cmake.exe" + & $CMake @Properties + + Import-Module -Force $PSScriptRoot/../PSCMake/PSCMake.psd1 -DisableNameChecking +} + +Describe 'ExecutableTargetsCompleter' { + It 'Returns the executable targets' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + $Completions = Get-CommandCompletion "Invoke-CMakeOutput -Target " + $Completions.CompletionMatches.CompletionText | Should -Be @( + 'SubDirectoryOther_Executable', + 'SubDirectory_Executable' + ) + } + } +} diff --git a/Tests/Get-MemberValue.Tests.ps1 b/Tests/Get-MemberValue.Tests.ps1 index ff767e1..3dd189e 100644 --- a/Tests/Get-MemberValue.Tests.ps1 +++ b/Tests/Get-MemberValue.Tests.ps1 @@ -3,24 +3,24 @@ BeforeAll { . $PSScriptRoot/../PSCMake/Common/Common.ps1 - $TestObject = [PSCustomObject]@{ + $script:TestObject = [PSCustomObject]@{ Breakfast = 'Chunky Bacon' } } Describe 'Get-MemberValue' { It 'Returns the member value when available' { - Get-MemberValue -InputObject $TestObject -Name Breakfast -Or Cereal | + Get-MemberValue -InputObject $script:TestObject -Name Breakfast -Or Cereal | Should -Be 'Chunky Bacon' } It 'Returns null when the member value is not available' { - Get-MemberValue -InputObject $TestObject -Name Lunch | + Get-MemberValue -InputObject $script:TestObject -Name Lunch | Should -BeNullOrEmpty } It 'Returns the -Or value when the member value is not available' { - Get-MemberValue -InputObject $TestObject -Name Lunch -Or Sandwich | + Get-MemberValue -InputObject $script:TestObject -Name Lunch -Or Sandwich | Should -Be Sandwich } } diff --git a/Tests/GetConfigurePresetNames.Tests.ps1 b/Tests/GetConfigurePresetNames.Tests.ps1 index a15274a..8a99252 100644 --- a/Tests/GetConfigurePresetNames.Tests.ps1 +++ b/Tests/GetConfigurePresetNames.Tests.ps1 @@ -4,9 +4,9 @@ BeforeAll { . $PSScriptRoot/../PSCMake/Common/CMake.ps1 Mock GetMacroConstants { @{ - '${hostSystemName}'='Linux' - '$vendor{PSCMake}'='true' - } } + '${hostSystemName}' = 'Linux' + '$vendor{PSCMake}' = 'true' + } } } Describe 'GetConfigurePresetNames' { diff --git a/Tests/GetMacroConstants.Tests.ps1 b/Tests/GetMacroConstants.Tests.ps1 new file mode 100644 index 0000000..6ddb5cd --- /dev/null +++ b/Tests/GetMacroConstants.Tests.ps1 @@ -0,0 +1,26 @@ +#Requires -PSEdition Core + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +BeforeAll { + . $PSScriptRoot/../PSCMake/Common/CMake.ps1 +} + +Describe 'GetMacroConstants' { + It 'Returns the correct macro constants' { + $HostSystemName = if ($IsWindows) { + 'Windows' + } elseif ($IsMacOS) { + 'Darwin' + } elseif ($IsLinux) { + 'Linux' + } else { + Write-Error "Unsupported `${hostSystemName} value." + } + + $MacroConstants = GetMacroConstants + $MacroConstants['${hostSystemName}'] | Should -Be $HostSystemName + $MacroConstants['$vendor{PSCMake}'] | Should -Be 'true' + } +} diff --git a/Tests/GetScopedTargets.Tests.ps1 b/Tests/GetScopedTargets.Tests.ps1 new file mode 100644 index 0000000..4c5358e --- /dev/null +++ b/Tests/GetScopedTargets.Tests.ps1 @@ -0,0 +1,57 @@ +#Requires -PSEdition Core + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +BeforeAll { + . $PSScriptRoot/TestUtilities.ps1 + . $PSScriptRoot/ReferenceBuild.ps1 + + $Properties = PrepareReferenceBuild + + $CMake = "$env:ProgramFiles/CMake/bin/cmake.exe" + & $CMake @Properties + + . $PSScriptRoot/../PSCMake/Common/CMake.ps1 + + $ReferenceBuildProperties = GetReferenceBuildProperties + $CodeModel = Get-CMakeBuildCodeModel $ReferenceBuildProperties.BinaryDirectory + $script:SourceLocation = $CodeModel.paths.source +} + +Describe 'GetScopedTargets' { + It 'Returns all targets when the ScopeLocation is the SourceLocation' { + $ScopeLocation = $SourceLocation + $Targets = GetScopedTargets $CodeModel $null $ScopeLocation + $Targets.name | + Should -Be @( + 'A_Library' + 'B_Library' + 'C_Library' + 'SubDirectoryOther_Executable' + 'SubDirectoryOther_Library' + 'SubDirectory_Executable' + 'SubDirectory_Library' + ) + } + + It 'Returns scoped targets when the ScopeLocation is a subdirectory of the SourceLocation' { + $ScopeLocation = Join-Path -Path $SourceLocation -ChildPath 'SubDirectoryOther' + $Targets = GetScopedTargets $CodeModel $null $ScopeLocation + $Targets.name | + Should -Be @( + 'SubDirectoryOther_Executable' + 'SubDirectoryOther_Library' + ) + } + + It 'Returns scoped targets when the ScopeLocation is a subdirectory of the SourceLocation, that is also a prefix of another subdirectory' { + $ScopeLocation = Join-Path -Path $SourceLocation -ChildPath 'SubDirectory' + $Targets = GetScopedTargets $CodeModel $null $ScopeLocation + $Targets.name | + Should -Be @( + 'SubDirectory_Executable' + 'SubDirectory_Library' + ) + } +} diff --git a/Tests/Invoke-CMakeOutput.Tests.ps1 b/Tests/Invoke-CMakeOutput.Tests.ps1 new file mode 100644 index 0000000..c4c3b22 --- /dev/null +++ b/Tests/Invoke-CMakeOutput.Tests.ps1 @@ -0,0 +1,81 @@ +#Requires -PSEdition Core + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +BeforeAll { + . $PSScriptRoot/TestUtilities.ps1 + . $PSScriptRoot/ReferenceBuild.ps1 + + $Properties = PrepareReferenceBuild + + $CMake = "$env:ProgramFiles/CMake/bin/cmake.exe" + & $CMake @Properties + + Import-Module -Force $PSScriptRoot/../PSCMake/PSCMake.psd1 -DisableNameChecking + + # Mock subsequent calls to invoke CMake so that we don't actually try to build anything. + Mock -ModuleName PSCMake InvokeExecutable { + param( + [string] $Path, + [string[]] $Arguments + ) + $script:ExecutableCalls += [PSCustomObject]@{Path = $Path; Arguments = $Arguments} + } +} + +Describe 'Invoke-CMakeOutput' { + BeforeEach { + $script:ExecutableCalls = @() + } + + It 'Builds then invokes a specified target' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + Invoke-CMakeOutput -Target SubDirectory_Executable + + $script:ExecutableCalls | Should -HaveCount 2 + $script:ExecutableCalls[0].Arguments | Should -Be @('--build', '--preset', 'windows-x64', '--target', 'SubDirectory_Executable') + $script:ExecutableCalls[1].Arguments | Should -BeNullOrEmpty + $script:ExecutableCalls[1].Path | Should -Be @("$PSScriptRoot\ReferenceBuild\__output\windows-x64\SubDirectory\Debug\SubDirectory_Executable.exe") + } + } + + It 'Builds then invokes a specified target - configuration specified' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + Invoke-CMakeOutput -Target SubDirectory_Executable -Configuration Release + + $script:ExecutableCalls | Should -HaveCount 2 + $script:ExecutableCalls[0].Arguments | Should -Be @('--build', '--preset', 'windows-x64', '--config', 'Release', '--target', 'SubDirectory_Executable') + $script:ExecutableCalls[1].Arguments | Should -BeNullOrEmpty + $script:ExecutableCalls[1].Path | Should -Be @("$PSScriptRoot\ReferenceBuild\__output\windows-x64\SubDirectory\Release\SubDirectory_Executable.exe") + } + } + + It 'Passes extra parameters to the invocation' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + Invoke-CMakeOutput -Target SubDirectory_Executable chunky bacon + + $script:ExecutableCalls | Should -HaveCount 2 + $script:ExecutableCalls[0].Arguments | Should -Be @('--build', '--preset', 'windows-x64', '--target', 'SubDirectory_Executable') + $script:ExecutableCalls[1].Arguments | Should -Be @('chunky', 'bacon') + $script:ExecutableCalls[1].Path | Should -Be @("$PSScriptRoot\ReferenceBuild\__output\windows-x64\SubDirectory\Debug\SubDirectory_Executable.exe") + } + } + + It 'Runs a single in-scope executable' { + Using-Location "$PSScriptRoot/ReferenceBuild/SubDirectory" { + Invoke-CMakeOutput + + $script:ExecutableCalls | Should -HaveCount 2 + $script:ExecutableCalls[0].Arguments | Should -Be @('--build', '--preset', 'windows-x64', '--target', 'SubDirectory_Executable') + $script:ExecutableCalls[1].Arguments | Should -BeNullOrEmpty + $script:ExecutableCalls[1].Path | Should -Be @("$PSScriptRoot\ReferenceBuild\__output\windows-x64\SubDirectory\Debug\SubDirectory_Executable.exe") + } + } + + It 'Fails with multiple executable targets in scope' { + Using-Location "$PSScriptRoot/ReferenceBuild" { + { Invoke-CMakeOutput } | Should -Throw "Multiple executable scoped targets match. Specify a target explicitly: SubDirectoryOther_Executable SubDirectory_Executable" + } + } +} diff --git a/Tests/MacroReplacement.Tests.ps1 b/Tests/MacroReplacement.Tests.ps1 index f6c62a6..332641f 100644 --- a/Tests/MacroReplacement.Tests.ps1 +++ b/Tests/MacroReplacement.Tests.ps1 @@ -6,15 +6,15 @@ BeforeAll { $env:PSCMAKE_ENV_TEST = 43 Mock GetMacroConstants { @{ - '${hostSystemName}'='Linux' - '$vendor{PSCMake}'='true' - } } + '${hostSystemName}' = 'Linux' + '$vendor{PSCMake}' = 'true' + } } Mock GetCMakePresetsPath { 'C:\chunky\bacon\CMakePresets.json' } - $PresetJson = ConvertFrom-Json -InputObject @' + $script:PresetJson = ConvertFrom-Json -InputObject @' { "name": "windows-x64", "configurePreset": "windows-x64" diff --git a/Tests/ReferenceBuild.ps1 b/Tests/ReferenceBuild.ps1 index 0dd8f5b..cb32a20 100644 --- a/Tests/ReferenceBuild.ps1 +++ b/Tests/ReferenceBuild.ps1 @@ -41,3 +41,13 @@ function PrepareReferenceBuild() { "-DCMAKE_MAKE_PROGRAM=$CMAKE_MAKE_PROGRAM" ) } + +function GetReferenceBuildProperties() { + $BinaryDirectory = "$PSScriptRoot/ReferenceBuild/__output/windows-x64" + + [PSCustomObject]@{ + BinaryDirectory = $BinaryDirectory + CodeModelFile = Get-CMakeBuildCodeModel $BinaryDirectory + SourceDirectory = (Resolve-Path -Path "$PSScriptRoot/ReferenceBuild/").Path + } +} diff --git a/Tests/ReferenceBuild/CMakeLists.txt b/Tests/ReferenceBuild/CMakeLists.txt index de4bf11..f737040 100644 --- a/Tests/ReferenceBuild/CMakeLists.txt +++ b/Tests/ReferenceBuild/CMakeLists.txt @@ -16,3 +16,6 @@ add_library(B_Library add_library(C_Library Reference.cpp ) + +add_subdirectory(SubDirectory) +add_subdirectory(SubDirectoryOther) \ No newline at end of file diff --git a/Tests/ReferenceBuild/SubDirectory/CMakeLists.txt b/Tests/ReferenceBuild/SubDirectory/CMakeLists.txt new file mode 100644 index 0000000..12ace25 --- /dev/null +++ b/Tests/ReferenceBuild/SubDirectory/CMakeLists.txt @@ -0,0 +1,7 @@ +add_executable(SubDirectory_Executable + ../Reference.cpp +) + +add_library(SubDirectory_Library + ../Reference.cpp +) diff --git a/Tests/ReferenceBuild/SubDirectoryOther/CMakeLists.txt b/Tests/ReferenceBuild/SubDirectoryOther/CMakeLists.txt new file mode 100644 index 0000000..473e828 --- /dev/null +++ b/Tests/ReferenceBuild/SubDirectoryOther/CMakeLists.txt @@ -0,0 +1,7 @@ +add_executable(SubDirectoryOther_Executable + ../Reference.cpp +) + +add_library(SubDirectoryOther_Library + ../Reference.cpp +) diff --git a/Tests/ResolvePresetProperty.Tests.ps1 b/Tests/ResolvePresetProperty.Tests.ps1 index 92be33b..4d82d3c 100644 --- a/Tests/ResolvePresetProperty.Tests.ps1 +++ b/Tests/ResolvePresetProperty.Tests.ps1 @@ -9,7 +9,7 @@ Describe 'ResolvePresetProperty' { $CMakePresetsJson = Get-Content "$PSScriptRoot/ReferencePresets/CMakePresets.Single.json" | ConvertFrom-Json $ConfigurePreset = $CMakePresetsJson.configurePresets[0] - $BinaryDirectory = ResolvePresetProperty $CMakePresetsJson $ConfigurePreset 'binaryDir' + $BinaryDirectory = ResolvePresetProperty $ConfigurePreset $CMakePresetsJson.configurePresets 'binaryDir' $BinaryDirectory | Should -Be '${sourceDir}/__output/${presetName}' } @@ -17,7 +17,7 @@ Describe 'ResolvePresetProperty' { $CMakePresetsJson = Get-Content "$PSScriptRoot/ReferencePresets/CMakePresets.Inherited.json" | ConvertFrom-Json $ConfigurePreset = $CMakePresetsJson.configurePresets | Where-Object { $_.name -eq 'windows-x64' } | Select-Object -First 1 - $BinaryDirectory = ResolvePresetProperty $CMakePresetsJson $ConfigurePreset 'binaryDir' + $BinaryDirectory = ResolvePresetProperty $ConfigurePreset $CMakePresetsJson.configurePresets 'binaryDir' $BinaryDirectory | Should -Be '${sourceDir}/__output/${presetName}' } } diff --git a/Tests/SearchAncestors.Tests.ps1 b/Tests/SearchAncestors.Tests.ps1 new file mode 100644 index 0000000..cf7e2e3 --- /dev/null +++ b/Tests/SearchAncestors.Tests.ps1 @@ -0,0 +1,91 @@ +#Requires -PSEdition Core + +BeforeAll { + $script:Presets = ConvertFrom-Json -InputObject @" +[ + { + "name": "Preset1" + }, + { + "name": "Preset2", + "inherits": "Preset1" + }, + { + "name": "Preset3" + }, + { + "name": "Preset4", + "inherits": ["Preset2", "Preset3"] + } +] +"@ + $script:Preset1 = $script:Presets | Where-Object { $_.name -eq 'Preset1' } | Select-Object -First 1 + $script:Preset2 = $script:Presets | Where-Object { $_.name -eq 'Preset2' } | Select-Object -First 1 + $script:Preset3 = $script:Presets | Where-Object { $_.name -eq 'Preset3' } | Select-Object -First 1 + $script:Preset4 = $script:Presets | Where-Object { $_.name -eq 'Preset4' } | Select-Object -First 1 + + . $PSScriptRoot/../PSCMake/Common/CMake.ps1 +} + +Describe 'SearchAncestors' { + It 'Returns $null when passed a $null preset' { + SearchAncestors $null $Presets { } | + Should -Be $null + } + + It 'Searches a single preset' { + $script:VisitedPresets = @() + SearchAncestors $Preset1 $Presets { + param($CurrentPreset) + $script:VisitedPresets += $CurrentPreset.name + } | + Should -Be $null + + $script:VisitedPresets | + Should -Be @('Preset1') + } + + It 'Searches a preset and its parent' { + $script:VisitedPresets = @() + SearchAncestors $Preset2 $Presets { + param($CurrentPreset) + $script:VisitedPresets += $CurrentPreset.name + } | + Should -Be $null + + $script:VisitedPresets | + Should -Be @('Preset2', 'Preset1') + } + + It 'Returns the result of a found preset' { + $Name = SearchAncestors $Preset2 $Presets { + param($CurrentPreset) + $CurrentPreset.name + } + $Name | Should -Be 'Preset2' + } + + It 'Stops searching when a result is found' { + $script:VisitedPresets = @() + $Name = SearchAncestors $Preset2 $Presets { + param($CurrentPreset) + $script:VisitedPresets += $CurrentPreset.name + $CurrentPreset.name + } + $Name | Should -Be 'Preset2' + $script:VisitedPresets | + Should -Be @('Preset2') + } + + It 'Searches a preset and its multiple parents' { + $script:VisitedPresets = @() + SearchAncestors $Preset4 $Presets { + param($CurrentPreset) + $script:VisitedPresets += $CurrentPreset.name + } | + Should -Be $null + + $script:VisitedPresets | + Should -Be @('Preset4', 'Preset2', 'Preset1', 'Preset3') + } +} diff --git a/Tests/TestUtilities.ps1 b/Tests/TestUtilities.ps1 index 4e367ab..4dc94c2 100644 --- a/Tests/TestUtilities.ps1 +++ b/Tests/TestUtilities.ps1 @@ -3,6 +3,28 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -function Get-CommandCompletions([string] $InputScript) { +function Get-CommandCompletion([string] $InputScript) { [System.Management.Automation.CommandCompletion]::CompleteInput($InputScript, $InputScript.Length, $null) } + +function Zip-Object { + $Objects = $args + + if ($Objects.Count -lt 2) { + throw "At least two collections must be specified" + } + + $First = $Objects[0] + $Objects | ForEach-Object { + if ($_.Count -ne $First.Count) { + throw "All collections must have the same number of elements" + } + } + for ($i = 0; $i -lt $First.Count; $i++) { + [array]$Current = @() + $Objects | ForEach-Object { + $Current += $_[$i] + } + (, $Current) + } +} diff --git a/Tests/Using-Location.Tests.ps1 b/Tests/Using-Location.Tests.ps1 index 2dbb424..4bc2d97 100644 --- a/Tests/Using-Location.Tests.ps1 +++ b/Tests/Using-Location.Tests.ps1 @@ -3,29 +3,33 @@ BeforeAll { . $PSScriptRoot/../PSCMake/Common/Common.ps1 - $OriginalLocation = Get-Location + $script:OriginalLocation = Get-Location $TestFolder = Join-Path -Path $PSScriptRoot -ChildPath '__test' $null = New-Item -Path $TestFolder -ItemType Directory -Force -ErrorAction SilentlyContinue } AfterAll { - Set-Location $OriginalLocation + Set-Location $script:OriginalLocation } Describe 'Using-Location' { It 'Navigates to the given location and back again afterwards.' { Set-Location $PSScriptRoot + Using-Location $TestFolder { Get-Location | Should -Be $TestFolder } + Get-Location | Should -Be $PSScriptRoot } It 'Restores the location when the scriptlet fails.' { Set-Location $PSScriptRoot - {Using-Location $TestFolder { Write-Error "Oh no!" } } | + + { Using-Location $TestFolder { Write-Error "Oh no!" } } | Should -Throw + Get-Location | Should -Be $PSScriptRoot } diff --git a/Tests/Write-CMakeBuild.Tests.ps1 b/Tests/Write-CMakeBuild.Tests.ps1 index b2fa6cd..00058f6 100644 --- a/Tests/Write-CMakeBuild.Tests.ps1 +++ b/Tests/Write-CMakeBuild.Tests.ps1 @@ -5,9 +5,11 @@ $ErrorActionPreference = 'Stop' BeforeAll { . $PSScriptRoot/TestUtilities.ps1 + . $PSScriptRoot/XmlUtilities.ps1 . $PSScriptRoot/ReferenceBuild.ps1 $Properties = PrepareReferenceBuild + $script:BuildProperties = GetReferenceBuildProperties $CMake = "$env:ProgramFiles/CMake/bin/cmake.exe" & $CMake @Properties @@ -17,16 +19,35 @@ BeforeAll { Describe 'Write-CMakeBuild' { It 'Writes the build with no parameters' { - Using-Location "$PSScriptRoot/ReferenceBuild" { + Using-Location $BuildProperties.SourceDirectory { $ExpectedDotFile = @' digraph CodeModel { "A_Library::@6890427a1f51a3e7e1df" [label="A_Library"] "B_Library::@6890427a1f51a3e7e1df" [label="B_Library"] "C_Library::@6890427a1f51a3e7e1df" [label="C_Library"] + "SubDirectoryOther_Executable::@01210d55993b56455dd6" [label="SubDirectoryOther_Executable"] + "SubDirectoryOther_Library::@01210d55993b56455dd6" [label="SubDirectoryOther_Library"] + "SubDirectory_Executable::@c68b9f6dab07fa391196" [label="SubDirectory_Executable"] + "SubDirectory_Library::@c68b9f6dab07fa391196" [label="SubDirectory_Library"] } '@ ((Write-CMakeBuild) -join '') | - Should -Be ($ExpectedDotFile -replace '\r\n','') + Should -Be ($ExpectedDotFile -replace '\r\n', '') + } + } + It 'Writes the DGML when specified' { + Using-Location $BuildProperties.SourceDirectory { + [xml]$ExpectedDgml = Get-Content "$PSScriptRoot/Write-CMakeBuild.dgml" + SortChildElements $ExpectedDgml.DirectedGraph.Links { $_.Target } + + [xml]$ActualDgml = Write-CMakeBuild -As Dgml + $ActualDgml.DirectedGraph.Nodes.Node | + Where-Object { Get-MemberValue $_ Definition } | + ForEach-Object { $_.Definition = $_.Definition.Replace($BuildProperties.SourceDirectory, "") } + SortChildElements $ActualDgml.DirectedGraph.Links { $_.Target } + + $ActualDgml.OuterXml | + Should -Be $ExpectedDgml.OuterXml } } } diff --git a/Tests/Write-CMakeBuild.dgml b/Tests/Write-CMakeBuild.dgml new file mode 100644 index 0000000..3603306 --- /dev/null +++ b/Tests/Write-CMakeBuild.dgml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/XmlUtilities.ps1 b/Tests/XmlUtilities.ps1 new file mode 100644 index 0000000..6d17017 --- /dev/null +++ b/Tests/XmlUtilities.ps1 @@ -0,0 +1,32 @@ +#Requires -PSEdition Core + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$TypeAccelerators = [psobject].Assembly.GetType("System.Management.Automation.TypeAccelerators") +$TypeAccelerators::Add("xmlelement", [System.Xml.XmlElement]) + +function RemoveChildElements([xmlelement] $XmlElement) { + $ChildNodes = $XmlElement.ChildNodes + for ($Index = 0; $Index -lt $ChildNodes.Count; ) { + $ChildXmlElement = $ChildNodes.Item($Index) + if ($ChildXmlElement.NodeType -eq 'Element') { + $XmlElement.RemoveChild($ChildXmlElement) + } else { + $Index++ + } + } +} + +function AddChildElements([xmlelement]$XmlElement, [array] $ChildElements) { + $ChildElements | + ForEach-Object { + $null = $XmlElement.AppendChild($_) + } +} + +function SortChildElements([xmlelement] $LinksElement, [scriptblock] $SortExpression) { + $LinkArray = RemoveChildElements $LinksElement + $LinkArray = $LinkArray | Sort-Object $SortExpression + AddChildElements $LinksElement $LinkArray +} diff --git a/Version b/Version index 899f24f..2774f85 100644 --- a/Version +++ b/Version @@ -1 +1 @@ -0.9.0 \ No newline at end of file +0.10.0 \ No newline at end of file