PSBuilder.psm1
Set-StrictMode -Version Latest $ErrorActionPreference='Stop' ##### BEGIN Exit-Powershell.ps1 ##### #.ExternalHelp PSBuilder-Help.xml function Exit-Powershell { param ([int]$ExitCode=0) exit $ExitCode } ##### END Exit-Powershell.ps1 ##### ##### BEGIN Invoke-Builder.ps1 ##### #.ExternalHelp PSBuilder-Help.xml function Invoke-Builder { [CmdletBinding(DefaultParameterSetName="Default")] param ( [Parameter(Position=0, ValueFromRemainingArguments=$true)] [string[]]$Tasks, [Parameter(Mandatory=$false)] [hashtable]$Parameters = $null, [switch]$ExitOnError ) $buildRoot = (Get-Location).Path $buildFile = "$PSScriptRoot/files/build.tasks.ps1" $buildScript = Get-Command -Name $buildFile $buildParameters = @{ "BuildRoot" = $buildRoot } (Get-ChildItem -Path "Env:").Where({ $_.Name -like "PSBuilder*" }).ForEach({ $buildParameters[$_.Name.Substring(9)] = $_.Value }) if ($null -ne $Parameters) { $buildParameters += $Parameters } foreach ($parameter in @($buildParameters.Keys)) { if (-not $buildScript.Parameters.ContainsKey($parameter)) { throw "Unknown parameter: $parameter" } $buildParameterType = $buildScript.Parameters[$parameter].ParameterType $currentValue = $buildParameters[$parameter] if ($buildParameterType -eq [string[]] -and $currentValue -is [string]) { $buildParameters[$parameter] = $currentValue -split "," } elseif ($buildParameterType -eq [securestring]) { $value = [securestring]::new() $currentValue.ToCharArray().ForEach({ $value.AppendChar($_) }) $buildParameters[$parameter] = $value } elseif ($buildParameterType -eq [bool]) { $buildParameters[$parameter] = [Convert]::ToBoolean($buildParameters[$parameter]) } elseif ($buildParameterType -eq [int]) { $buildParameters[$parameter] = [Convert]::ToInt32($buildParameters[$parameter]) } elseif ($buildParameterType -eq [datetime]) { $buildParameters[$parameter] = [Convert]::ToDateTime($buildParameters[$parameter]) } } try { $failed = $false Invoke-Build -Task $Tasks -File $BuildFile -Result "result" @buildParameters } catch { $failed = $true if (-not $ExitOnError) { throw } } if ($failed -or $result.Errors.Count -gt 0) { if ($ExitOnError) { Exit-Powershell -ExitCode 1 } else { throw "Build Failed..." } } } ##### END Invoke-Builder.ps1 ##### ##### BEGIN Invoke-CompileModule.ps1 ##### #.ExternalHelp PSBuilder-Help.xml function Invoke-CompileModule { param ( [Parameter(Mandatory=$true)] [string]$Name, [Parameter(Mandatory=$true)] [string]$Source, [Parameter(Mandatory=$true)] [string]$Destination ) if (-not [IO.Path]::IsPathRooted($Source)) { $Source = Resolve-Path -Path $Source } if (-not [IO.Path]::IsPathRooted($Destination)) { $Destination = [IO.Path]::GetFullPath($Destination) } $buildFolders = ("Classes", "Private", "Public") $SourceFile = Join-Path -Path $Source -ChildPath "$Name.psm1" if (Test-Path -Path $SourceFile) { Copy-Item -Path $SourceFile -Destination $Destination foreach ($buildFolder in $buildFolders) { $path = Join-Path -Path $Source -ChildPath $buildFolder if (Test-Path -Path $path) { Copy-Item -Path $path -Destination $BuildOutput -Recurse -Container -Force } } } else { $publicFolder = Join-Path -Path $Source -ChildPath "Public" $publicFunctions = @(Get-ChildItem -Path $publicFolder -Filter "*.ps1" -Recurse).ForEach({ $_.BaseName }) $builder = [System.Text.StringBuilder]::new() [void]$builder.AppendLine("Set-StrictMode -Version Latest") [void]$builder.AppendLine("`$ErrorActionPreference='Stop'") foreach ($buildFolder in $buildFolders) { $path = Join-Path -Path $Source -ChildPath $buildFolder if (-not (Test-Path -Path $path)) { continue } $files = Get-ChildItem -Path $path -Filter "*.ps1" -Recurse foreach ($file in $files) { $content = Get-Content -Path $file.FullName -Raw [void]$builder.AppendLine("") [void]$builder.AppendLine("##### BEGIN $($file.Name) #####") [void]$builder.AppendLine("#.ExternalHelp $Name-Help.xml") [void]$builder.AppendLine($content) [void]$builder.AppendLine("##### END $($file.Name) #####") [void]$builder.AppendLine("") } } if ($publicFunctions.Count -gt 0) { [void]$builder.AppendLine("Export-ModuleMember -Function @($($publicFunctions.ForEach({ "'$_'" }) -join ", "))") } Set-Content -Path $Destination -Value ($builder.ToString()) -Force } } ##### END Invoke-CompileModule.ps1 ##### ##### BEGIN Invoke-CreateModuleManifest.ps1 ##### #.ExternalHelp PSBuilder-Help.xml function Invoke-CreateModuleManifest { param ( [Parameter(Mandatory=$true)] [string]$Name, [Parameter(Mandatory=$true)] [string]$Guid, [Parameter(Mandatory=$true)] [string]$Author, [Parameter(Mandatory=$true)] [string]$Description, [Parameter(Mandatory=$true)] [string]$Path, [Parameter(Mandatory=$true)] [string]$ModuleFilePath, [Parameter(Mandatory=$true)] [string]$Version, [string]$LicenseUri = $null, [string]$IconUri = $null, [string]$ProjectUri = $null, [string[]]$Tags = $null, [string]$Prerelease = $null ) $module = Get-Module -Name $ModuleFilePath -ListAvailable $Exports = @{ "Aliases" = @($module.ExportedAliases.Keys) "Cmdlets" = @($module.ExportedCmdlets.Keys) "Functions" = @($module.ExportedFunctions.Keys) "Variables" = @($module.ExportedVariables.Keys) } $ManifestArguments = @{ "RootModule" = "$Name.psm1" "Guid" = $Guid "Author" = $Author "Description" = $Description "Copyright" = "(c) $((Get-Date).Year) $Author. All rights reserved." "AliasesToExport" = $Exports.Aliases "CmdletsToExport" = $Exports.Cmdlets "FunctionsToExport" = $Exports.Functions "VariablesToExport" = $Exports.Variables "ModuleVersion" = $Version } if ($PSBoundParameters.ContainsKey("LicenseUri")) { $ManifestArguments.LicenseUri = $LicenseUri } if ($PSBoundParameters.ContainsKey("ProjectUri")) { $ManifestArguments.ProjectUri = $ProjectUri } if ($PSBoundParameters.ContainsKey("IconUri")) { $ManifestArguments.IconUri = $IconUri } if ($PSBoundParameters.ContainsKey("Tags") -and $Tags.Count -gt 0) { $ManifestArguments.Tags = $Tags } New-ModuleManifest -Path $Path @ManifestArguments if ($PSBoundParameters.ContainsKey("Prerelease")) { Update-ModuleManifest -Path $Path -Prerelease $Prerelease } } ##### END Invoke-CreateModuleManifest.ps1 ##### ##### BEGIN Invoke-GenerateSelfSignedCert.ps1 ##### #.ExternalHelp PSBuilder-Help.xml function Invoke-GenerateSelfSignedCert { $AvailableCerts = @(Get-ChildItem -Path "Cert:\CurrentUser" -CodeSigningCert -Recurse).Where({ $_.Subject -eq "CN=Test Code Signing Certificate" -and (Test-Certificate -AllowUntrustedRoot -Cert $_) }) $Certificate = $AvailableCerts | Sort-Object -Descending -Property NotAfter | Select-Object -First 1 if ($null -eq $Certificate) { $CertArgs = @{ Subject = "CN=Test Code Signing Certificate" KeyFriendlyName = "Test Code Signing Certificate" CertStoreLocation = "Cert:\CurrentUser" KeyAlgorithm = "RSA" KeyLength = 4096 Provider = "Microsoft Enhanced RSA and AES Cryptographic Provider" KeyExportPolicy = "NonExportable" KeyUsage = "DigitalSignature" Type = "CodeSigningCert" Verbose = $VerbosePreference } $Certificate = New-SelfSignedCertificate @CertArgs "Generated $Certificate" } $RootPath = "Cert:LocalMachine\Root" $TrustedRootEntry = @(Get-ChildItem -Path $RootPath -Recurse).Where({ $_.Thumbprint -eq $Certificate.Thumbprint }) | Select-Object -First 1 if ($null -eq $TrustedRootEntry) { $ExportPath = Join-Path -Path $Env:TEMP -ChildPath "cert.crt" Export-Certificate -Type CERT -FilePath $ExportPath -Cert $Certificate -Force | Out-Null Import-Certificate -FilePath $ExportPath -CertStoreLocation $RootPath | Out-Null Remove-Item -Path $ExportPath "Copied $Certificate to trusted root" } } ##### END Invoke-GenerateSelfSignedCert.ps1 ##### ##### BEGIN Invoke-Sign.ps1 ##### #.ExternalHelp PSBuilder-Help.xml function Invoke-Sign { param ( [Parameter(Mandatory=$false)] [string]$CertificateThumbprint, [Parameter(Mandatory=$false)] [string]$CertificateSubject, [string]$CertificatePath, [securestring]$CertificatePassword, [string]$Name, [string]$Path, [string]$HashAlgorithm ) if ($CertificatePath -like "Cert:*") { $Certificates = @(Get-ChildItem -Path $CertificatePath -CodeSigningCert) } else { $Certificates = @([System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertificatePath, $CertificatePassword)) } if (-not [string]::IsNullOrEmpty($CertificateThumbprint)) { $Certificates = $Certificates.Where({ $_.Thumbprint -eq $CertificateThumbprint }) } elseif (-not [string]::IsNullOrEmpty($CertificateSubject)) { $Certificates = $Certificates.Where({ $_.Subject -eq $CertificateSubject }) } $Script:Certificate = @($Certificates).Where({ $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) }) | Sort-Object -Descending -Property NotAfter | Select-Object -First 1 if ($null -eq $Certificate) { throw "$($Certificates.Count) code signing certificates were found but none are valid." } $files = @(Get-ChildItem -Path $Path -Recurse -Include $ExtensionsToSign).ForEach({ $_.FullName }) foreach ($file in $files) { $setAuthSigParams = @{ FilePath = $file Certificate = $certificate HashAlgorithm = $HashAlgorithm Verbose = $VerbosePreference } $result = Set-AuthenticodeSignature @setAuthSigParams if ($result.Status -ne 'Valid') { throw "Failed to sign: $file. Status: $($result.Status) $($result.StatusMessage)" } "Successfully signed: $file" } $catalogFile = "$Path\$Name.cat" $catalogParams = @{ Path = $Path CatalogFilePath = $catalogFile CatalogVersion = 2.0 Verbose = $VerbosePreference } New-FileCatalog @catalogParams | Out-Null $catalogSignParams = @{ FilePath = $catalogFile Certificate = $certificate HashAlgorithm = $HashAlgorithm Verbose = $VerbosePreference } $result = Set-AuthenticodeSignature @catalogSignParams if ($result.Status -ne 'Valid') { throw "Failed to sign the catalog file. Status: $($result.Status) $($result.StatusMessage)" } } ##### END Invoke-Sign.ps1 ##### ##### BEGIN Invoke-CreateHelp.ps1 ##### #.ExternalHelp PSBuilder-Help.xml function Invoke-CreateHelp { param ( [Parameter(Mandatory=$true)] [string]$Source, [Parameter(Mandatory=$true)] [string]$Destination, [string]$Language="en-US" ) $destinationPath = Join-Path -Path $Destination -ChildPath $Language New-ExternalHelp -Path $Source -OutputPath $destinationPath -Force | Out-Null } ##### END Invoke-CreateHelp.ps1 ##### ##### BEGIN Invoke-CreateMarkdown.ps1 ##### #.ExternalHelp PSBuilder-Help.xml function Invoke-CreateMarkdown { param ( [Parameter(Mandatory=$true)] [string]$Manifest, [Parameter(Mandatory=$true)] [string]$Path ) $module = Import-Module -Name $Manifest -Global -Force -PassThru if (-not (Test-Path -Path $Path)) { New-Item -Path $Path -ItemType Directory -Force | Out-Null } $moduleFile = Join-Path -Path $Path -ChildPath "$($module.Name).md" if (-not (Test-Path -Path $moduleFile)) { New-MarkdownHelp -Module $($module.Name) -OutputFolder $Path -WithModulePage | Out-Null } Update-MarkdownHelpModule -Path $Path -RefreshModulePage | Out-Null } ##### END Invoke-CreateMarkdown.ps1 ##### ##### BEGIN Invoke-PublishToRepository.ps1 ##### #.ExternalHelp PSBuilder-Help.xml function Invoke-PublishToRepository { param ( [Parameter(Mandatory=$true)] [string]$Path, [Parameter(Mandatory=$false)] [string]$Repository, [Parameter(Mandatory=$false)] [string]$NugetApiKey ) $publishParams = @{ "Path" = $Path } if (-not [string]::IsNullOrEmpty($Repository)) { $publishParams["Repository"] = $Repository } if (-not [string]::IsNullOrEmpty($NugetApiKey)) { $publishParams["NuGetApiKey"] = $NugetApiKey } Publish-Module @publishParams } ##### END Invoke-PublishToRepository.ps1 ##### ##### BEGIN Invoke-CodeAnalysis.ps1 ##### #.ExternalHelp PSBuilder-Help.xml function Invoke-CodeAnalysis { param ( [Parameter(Mandatory=$true)] [string]$Path, [string]$FailureLevel, [string]$SettingsFile ) $analysisParameters = @{} if (-not [string]::IsNullOrEmpty($SettingsFile) -and (Test-Path -Path $SettingsFile)) { $analysisParameters["Settings"] = $SettingsFile } $analysisResult = Invoke-ScriptAnalyzer -Path $Path -Recurse @analysisParameters $analysisResult | Format-Table | Out-String -Width 192 $warnings = $analysisResult.Where({ $_.Severity -eq "Warning" -or $_.Severity -eq "Warning" }).Count $errors = $analysisResult.Where({ $_.Severity -eq "Error" }).Count "Script analyzer triggered {0} warnings and {1} errors" -f $warnings, $errors if ($FailureLevel -eq "Warning") { Assert ($warnings -eq 0 -and $errors -eq 0) "Build failed due to warnings or errors found in analysis." } elseif ($FailureLevel -eq "Error") { Assert ($errors -eq 0) "Build failed due to errors found in analysis." } } ##### END Invoke-CodeAnalysis.ps1 ##### ##### BEGIN Invoke-PesterTest.ps1 ##### #.ExternalHelp PSBuilder-Help.xml function Invoke-PesterTest { param ( [string[]]$Tags = @(), [string]$Path, [string]$Module, [string]$OutputPath, [string]$CoverageOutputPath, [int]$MinCoverage=0 ) if ($Tags -eq "*") { $Tags = @() } if (-not (Test-Path -Path $Path)) { $testCoverage = 0 } else { Set-Location -Path $Path Import-Module -Name $Module -Force $files = @(Get-ChildItem -Path ([IO.Path]::GetDirectoryName($Module)) -Include "*.ps1","*.psm1" -File -Recurse) $pesterArgs = @{ CodeCoverage = $files Tag = $tags OutputFile = $OutputPath OutputFormat = "NUnitXml" CodeCoverageOutputFile = $CoverageOutputPath CodeCoverageOutputFileFormat = "JaCoCo" PassThru = $true } $testResult = Invoke-Pester @pesterArgs assert ($testResult.FailedCount -eq 0) ('Failed {0} Unit tests. Aborting Build' -f $testResult.FailedCount) if (0 -eq $testResult.CodeCoverage.NumberOfCommandsAnalyzed) { $testCoverage = 0 } else { $testCoverage = [int]($testResult.CodeCoverage.NumberOfCommandsExecuted / $testResult.CodeCoverage.NumberOfCommandsAnalyzed * 100) } } Write-Output "Code coverage: ${testCoverage}%" assert ($MinCoverage -le $testCoverage) ('Code coverage must be higher or equal to {0}%. Current coverage: {1}%' -f ($MinCoverage, $testCoverage)) } ##### END Invoke-PesterTest.ps1 ##### Export-ModuleMember -Function @('Invoke-Builder', 'Invoke-CompileModule', 'Invoke-CreateModuleManifest', 'Invoke-GenerateSelfSignedCert', 'Invoke-Sign', 'Invoke-CreateHelp', 'Invoke-CreateMarkdown', 'Invoke-PublishToRepository', 'Invoke-CodeAnalysis', 'Invoke-PesterTest') |