diff --git a/.build/tasks/WorkspaceDependencies.build.ps1 b/.build/tasks/WorkspaceDependencies.build.ps1 index fea88e83..8e1ab403 100644 --- a/.build/tasks/WorkspaceDependencies.build.ps1 +++ b/.build/tasks/WorkspaceDependencies.build.ps1 @@ -22,7 +22,15 @@ `$BuildRoot`. .PARAMETER BuiltModuleSubdirectory - The parent path of the module to be built. Defaults to `module`. + The parent path of the module to be built, relative to `$OutputDirectory`. + Defaults to an empty string, which resolves the link root to the output + root - the same location `build.ps1` builds to and pre-pends to + `PSModulePath` when `BuiltModuleSubdirectory` is not set in `build.yaml`. + The default must stay empty because InvokeBuild dot-sources every task + file into the same scope and a non-empty default here would overwrite the + shared value for all other tasks. `Get-SamplerWorkspaceLinkedModuleRoot` + keeps `module` as its own parameter default for when the argument is + omitted entirely. .PARAMETER WorkspaceModules The sibling workspace modules to link into the local output module path. @@ -41,7 +49,7 @@ param [Parameter()] [System.String] - $BuiltModuleSubdirectory = (property BuiltModuleSubdirectory 'module'), + $BuiltModuleSubdirectory = (property BuiltModuleSubdirectory ''), [Parameter()] [System.String[]] diff --git a/CHANGELOG.md b/CHANGELOG.md index 4229c758..ac118c7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- The `WorkspaceDependencies` build task declared its `BuiltModuleSubdirectory` + parameter default as `(property BuiltModuleSubdirectory 'module')` while every + other task file uses `''`. InvokeBuild dot-sources all imported task files into + the same scope and the `property` helper treats an empty string as unset, so + this non-empty default (dot-sourced last) overwrote the shared + `$BuiltModuleSubdirectory` value for every task in the build. Projects that do + not set `BuiltModuleSubdirectory` in `build.yaml` build to the output root, but + the tasks then looked for the built module under `output\module`, failing with + `Could not find the built module manifest for module ''. Build the module + before running tasks that require the built module output.` from + `Set-SamplerTaskVariable`. The default is now `''`, which also makes + `Link_Local_Workspace_Dependencies` link sibling modules into the location that + is actually on `PSModulePath` for the consuming project. Fixes #593. + ## [0.120.0] - 2026-07-14 ### Added diff --git a/GitVersion.yml b/GitVersion.yml index 1ef14602..2f277fa5 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,5 +1,5 @@ mode: ContinuousDelivery -next-version: 0.90 +next-version: 0.90.0 major-version-bump-message: '\s?(breaking|major|breaking\schange)' minor-version-bump-message: '(adds?|features?|minor)\b' patch-version-bump-message: '\s?(fix|patch)' diff --git a/tests/QA/module.tests.ps1 b/tests/QA/module.tests.ps1 index 2c1ba30f..ac90a72c 100644 --- a/tests/QA/module.tests.ps1 +++ b/tests/QA/module.tests.ps1 @@ -110,6 +110,52 @@ Describe 'General module control' -Tags 'FunctionalQuality' { } } +Describe 'Build task shared property defaults' -Tags 'FunctionalQuality' { + <# + InvokeBuild dot-sources every imported task file into the same scope, and + the 'property' helper treats an empty string as unset. A task file that + declares a non-empty default for a shared property therefore overwrites + the value used by every other task in the build. + #> + It 'Should use the same BuiltModuleSubdirectory property default in every build task file' { + $taskFilePath = Join-Path -Path $projectPath -ChildPath (Join-Path -Path '.build' -ChildPath 'tasks') + + $astSearchDelegate = { $args[0] -is [System.Management.Automation.Language.CommandAst] } + + $propertyDefault = @() + + foreach ($taskFile in (Get-ChildItem -Path $taskFilePath -Filter '*.build.ps1')) + { + $abstractSyntaxTree = [System.Management.Automation.Language.Parser]::ParseFile($taskFile.FullName, [ref] $null, [ref] $null) + + $parameterAst = @( + $abstractSyntaxTree.ParamBlock.Parameters | + Where-Object -FilterScript { + $_.Name.VariablePath.UserPath -eq 'BuiltModuleSubdirectory' + } + ) + + if ($parameterAst.Count -eq 0) + { + continue + } + + $propertyCommandAst = $parameterAst[0].DefaultValue.Find($astSearchDelegate, $true) + + $propertyDefault += [PSCustomObject] @{ + FileName = $taskFile.Name + Default = $propertyCommandAst.CommandElements[2].Value + } + } + + $propertyDefault.Count | Should -BeGreaterThan 0 -Because 'the build task files must declare the shared BuiltModuleSubdirectory property' + + $divergentDefault = @($propertyDefault | Where-Object -FilterScript { $_.Default -ne '' }) + + $divergentDefault.Count | Should -Be 0 -Because ("the shared 'BuiltModuleSubdirectory' property default must be an empty string in every build task file, but these files diverge: {0}" -f ($divergentDefault.FileName -join ', ')) + } +} + BeforeDiscovery { # Must use the imported module to build test cases. $allModuleFunctions = & $mut { Get-Command -Module $args[0] -CommandType Function } $script:moduleName diff --git a/tests/Unit/tasks/WorkspaceDependencies.build.Tests.ps1 b/tests/Unit/tasks/WorkspaceDependencies.build.Tests.ps1 index ffab678f..81da0940 100644 --- a/tests/Unit/tasks/WorkspaceDependencies.build.Tests.ps1 +++ b/tests/Unit/tasks/WorkspaceDependencies.build.Tests.ps1 @@ -24,6 +24,35 @@ Describe 'WorkspaceDependencies.build' { $taskAlias.ReferencedCommand | Should -Be 'WorkspaceDependencies.build.ps1' $taskAlias.Definition | Should -Match 'Sampler[\/|\\]\d+\.\d+\.\d+[\/|\\]tasks[\/|\\]WorkspaceDependencies\.build\.ps1' } + + <# + InvokeBuild dot-sources every imported task file into the same scope, and + the 'property' helper treats an empty string as unset. A non-empty default + here would therefore overwrite the shared $BuiltModuleSubdirectory value + for every other task in the build. + #> + It 'Should default the BuiltModuleSubdirectory property to an empty string' { + $taskFilePath = (Get-Alias -Name 'WorkspaceDependencies.build.Sampler.ib.tasks').Definition + + $abstractSyntaxTree = [System.Management.Automation.Language.Parser]::ParseFile($taskFilePath, [ref] $null, [ref] $null) + + $parameterAst = @( + $abstractSyntaxTree.ParamBlock.Parameters | + Where-Object -FilterScript { + $_.Name.VariablePath.UserPath -eq 'BuiltModuleSubdirectory' + } + ) + + $parameterAst.Count | Should -Be 1 + + $astSearchDelegate = { $args[0] -is [System.Management.Automation.Language.CommandAst] } + + $propertyCommandAst = $parameterAst[0].DefaultValue.Find($astSearchDelegate, $true) + + $propertyCommandAst.CommandElements[0].Value | Should -Be 'property' + $propertyCommandAst.CommandElements[1].Value | Should -Be 'BuiltModuleSubdirectory' + $propertyCommandAst.CommandElements[2].Value | Should -Be '' -Because 'a non-empty default leaks into the shared build scope of every other task' + } } Describe 'Link_Local_Workspace_Dependencies' {