diff --git a/build/Add-PSModuleHeader.ps1 b/build/Add-PSModuleHeader.ps1 new file mode 100644 index 0000000..a95b037 --- /dev/null +++ b/build/Add-PSModuleHeader.ps1 @@ -0,0 +1,71 @@ +param +( + # Path to Module Manifest + [Parameter(Mandatory = $false)] + [string] $ModuleManifestPath = ".\release\*\*.*.*", + # + [Parameter(Mandatory = $false)] + [string] $OutputModulePath +) + +## Initialize +Import-Module "$PSScriptRoot\CommonFunctions.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop + +[System.IO.FileInfo] $ModuleManifestFileInfo = Get-PathInfo $ModuleManifestPath -DefaultFilename "*.psd1" -ErrorAction Stop + +## Read Module Manifest +$ModuleManifest = Import-PowerShellDataFile $ModuleManifestFileInfo.FullName + +if ($OutputModulePath) { + [System.IO.FileInfo] $OutputModuleFileInfo = Get-PathInfo $OutputModulePath -InputPathType File -DefaultFilename "$($ModuleManifestFileInfo.BaseName).psm1" -ErrorAction SilentlyContinue +} +else { + [System.IO.FileInfo] $OutputModuleFileInfo = Get-PathInfo $ModuleManifest['RootModule'] -InputPathType File -DefaultDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction SilentlyContinue +} + +if ($OutputModuleFileInfo.Extension -eq ".psm1") { + ## Add Requires Statements + $RequiresStatements = "" + if ($ModuleManifest['PowerShellVersion']) { $RequiresStatements += "#Requires -Version {0}`r`n" -f $ModuleManifest['PowerShellVersion'] } + if ($ModuleManifest['CompatiblePSEditions']) { $RequiresStatements += "#Requires -PSEdition {0}`r`n" -f ($ModuleManifest['CompatiblePSEditions'] -join ',') } + foreach ($RequiredAssembly in $ModuleManifest['RequiredAssemblies']) { + $RequiresStatements += "#Requires -Assembly $_`r`n" + } + foreach ($RequiredModule in $ModuleManifest['RequiredModules']) { + $RequiresStatements += ConvertTo-PsString $ModuleManifest['RequiredModules'] -Compact -RemoveTypes ([hashtable], [string]) | ForEach-Object { "#Requires -Module $_`r`n" } + } + + ## Build Module Comment Header + [string] $CommentHeader = "<#`r`n" + $CommentHeader += ".SYNOPSIS`r`n {0}`r`n" -f $ModuleManifestFileInfo.BaseName + + if ($ModuleManifest['Description']) { + $CommentHeader += ".DESCRIPTION`r`n {0}`r`n" -f $ModuleManifest['Description'] + } + + [string]$ModuleVersion = if ($ModuleManifest.PrivateData.PSData['Prerelease']) { '{0}-{1}' -f $ModuleManifest['ModuleVersion'], $ModuleManifest.PrivateData.PSData['Prerelease'] } else { $ModuleManifest['ModuleVersion'] } + $CommentHeader += ".NOTES`r`n" + $CommentHeader += " ModuleVersion: {0}`r`n" -f $ModuleVersion + if ($ModuleManifest['GUID']) { $CommentHeader += " GUID: {0}`r`n" -f $ModuleManifest['GUID'] } + if ($ModuleManifest['Author']) { $CommentHeader += " Author: {0}`r`n" -f $ModuleManifest['Author'] } + if ($ModuleManifest['CompanyName']) { $CommentHeader += " CompanyName: {0}`r`n" -f $ModuleManifest['CompanyName'] } + if ($ModuleManifest['Copyright']) { $CommentHeader += " Copyright: {0}`r`n" -f $ModuleManifest['Copyright'] } + if ($ModuleManifest['FunctionsToExport']) { + ## ToDo: Account for modules with functions and/or cmdlets. + $CommentHeader += ".FUNCTIONALITY`r`n {0}`r`n" -f ($ModuleManifest['FunctionsToExport'] -join ', ') + } + if ($ModuleManifest.PrivateData.PSData['ProjectUri']) { + $CommentHeader += ".LINK`r`n {0}`r`n" -f $ModuleManifest.PrivateData.PSData['ProjectUri'] + } + $CommentHeader += "#>" + + ## Add Comment Header to Script Module + if ($OutputModuleFileInfo.Exists) { + $RootModuleContent = (Get-Content $OutputModuleFileInfo.FullName -Raw) + } + else { + $RootModuleContent = $null + } + + $RequiresStatements, $CommentHeader, $RootModuleContent | Set-Content $OutputModuleFileInfo.FullName -Encoding utf8BOM +} diff --git a/build/Build-PSModule.ps1 b/build/Build-PSModule.ps1 new file mode 100644 index 0000000..fb2fde6 --- /dev/null +++ b/build/Build-PSModule.ps1 @@ -0,0 +1,89 @@ +param +( + # Directory used to base all relative paths + [Parameter(Mandatory = $false)] + [string] $BaseDirectory = "..\", + # + [Parameter(Mandatory = $false)] + [string] $OutputDirectory = ".\build\release\", + # + [Parameter(Mandatory = $false)] + [string] $SourceDirectory = ".\src\", + # + [Parameter(Mandatory = $false)] + [string] $ModuleManifestPath, + # + [Parameter(Mandatory = $false)] + [string] $PackagesConfigPath = ".\packages.config", + # + [Parameter(Mandatory = $false)] + [string] $PackagesDirectory = ".\build\packages", + # + [Parameter(Mandatory = $false)] + [string] $LicensePath = ".\LICENSE", + # + [Parameter(Mandatory = $false)] + [switch] $SkipMergingNestedModuleScripts +) + +## Initialize +Import-Module "$PSScriptRoot\CommonFunctions.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop + +[System.IO.DirectoryInfo] $BaseDirectoryInfo = Get-PathInfo $BaseDirectory -InputPathType Directory -ErrorAction Stop +[System.IO.DirectoryInfo] $OutputDirectoryInfo = Get-PathInfo $OutputDirectory -InputPathType Directory -DefaultDirectory $BaseDirectoryInfo.FullName -ErrorAction SilentlyContinue +[System.IO.DirectoryInfo] $SourceDirectoryInfo = Get-PathInfo $SourceDirectory -InputPathType Directory -DefaultDirectory $BaseDirectoryInfo.FullName -ErrorAction Stop +[System.IO.FileInfo] $ModuleManifestFileInfo = Get-PathInfo $ModuleManifestPath -DefaultDirectory $SourceDirectoryInfo.FullName -DefaultFilename "*.psd1" -ErrorAction Stop +[System.IO.FileInfo] $PackagesConfigFileInfo = Get-PathInfo $PackagesConfigPath -DefaultDirectory $BaseDirectoryInfo.FullName -DefaultFilename "packages.config" -ErrorAction SilentlyContinue +[System.IO.DirectoryInfo] $PackagesDirectoryInfo = Get-PathInfo $PackagesDirectory -InputPathType Directory -DefaultDirectory $BaseDirectoryInfo.FullName -ErrorAction SilentlyContinue +[System.IO.FileInfo] $LicenseFileInfo = Get-PathInfo $LicensePath -DefaultDirectory $BaseDirectoryInfo.FullName -DefaultFilename "LICENSE" -ErrorAction SilentlyContinue + +## Read Module Manifest +$ModuleManifest = Import-PowerShellDataFile $ModuleManifestFileInfo.FullName +[System.IO.DirectoryInfo] $ModuleOutputDirectoryInfo = Join-Path $OutputDirectoryInfo.FullName (Join-Path $ModuleManifestFileInfo.BaseName $ModuleManifest['ModuleVersion']) +[System.IO.FileInfo] $OutputModuleManifestFileInfo = Join-Path $ModuleOutputDirectoryInfo.FullName $ModuleManifestFileInfo.Name + +## Copy Source Module Code to Module Output Directory +Assert-DirectoryExists $ModuleOutputDirectoryInfo -ErrorAction Stop | Out-Null +Copy-Item ("{0}\*" -f $SourceDirectoryInfo.FullName) -Destination $ModuleOutputDirectoryInfo.FullName -Recurse -Force +if (!$SkipMergingNestedModuleScripts) { + [System.IO.FileInfo] $OutputRootModuleFileInfo = (Join-Path $ModuleOutputDirectoryInfo.FullName $ModuleManifest['RootModule']) + &$PSScriptRoot\Merge-PSModuleNestedModuleScripts.ps1 -ModuleManifestPath $OutputModuleManifestFileInfo.FullName -OutputModulePath $OutputRootModuleFileInfo.FullName -MergeWithRootModule -RemoveNestedModuleScriptFiles +} +if ($LicenseFileInfo.Exists) { + Copy-Item $LicenseFileInfo.FullName -Destination (Join-Path $ModuleOutputDirectoryInfo.FullName License.txt) -Force +} + +if ($PackagesConfigFileInfo.Exists) { + ## NuGet Restore + &$PSScriptRoot\Restore-NugetPackages.ps1 -PackagesConfigPath $PackagesConfigFileInfo.FullName -OutputDirectory $PackagesDirectoryInfo.FullName + + ## Read Packages Configuration + $xmlPackagesConfig = New-Object xml + $xmlPackagesConfig.Load($PackagesConfigFileInfo.FullName) + + ## Copy Packages to Module Output Directory + foreach ($package in $xmlPackagesConfig.packages.package) { + [string[]] $targetFrameworks = $package.targetFramework + if (!$targetFrameworks) { [string[]] $targetFrameworks = "net45", "netcoreapp2.1" } + foreach ($targetFramework in $targetFrameworks) { + [System.IO.DirectoryInfo] $PackageDirectory = Join-Path $PackagesDirectoryInfo.FullName ("{0}.{1}\lib\{2}" -f $package.id, $package.version, $targetFramework) + [System.IO.DirectoryInfo] $PackageOutputDirectory = "{0}\{1}.{2}\{3}" -f $ModuleOutputDirectoryInfo.FullName, $package.id, $package.version, $targetFramework + $PackageOutputDirectory + Assert-DirectoryExists $PackageOutputDirectory -ErrorAction Stop | Out-Null + Copy-Item ("{0}\*" -f $PackageDirectory) -Destination $PackageOutputDirectory.FullName -Recurse -Force + } + } +} + +## Get Module Output FileList +#$ModuleFileListFileInfo = Get-ChildItem $ModuleOutputDirectoryInfo.FullName -Recurse -File +#$ModuleManifestOutputFileInfo = $ModuleFileListFileInfo | Where-Object Name -EQ $ModuleManifestFileInfo.Name + +## Update Module Manifest in Module Output Directory +&$PSScriptRoot\Update-PSModuleManifest.ps1 -ModuleManifestPath $OutputModuleManifestFileInfo.FullName +if (!$SkipMergingNestedModuleScripts) { + &$PSScriptRoot\Add-PSModuleHeader.ps1 -ModuleManifestPath $OutputModuleManifestFileInfo.FullName +} + +## Sign Module +&$PSScriptRoot\Sign-PSModule.ps1 -ModuleManifestPath $OutputModuleManifestFileInfo.FullName | Format-Table Path, Status, StatusMessage diff --git a/build/CommonFunctions.psm1 b/build/CommonFunctions.psm1 index c87d617..042ed67 100644 --- a/build/CommonFunctions.psm1 +++ b/build/CommonFunctions.psm1 @@ -179,8 +179,10 @@ function Get-PathInfo { if ($Path) { ## Look for existing path try { - $ResolvePath = Resolve-FullPath $Path -BaseDirectory $DefaultDirectory -ErrorAction SilentlyContinue - $OutputPath = Get-Item $ResolvePath -ErrorAction SilentlyContinue + $ResolvePath = Resolve-FullPath $Path -BaseDirectory $DefaultDirectory -ErrorAction Ignore + if ($ResolvePath) { + $OutputPath = Get-Item $ResolvePath -ErrorAction Ignore + } } catch { } if ($OutputPath -is [array]) { @@ -189,6 +191,7 @@ function Get-PathInfo { return } + [string] $AbsolutePath = $null ## If path could not be found and there are no wildcards, then create a FileSystemInfo object for the path. if (!$OutputPath -and $Path -notmatch '[*?]') { ## Get Absolute Path @@ -206,8 +209,10 @@ function Get-PathInfo { [string] $AbsolutePath = (Join-Path $OutputPath.FullName $DefaultFileName) $OutputPath = $null try { - $ResolvePath = Resolve-FullPath $AbsolutePath -BaseDirectory $DefaultDirectory -ErrorAction SilentlyContinue - $OutputPath = Get-Item $ResolvePath -ErrorAction SilentlyContinue + $ResolvePath = Resolve-FullPath $AbsolutePath -BaseDirectory $DefaultDirectory -ErrorAction Ignore + if ($ResolvePath) { + $OutputPath = Get-Item $ResolvePath -ErrorAction Ignore + } } catch { } if (!$OutputPath -and $AbsolutePath -notmatch '[*?]') { @@ -216,8 +221,9 @@ function Get-PathInfo { } if (!$OutputPath -or !$OutputPath.Exists) { - if ($OutputPath) { Write-Error -Exception (New-Object System.Management.Automation.ItemNotFoundException -ArgumentList ('Cannot find path ''{0}'' because it does not exist.' -f $OutputPath.FullName)) -TargetObject $OutputPath.FullName -ErrorId 'PathNotFound' -Category ObjectNotFound } - else { Write-Error -Exception (New-Object System.Management.Automation.ItemNotFoundException -ArgumentList ('Cannot find path ''{0}'' because it does not exist.' -f $AbsolutePath)) -TargetObject $AbsolutePath -ErrorId 'PathNotFound' -Category ObjectNotFound } + if ($OutputPath) { Write-Error -Exception (New-Object System.Management.Automation.ItemNotFoundException -ArgumentList ('Cannot find path ''{0}'' because it does not exist.' -f $OutputPath.FullName)) -TargetObject $OutputPath.FullName -ErrorId 'PathNotFound' -Category ObjectNotFound -ErrorAction $ErrorActionPreference } + elseif ($AbsolutePath) { Write-Error -Exception (New-Object System.Management.Automation.ItemNotFoundException -ArgumentList ('Cannot find path ''{0}'' because it does not exist.' -f $AbsolutePath)) -TargetObject $AbsolutePath -ErrorId 'PathNotFound' -Category ObjectNotFound -ErrorAction $ErrorActionPreference } + else { Write-Error -Exception (New-Object System.Management.Automation.ItemNotFoundException -ArgumentList ('Cannot find path ''{0}'' because it does not exist.' -f $Path)) -TargetObject $Path -ErrorId 'PathNotFound' -Category ObjectNotFound -ErrorAction $ErrorActionPreference } } } @@ -658,7 +664,7 @@ function ConvertTo-PsString { break } ## Convert objects with object initializers - { $_ -is [object] -and ($_.GetConstructors() | foreach { if ($_.IsPublic -and !$_.GetParameters()) { $true } }) } { + { $_ -is [object] -and ($_.GetConstructors() | ForEach-Object { if ($_.IsPublic -and !$_.GetParameters()) { $true } }) } { [void]$OutputString.Append('@{') $iInput = 0 foreach ($Item in ($InputObject | Get-Member -MemberType Property, NoteProperty)) { @@ -790,7 +796,7 @@ function Select-PsBoundParameters { if ($CommandParameterSets) { [System.Collections.Generic.List[string]] $listCommandParameters = New-Object System.Collections.Generic.List[string] foreach ($CommandParameterSet in $CommandParameterSets) { - $listCommandParameters.AddRange([string[]]($CommandInfo.ParameterSets | Where-Object Name -eq $CommandParameterSet | Select-Object -ExpandProperty Parameters | Select-Object -ExpandProperty Name)) + $listCommandParameters.AddRange([string[]]($CommandInfo.ParameterSets | Where-Object Name -EQ $CommandParameterSet | Select-Object -ExpandProperty Parameters | Select-Object -ExpandProperty Name)) } $CommandParameters = $listCommandParameters | Select-Object -Unique } diff --git a/build/Get-PSModuleInfo.ps1 b/build/Get-PSModuleInfo.ps1 index 1eeec0f..b916247 100644 --- a/build/Get-PSModuleInfo.ps1 +++ b/build/Get-PSModuleInfo.ps1 @@ -26,7 +26,7 @@ Write-Host ('##vso[task.setvariable variable=moduleName;isOutput=true]{0}' -f $e Write-Host ('##[debug] {0} = {1}' -f 'moduleName', $env:moduleName) ## Output moduleVersion Azure Pipelines -$env:moduleVersion = $ModuleManifest.ModuleVersion +$env:moduleVersion = $ModuleManifest['ModuleVersion'] Write-Host ('##vso[task.setvariable variable=moduleVersion;isOutput=true]{0}' -f $env:moduleVersion) Write-Host ('##[debug] {0} = {1}' -f 'moduleVersion', $env:moduleVersion) diff --git a/build/Merge-PSModuleNestedModuleScripts.ps1 b/build/Merge-PSModuleNestedModuleScripts.ps1 new file mode 100644 index 0000000..cb0d131 --- /dev/null +++ b/build/Merge-PSModuleNestedModuleScripts.ps1 @@ -0,0 +1,80 @@ +param +( + # Path to Module Manifest + [Parameter(Mandatory = $false)] + [string] $ModuleManifestPath = ".\release\*\*.*.*", + # + [Parameter(Mandatory = $false)] + [string] $OutputModulePath, + # + [Parameter(Mandatory = $false)] + [switch] $MergeWithRootModule, + # + [Parameter(Mandatory = $false)] + [switch] $RemoveNestedModuleScriptFiles +) + +## Initialize +Import-Module "$PSScriptRoot\CommonFunctions.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop + +[System.IO.FileInfo] $ModuleManifestFileInfo = Get-PathInfo $ModuleManifestPath -DefaultFilename "*.psd1" -ErrorAction Stop +#[System.IO.DirectoryInfo] $ModuleSourceDirectoryInfo = $ModuleManifestFileInfo.Directory +#[System.IO.DirectoryInfo] $ModuleOutputDirectoryInfo = $OutputModuleFileInfo.Directory + +## Read Module Manifest +$ModuleManifest = Import-PowerShellDataFile $ModuleManifestFileInfo.FullName + +if ($OutputModulePath) { + [System.IO.FileInfo] $OutputModuleFileInfo = Get-PathInfo $OutputModulePath -InputPathType File -DefaultFilename "$($ModuleManifestFileInfo.BaseName).psm1" -ErrorAction SilentlyContinue +} +else { + [System.IO.FileInfo] $OutputModuleFileInfo = Get-PathInfo $ModuleManifest['RootModule'] -InputPathType File -DefaultDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction SilentlyContinue + if (!$PSBoundParameters.ContainsKey('MergeWithRootModule')) { $MergeWithRootModule = $true } +} + +if ($OutputModuleFileInfo.Extension -eq ".psm1") { + + [System.IO.FileInfo] $RootModuleFileInfo = Get-PathInfo $ModuleManifest['RootModule'] -InputPathType File -DefaultDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction SilentlyContinue + [System.IO.FileInfo[]] $NestedModulesFileInfo = $ModuleManifest['NestedModules'] | Get-PathInfo -InputPathType File -DefaultDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction SilentlyContinue + [System.IO.FileInfo[]] $ScriptsToProcessFileInfo = $ModuleManifest['ScriptsToProcess'] | Get-PathInfo -InputPathType File -DefaultDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction SilentlyContinue + + if ($MergeWithRootModule) { + ## Split module parameters from the rest of the module content + [string] $RootModuleParameters = $null + [string] $RootModuleContent = $null + if ($RootModuleFileInfo.Extension -eq ".psm1" -and (Get-Content $RootModuleFileInfo.FullName -Raw) -match "(?s)^(.*\n?\s*param\s*[(](?:[^()]|(?'Nested'[(])|(?'-Nested'[)]))*[)]\s*)?(.*)$") { + $RootModuleParameters = $Matches[1] + $RootModuleContent = $Matches[2] + } + + $NestedModuleRegion = "#region NestedModules Script(s)`r`n" + + $RootModuleParameters, $NestedModuleRegion | Set-Content $OutputModuleFileInfo.FullName -Encoding utf8BOM + } + + ## Add Nested Module Scripts + $NestedModulesFileInfo | Where-Object Extension -EQ '.ps1' | ForEach-Object { "#region $($_.Name)`r`n`r`n$(Get-Content $_ -Raw)`r`n#endregion`r`n" } | Add-Content $OutputModuleFileInfo.FullName -Encoding utf8BOM + + if ($MergeWithRootModule) { + function Join-ModuleMembers ([string[]]$Members) { + if ($Members.Count -gt 0) { + return "'{0}'" -f ($Members -join "','") + } + else { return "" } + } + + ## Add remainder of root module content + $NestedModuleEndRegion = "#endregion`r`n" + $ExportModuleMember += "Export-ModuleMember -Function @({0}) -Cmdlet @({1}) -Variable @({2}) -Alias @({3})" -f (Join-ModuleMembers $ModuleManifest['FunctionsToExport']), (Join-ModuleMembers $ModuleManifest['CmdletsToExport']), (Join-ModuleMembers $ModuleManifest['VariablesToExport']), (Join-ModuleMembers $ModuleManifest['AliasesToExport']) + + $NestedModuleEndRegion, $RootModuleContent, $ExportModuleMember | Add-Content $OutputModuleFileInfo.FullName -Encoding utf8BOM + } + + if ($RemoveNestedModuleScriptFiles) { + ## Remove Nested Module Scripts + $NestedModulesFileInfo | Where-Object Extension -EQ '.ps1' | Where-Object { !$ScriptsToProcessFileInfo -or $_.FullName -notin $ScriptsToProcessFileInfo.FullName } | Remove-Item + + ## Remove Empty Directories + Get-ChildItem $ModuleManifestFileInfo.DirectoryName -Recurse -Directory | Where-Object { !(Get-ChildItem $_.FullName -Recurse -File) } | Remove-Item -Recurse + } +} diff --git a/build/Publish-PSModule.ps1 b/build/Publish-PSModule.ps1 index 88c227e..b505602 100644 --- a/build/Publish-PSModule.ps1 +++ b/build/Publish-PSModule.ps1 @@ -24,7 +24,7 @@ Import-Module "$PSScriptRoot\CommonFunctions.psm1" -Force -WarningAction Silentl $ModuleManifest = Import-PowerShellDataFile $ModuleManifestFileInfo.FullName ## Install Module Dependencies -foreach ($Module in $ModuleManifest.RequiredModules) { +foreach ($Module in $ModuleManifest['RequiredModules']) { if ($Module -is [hashtable]) { $ModuleName = $Module.ModuleName } else { $ModuleName = $Module } if ($ModuleName -notin $ModuleManifest.PrivateData.PSData['ExternalModuleDependencies'] -and !(Get-Module $ModuleName -ListAvailable)) { @@ -34,7 +34,7 @@ foreach ($Module in $ModuleManifest.RequiredModules) { ## Publish $PSRepositoryAll = Get-PSRepository -$PSRepository = $PSRepositoryAll | Where-Object SourceLocation -like "$RepositorySourceLocation*" +$PSRepository = $PSRepositoryAll | Where-Object SourceLocation -Like "$RepositorySourceLocation*" if (!$PSRepository) { try { [string] $RepositoryName = New-Guid @@ -54,9 +54,9 @@ else { ## Unlist the Package if ($Unlist) { if ($ModuleManifest.PrivateData.PSData['Prerelease']) { - Invoke-RestMethod -Method Delete -Uri ("{0}/{1}/{2}-{3}" -f $PSRepository.PublishLocation, $ModuleManifestFileInfo.BaseName, $ModuleManifest.ModuleVersion, $ModuleManifest.PrivateData.PSData['Prerelease']) -Headers @{ 'X-NuGet-ApiKey' = ConvertFrom-SecureString $NuGetApiKey -AsPlainText } + Invoke-RestMethod -Method Delete -Uri ("{0}/{1}/{2}-{3}" -f $PSRepository.PublishLocation, $ModuleManifestFileInfo.BaseName, $ModuleManifest['ModuleVersion'], $ModuleManifest.PrivateData.PSData['Prerelease']) -Headers @{ 'X-NuGet-ApiKey' = ConvertFrom-SecureString $NuGetApiKey -AsPlainText } } else { - Invoke-RestMethod -Method Delete -Uri ("{0}/{1}/{2}" -f $PSRepository.PublishLocation, $ModuleManifestFileInfo.BaseName, $ModuleManifest.ModuleVersion) -Headers @{ 'X-NuGet-ApiKey' = ConvertFrom-SecureString $NuGetApiKey -AsPlainText } + Invoke-RestMethod -Method Delete -Uri ("{0}/{1}/{2}" -f $PSRepository.PublishLocation, $ModuleManifestFileInfo.BaseName, $ModuleManifest['ModuleVersion']) -Headers @{ 'X-NuGet-ApiKey' = ConvertFrom-SecureString $NuGetApiKey -AsPlainText } } } diff --git a/build/Sign-PSModule.ps1 b/build/Sign-PSModule.ps1 index 312fe0a..ef98982 100644 --- a/build/Sign-PSModule.ps1 +++ b/build/Sign-PSModule.ps1 @@ -2,7 +2,7 @@ param ( # Path to Module Manifest [Parameter(Mandatory = $false)] - [string] $ModuleManifestPath = ".\release\*\*.*.*\*.psd1", + [string] $ModuleManifestPath = ".\release\*\*.*.*", # Specifies the certificate that will be used to sign the script or file. [Parameter(Mandatory = $false)] [object] $SigningCertificate = (Get-ChildItem Cert:\CurrentUser\My\E7413D745138A6DC584530AECE27CEFDDA9D9CD6 -CodeSigningCert), @@ -27,7 +27,7 @@ else { $SigningCertificate = Get-X509Certificate $SigningCertificate -EndEntityC ## Read Module Manifest $ModuleManifest = Import-PowerShellDataFile $ModuleManifestFileInfo.FullName -$FileList = $ModuleManifest.FileList -like "*.ps*1" +$FileList = $ModuleManifest['FileList'] -like "*.ps*1*" for ($i = 0; $i -lt $FileList.Count; $i++) { $FileList[$i] = Join-Path $ModuleManifestFileInfo.DirectoryName $FileList[$i] -Resolve } diff --git a/build/Update-PSModuleManifest.ps1 b/build/Update-PSModuleManifest.ps1 index 52c9bc0..93e2b02 100644 --- a/build/Update-PSModuleManifest.ps1 +++ b/build/Update-PSModuleManifest.ps1 @@ -20,44 +20,54 @@ param ## Initialize Import-Module "$PSScriptRoot\CommonFunctions.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop [hashtable] $paramUpdateModuleManifest = @{ } -if ($Guid) { $paramUpdateModuleManifest['Guid'] = $Guid } -if ($ModuleVersion) { $paramUpdateModuleManifest['ModuleVersion'] = $ModuleVersion } [System.IO.FileInfo] $ModuleManifestFileInfo = Get-PathInfo $ModuleManifestPath -DefaultFilename "*.psd1" -ErrorAction Stop +#[System.IO.DirectoryInfo] $ModuleOutputDirectoryInfo = $ModuleManifestFileInfo.Directory ## Read Module Manifest $ModuleManifest = Import-PowerShellDataFile $ModuleManifestFileInfo.FullName +$paramUpdateModuleManifest['NestedModules'] = $ModuleManifest['NestedModules'] | Where-Object { $null -ne $_ -and (Get-PathInfo $_ -DefaultDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction Ignore).Exists } +$paramUpdateModuleManifest['FunctionsToExport'] = $ModuleManifest['FunctionsToExport'] +$paramUpdateModuleManifest['CmdletsToExport'] = $ModuleManifest['CmdletsToExport'] +$paramUpdateModuleManifest['AliasesToExport'] = $ModuleManifest['AliasesToExport'] if ($ModuleManifest.PrivateData.PSData['Prerelease'] -eq 'source') { $paramUpdateModuleManifest['Prerelease'] = " " } + +## Override from Parameters +if ($Guid) { $paramUpdateModuleManifest['Guid'] = $Guid } +if ($ModuleVersion) { $paramUpdateModuleManifest['ModuleVersion'] = $ModuleVersion } if ($Prerelease) { $paramUpdateModuleManifest['Prerelease'] = $Prerelease } -if ($ModuleManifest.NestedModules) { $paramUpdateModuleManifest['NestedModules'] = $ModuleManifest.NestedModules } -$paramUpdateModuleManifest['FunctionsToExport'] = $ModuleManifest.FunctionsToExport -$paramUpdateModuleManifest['CmdletsToExport'] = $ModuleManifest.CmdletsToExport -$paramUpdateModuleManifest['AliasesToExport'] = $ModuleManifest.AliasesToExport -[System.IO.DirectoryInfo] $ModuleOutputDirectoryInfo = $ModuleManifestFileInfo.Directory ## Get Module Output FileList -$ModuleFileListFileInfo = Get-ChildItem $ModuleOutputDirectoryInfo.FullName -Recurse -File -$ModuleRequiredAssembliesFileInfo = $ModuleFileListFileInfo | Where-Object Extension -eq '.dll' +$ModuleFileListFileInfo = Get-ChildItem $ModuleManifestFileInfo.DirectoryName -Recurse -File +$ModuleRequiredAssembliesFileInfo = $ModuleFileListFileInfo | Where-Object Extension -EQ '.dll' ## Get Paths Relative to Module Base Directory -$ModuleFileList = Get-RelativePath $ModuleFileListFileInfo.FullName -WorkingDirectory $ModuleOutputDirectoryInfo.FullName -ErrorAction Stop +$ModuleFileList = Get-RelativePath $ModuleFileListFileInfo.FullName -WorkingDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction Stop $ModuleFileList = $ModuleFileList -replace '\\net45\\', '\!!!\' -replace '\\netcoreapp2.1\\', '\net45\' -replace '\\!!!\\', '\netcoreapp2.1\' # PowerShell Core fails to load assembly if net45 dll comes before netcoreapp2.1 dll in the FileList. $paramUpdateModuleManifest['FileList'] = $ModuleFileList ## Generate RequiredAssemblies list based on existing items and file list -$paramUpdateModuleManifest['RequiredAssemblies'] += $ModuleManifest.RequiredAssemblies | Where-Object { $_ -notin $ModuleFileListFileInfo.Name } +$paramUpdateModuleManifest['RequiredAssemblies'] = $ModuleManifest['RequiredAssemblies'] | Where-Object { $_ -notin $ModuleFileListFileInfo.Name } if (!$SkipRequiredAssembliesDetection -and $ModuleRequiredAssembliesFileInfo) { - $ModuleRequiredAssemblies = Get-RelativePath $ModuleRequiredAssembliesFileInfo.FullName -WorkingDirectory $ModuleOutputDirectoryInfo.FullName -ErrorAction Stop + $ModuleRequiredAssemblies = Get-RelativePath $ModuleRequiredAssembliesFileInfo.FullName -WorkingDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction Stop $paramUpdateModuleManifest['RequiredAssemblies'] += $ModuleRequiredAssemblies } -if (!$paramUpdateModuleManifest['RequiredAssemblies']) { $paramUpdateModuleManifest.Remove('RequiredAssemblies') } -## Clear RequiredAssemblies -(Get-Content $ModuleManifestFileInfo.FullName -Raw) -replace "(?s)RequiredAssemblies\ =\ @\([^)]*\)", "# RequiredAssemblies = @()" | Set-Content $ModuleManifestFileInfo.FullName -(Get-Content $ModuleManifestFileInfo.FullName -Raw) -replace "(?s)FileList\ =\ @\([^)]*\)", "# FileList = @()" | Set-Content $ModuleManifestFileInfo.FullName +## Clear Existing RequiredAssemblies, NestedModules, and FileList +if ($paramUpdateModuleManifest.ContainsKey('RequiredAssemblies')) { + if (!$paramUpdateModuleManifest['RequiredAssemblies']) { $paramUpdateModuleManifest.Remove('RequiredAssemblies') } + (Get-Content $ModuleManifestFileInfo.FullName -Raw) -replace "(?s)(#\s*)?RequiredAssemblies\s*=\s*@\([^)]*\)", "# RequiredAssemblies = @()" | Set-Content $ModuleManifestFileInfo.FullName +} +if ($paramUpdateModuleManifest.ContainsKey('NestedModules') -and !$paramUpdateModuleManifest['NestedModules']) { + $paramUpdateModuleManifest.Remove('NestedModules') + (Get-Content $ModuleManifestFileInfo.FullName -Raw) -replace "(?s)(#\s*)?NestedModules\s*=\s*@\([^)]*\)", "# NestedModules = @()" | Set-Content $ModuleManifestFileInfo.FullName +} +if ($paramUpdateModuleManifest.ContainsKey('FileList')) { + (Get-Content $ModuleManifestFileInfo.FullName -Raw) -replace "(?s)(#\s*)?FileList\s*=\s*@\([^)]*\)", "# FileList = @()" | Set-Content $ModuleManifestFileInfo.FullName +} ## Install Module Dependencies -foreach ($Module in $ModuleManifest.RequiredModules) { +foreach ($Module in $ModuleManifest['RequiredModules']) { if ($Module -is [hashtable]) { $ModuleName = $Module.ModuleName } else { $ModuleName = $Module } if ($ModuleName -notin $ModuleManifest.PrivateData.PSData['ExternalModuleDependencies'] -and !(Get-Module $ModuleName -ListAvailable)) { diff --git a/build/azure-pipelines/template-psmodule-build.yml b/build/azure-pipelines/template-psmodule-build.yml index fc808fa..36b1e27 100644 --- a/build/azure-pipelines/template-psmodule-build.yml +++ b/build/azure-pipelines/template-psmodule-build.yml @@ -36,6 +36,13 @@ steps: TargetFolder: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}' preserveTimestamp: true +- task: PowerShell@2 + displayName: 'Merge PowerShell Module Nested Module Scripts' + inputs: + filePath: '$(System.DefaultWorkingDirectory)/build/Merge-PSModuleNestedModuleScripts.ps1' + arguments: '-ModuleManifestPath "$(Pipeline.Workspace)/${{ parameters.artifactOutput }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}.psd1" -MergeWithRootModule -RemoveNestedModuleScriptFiles' + pwsh: true + - task: CopyFiles@2 displayName: 'Copy LICENSE to Staging' inputs: @@ -84,6 +91,13 @@ steps: arguments: '-ModuleManifestPath "$(Pipeline.Workspace)/${{ parameters.artifactOutput }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}.psd1" -Guid "${{ parameters.moduleGuid }}" -ModuleVersion "${{ parameters.moduleVersion }}" -Prerelease "${{ parameters.prereleaseTag }}"' pwsh: true +- task: PowerShell@2 + displayName: 'Add PowerShell Module Header' + inputs: + filePath: '$(System.DefaultWorkingDirectory)/build/Add-PSModuleHeader.ps1' + arguments: '-ModuleManifestPath "$(Pipeline.Workspace)/${{ parameters.artifactOutput }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}.psd1"' + pwsh: true + - ${{ if parameters.GenerateManifest }}: - task: ManifestGeneratorTask@0 displayName: 'Generate Software Bill of Materials (SBOM)' diff --git a/build/azure-pipelines/template-psmodule-sign.yml b/build/azure-pipelines/template-psmodule-sign.yml index e4c8d60..a0549a3 100644 --- a/build/azure-pipelines/template-psmodule-sign.yml +++ b/build/azure-pipelines/template-psmodule-sign.yml @@ -1,4 +1,4 @@ -# PowerShell Module Package Pipeline Template +# PowerShell Module Sign Pipeline Template # https://aka.ms/yaml parameters: @@ -97,12 +97,14 @@ steps: - pwsh: 'New-FileCatalog "$env:StagingDirectory/$env:ModuleName/$env:ModuleName.cat" -Path "$env:StagingDirectory/$env:ModuleName" -CatalogVersion 2.0' displayName: 'Create File Catalog' + condition: 'and(succeeded(), eq(variables[''Agent.OS''], ''Windows_NT''))' env: StagingDirectory: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}' ModuleName: '${{ parameters.moduleName }}' - task: EsrpCodeSigning@2 displayName: 'Sign File Catalog' + condition: 'and(succeeded(), eq(variables[''Agent.OS''], ''Windows_NT''))' inputs: ConnectedServiceName: '${{ parameters.EsrpCodeSigningServiceName }}' FolderPath: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}'