Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,48 @@

### Fixed

- [**#201**](https://github.com/psake/PowerShellBuild/issues/201)
`$PSBPreference.Build.CompileModule = $true` now warns when the source
`.psm1` calls `Export-ModuleMember`. Compiling appends the source root
module after the concatenated function files, and compile mode copies no
`Public/` directory to the output, so the dot-sourcing loader that almost
every module template generates discovers nothing and the appended call
becomes `Export-ModuleMember -Function @()`. A module's effective exports
are the intersection of that call and `FunctionsToExport`, so the built
module exported nothing while the manifest correctly named every public
function, and the build reported success. The only previous symptom was
`No commands have been exported. Skipping markdown generation.` from a
later task, which names a documentation problem rather than an empty
module. The build still produces the same files; what changes is that it
now tells you. Guard the call so it does nothing when the function
directories are absent, or remove it and let `FunctionsToExport` govern
the export set. A mention of `Export-ModuleMember` in a comment does not
trigger the warning.

- [**#98**](https://github.com/psake/PowerShellBuild/issues/98)
`$PSBPreference.Build.Exclude` no longer aborts the build it is filtering.
The exclusion used a labeled `break` aimed at the loop over its whole input,
so a caller passing a collection lost every item after the first excluded
one; on Windows PowerShell 5.1 the `break` escaped the function entirely and
aborted whatever loop the caller was running, silently and with no error.
Found while adding the coverage for `Build-PSBuildModule` that #98 asked
for, which reaches the filter directly rather than one item at a time
through the pipeline.

- [**#98**](https://github.com/psake/PowerShellBuild/issues/98)
`$PSBPreference.Build.CompileModule = $true` no longer produces a module
with no functions in it when the build runs from a different drive than the
module source. The compile loop read each function file through a path
resolved relative to the current location, and a current location cannot
always express another path relatively: on Windows PowerShell 5.1 a source
on another drive resolves to `.\C:\...`, and a source outside the current
PSDrive's root to a `..\` path that cannot climb past that root. Neither
reads back, so every function was silently dropped and the compiled `.psm1`
came out holding only its headers and footers -- and the build reported
success. Files are now read through their full path. Also found while
adding the #98 coverage; this repository's own Windows PowerShell 5.1 CI
job reproduces it, with the checkout on `D:` and the test drive on `C:`.

- [**#193**](https://github.com/psake/PowerShellBuild/issues/193)
`$PSBPreference.Sign.SkipCertificateValidation` now does something. It was
read only by `psakeFile.ps1`, and even there `Get-PSBuildCertificate` consulted
Expand Down Expand Up @@ -222,7 +264,7 @@
that passed before may now correctly fail.
- [**#96**](https://github.com/psake/PowerShellBuild/issues/96)
`Test-PSBuildScriptAnalysis` no longer fails with a path-resolution error
when `SettingsPath` is not supplied. An unsupplied path was forwarded to

Check warning on line 267 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (unsupplied) Suggestions: (unapplied, unsullied, unspoiled, unstapled, unsupported)
PSScriptAnalyzer as `-Settings ''`, which resolved against the current
directory and threw before any analysis ran, so the function's own
documented example could not run as written.
Expand Down
22 changes: 18 additions & 4 deletions PowerShellBuild/Private/Remove-ExcludedItem.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
function Remove-ExcludedItem {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
[cmdletbinding()]

Check warning on line 3 in PowerShellBuild/Private/Remove-ExcludedItem.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (cmdletbinding)
[OutputType([IO.FileSystemInfo[]])]
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
Expand All @@ -16,14 +16,28 @@
}

process {
:item
foreach ($item in $InputObject) {
# A labeled break used to skip an excluded item here. On Windows PowerShell 5.1 that
# break escapes this function instead of ending the loop it names, and an escaping
# break aborts whatever loop the caller happened to be running, silently and with no
# error. Pester reports it as "a 'break' or 'continue' statement with a label that
# does not match any enclosing loop escaped from your code". A flag keeps all flow
# control inside this function.
#
# The labeled break was also aimed at the wrong loop: it ended the loop over
# $InputObject rather than skipping the one excluded item, so a caller that passed a
# collection through -InputObject lost every item after the first excluded one.
$isExcluded = $false
foreach ($regex in $Exclude) {
if ($_ -match $regex) {
break item
if ($item -match $regex) {
$isExcluded = $true
break
}
}
$keepers.Add($_)

if (-not $isExcluded) {
$keepers.Add($item)
}
}
}

Expand Down
55 changes: 51 additions & 4 deletions PowerShellBuild/Public/Build-PSBuildModule.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# spell-checker:ignore modulename
# spell-checker:ignore modulename
function Build-PSBuildModule {
<#
.SYNOPSIS
Expand Down Expand Up @@ -132,6 +132,39 @@ function Build-PSBuildModule {
# Grab the contents of the copied over PSM1
# This will be appended to the end of the finished PSM1
$psm1Contents = Get-Content -Path $rootModule -Raw

# Because that content is appended last, an Export-ModuleMember call inside it runs
# after the concatenated functions, and the module's effective export set is the
# intersection of that call and FunctionsToExport in the manifest. Compiling copies no
# function directories to the output, so the scaffold loader that almost every module
# template generates discovers nothing and exports nothing, while the manifest still
# names every public function and the build still succeeds. Warned about rather than
# rewritten: what the consumer's root module should do instead depends on the module.
# See psake/PowerShellBuild#201.
#
# Found by parsing rather than by matching text, so that the command named in a
# comment or quoted in a here-string -- including a comment explaining why the loader
# deliberately does not call it -- is not reported as a call. A line-anchored regex
# gets the single-line # comment right and still fires inside a <# #> block or a
# here-string, because it cannot see that the line it anchored on is not code.
$rootModuleAst = [System.Management.Automation.Language.Parser]::ParseInput(
$psm1Contents, [ref] $null, [ref] $null
)
$exportModuleMemberCall = $rootModuleAst.FindAll(
{
param($node)
$node -is [System.Management.Automation.Language.CommandAst] -and
$node.GetCommandName() -eq 'Export-ModuleMember'
},
$true
)
if ($exportModuleMemberCall) {
$sourceRootModule = [IO.Path]::Combine($Path, "$ModuleName.psm1")
Write-Warning (
$LocalizedData.ExportModuleMemberInSourceRootModule -f $sourceRootModule
)
}

'' | Out-File -FilePath $rootModule -Encoding 'utf8'

if ($CompileHeader) {
Expand All @@ -157,14 +190,28 @@ function Build-PSBuildModule {
Encoding = 'utf8'
}
$allScripts | ForEach-Object {
$srcFile = Resolve-Path $_.FullName -Relative
Write-Verbose ($LocalizedData.AddingFileToPsm1 -f $srcFile)
# Read through the full path. This used to read through a path resolved relative to
# the current location, which is not something every current location can express.
# On Windows PowerShell 5.1 a source file on another drive resolves to a path like
# .\C:\src\Public\Get-Widget.ps1, and a source outside the current PSDrive's root to
# a ..\ path that cannot climb past that root. Neither can be read back, so
# Get-Content silently returned nothing for every file and the compiled .psm1 came
# out holding its headers and footers and none of the functions -- while the build
# reported success. It is reachable whenever the build runs from a different drive
# than the module source, which is how the Windows PowerShell 5.1 CI job is laid out.
$sourceFilePath = $_.FullName
Write-Verbose ($LocalizedData.AddingFileToPsm1 -f $sourceFilePath)

if ($CompileScriptHeader) {
Write-Output $CompileScriptHeader
}

Get-Content $srcFile
# -LiteralPath, not -Path: the value is a FullName, and -Path reads [ ] * ? as
# wildcards. A source file named Get-Widget[1].ps1, or any checkout under a
# directory like repo[main], would then match nothing and be dropped from the
# compiled module in silence -- the same failure this full-path read exists to
# prevent, reached a different way.
Get-Content -LiteralPath $sourceFilePath

if ($CompileScriptFooter) {
Write-Output $CompileScriptFooter
Expand Down
1 change: 1 addition & 0 deletions PowerShellBuild/en-US/Messages.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ CertificateExpired=The resolved certificate has expired (NotAfter: {0}). Code si
CertificateMissingCodeSigningEku=The resolved certificate does not have the Code Signing Enhanced Key Usage (EKU: 1.3.6.1.5.5.7.3.3). Subject=[{0}]
CertificateSourceStoreNotSupported=CertificateSource 'Store' is only supported on Windows platforms.
CertificateValidationRelaxed=No unexpired code signing certificate was found, and validation was skipped, so an expired certificate was selected (NotAfter: {0}). Subject=[{1}]
ExportModuleMemberInSourceRootModule=The source root module [{0}] calls Export-ModuleMember. Compiling appends that call after the concatenated function files, where it runs against an output directory that no longer has the function directories to discover, and the effective export set is the intersection of that call and FunctionsToExport in the manifest. A loader that finds nothing therefore leaves the built module exporting nothing. Guard the call so it does nothing when the function directories are absent, or remove it and let the manifest govern the export set.
'@
1 change: 1 addition & 0 deletions instructions/repository-specific.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@
| ---------------------------------------- | -------------------------------------------------------------------- |
| `build.tests.ps1` | Module compilation, file staging, exclusion, header/footer injection |
| `Build-PSBuildHelp.tests.ps1` | Markdown and MAML help generation (skipped without PlatyPS) |
| `Build-PSBuildModule.tests.ps1` | The Build-PSBuildModule function directly, compiled and not |
| `Clear-PSBuildOutputFolder.tests.ps1` | Output directory removal |
| `Fixtures.tests.ps1` | The shared test fixture helpers themselves |
| `Get-PSBuildCertificate.tests.ps1` | Signing certificate resolution |
Expand Down Expand Up @@ -289,7 +290,7 @@

- Triggers: manual dispatch, GitHub release published
- Runs on: `ubuntu-latest`
- Reads `PSGALLERY_API_KEY` secret, converts to `PSCredential`, runs

Check warning on line 293 in instructions/repository-specific.instructions.md

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (PSGALLERY) Suggestions: (psaltery, spaller, psaltry, psalter, psalters)
`./build.ps1 -Task Publish -PSGalleryApiKey $cred -Bootstrap`

## Repo-Specific Conventions
Expand Down Expand Up @@ -370,9 +371,9 @@
| ------------------------- | ---------------------------------------------------- |
| `$env:BHProjectPath` | Repository root directory |
| `$env:BHProjectName` | Module name (from directory structure) |
| `$env:BHPSModulePath` | Path to module source directory |

Check warning on line 374 in instructions/repository-specific.instructions.md

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (BHPS) Suggestions: (baps, bops, bhp, BHP, bps)
| `$env:BHPSModuleManifest` | Path to `.psd1` manifest |

Check warning on line 375 in instructions/repository-specific.instructions.md

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (BHPS) Suggestions: (baps, bops, bhp, BHP, bps)
| `$env:BHModulePath` | Same as `BHPSModulePath` |

Check warning on line 376 in instructions/repository-specific.instructions.md

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (BHPS) Suggestions: (baps, bops, bhp, BHP, bps)
| `$env:BHBuildSystem` | Detected CI system (e.g., `GitHubActions`, `Unknown`)|
| `$env:BHBranchName` | Current git branch |
| `$env:BHCommitMessage` | Latest git commit message |
Expand Down
Loading
Loading