Includes/PwSh.Fw.Build.Powershell.psm1
<#
.SYNOPSIS Convert a generic hashtable into useful ModuleSettings metadata .DESCRIPTION Extract from an object useful properties to use as a module manifest settings .PARAMETER Metadata object filled with various properties .EXAMPLE $project = gc ./project.yml -raw | convertfrom-yaml $project | ConvertTo-PowershellModuleSettings This example will convert a project definition file into a useable hashtable to inject into Update-ModuleManifest .NOTES General notes #> function ConvertTo-PowershellModuleSettings { [CmdletBinding()][OutputType([hashtable])]Param ( [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$Metadata ) Begin { } Process { $ModuleSettings = @{} if ($Metadata) { # if ($Metadata.name) { $ModuleSettings.Name = $Metadata.name } if ($Metadata.Version) { $ModuleSettings.ModuleVersion = $Metadata.Version } if ($Metadata.ModuleVersion) { $ModuleSettings.ModuleVersion = $Metadata.ModuleVersion } if ($Metadata.GUID) { $ModuleSettings.GUID = $Metadata.GUID } if ($Metadata.NestedModules) { $ModuleSettings.NestedModules = $Metadata.NestedModules } if ($Metadata.Author) { $ModuleSettings.Author = $Metadata.Author } if ($Metadata.owner) { $ModuleSettings.Author = $Metadata.owner } if ($Metadata.CompanyName) { $ModuleSettings.CompanyName = $Metadata.CompanyName } if ($Metadata.Copyright) { $ModuleSettings.Copyright = $Metadata.Copyright } if ($Metadata.Description) { $ModuleSettings.Description = $Metadata.Description } if ($Metadata.ProcessorArchitecture) { $ModuleSettings.ProcessorArchitecture = $Metadata.ProcessorArchitecture } if ($Metadata.Architecture) { $ModuleSettings.ProcessorArchitecture = $Metadata.Architecture } if ($Metadata.Arch) { $ModuleSettings.ProcessorArchitecture = $Metadata.Arch } # translate Arch to correct values @see @url https://docs.microsoft.com/fr-fr/powershell/scripting/developer/module/how-to-write-a-powershell-module-manifest switch ($ModuleSettings.ProcessorArchitecture) { 'all' { $ModuleSettings.ProcessorArchitecture = 'None' } 'x64' { $ModuleSettings.ProcessorArchitecture = 'AMD64' } 'arm32' { $ModuleSettings.ProcessorArchitecture = 'ARM' } 'arm64' { $ModuleSettings.ProcessorArchitecture = 'ARM' } } if ($Metadata.RequiredModules) { $ModuleSettings.RequiredModules = $Metadata.RequiredModules } if ($Metadata.Depends) { # to be a valid RequiredModule, the module must be installed on the system $Metadata.Depends | ForEach-Object { Install-Module -Name $_ -Repository PSGallery } $ModuleSettings.RequiredModules = $Metadata.Depends } if ($Metadata.PrivateData) { $ModuleSettings.PrivateData = $Metadata.PrivateData } if ($Metadata.Tags) { $ModuleSettings.Tags = $Metadata.Tags } if ($Metadata.ProjectUri) { $ModuleSettings.ProjectUri = $Metadata.ProjectUri } if ($Metadata.ProjectUrl) { $ModuleSettings.ProjectUri = $Metadata.ProjectUrl } if ($Metadata.LicenseUri) { $ModuleSettings.LicenseUri = $Metadata.LicenseUri } if ($Metadata.LicenseUrl) { $ModuleSettings.LicenseUri = $Metadata.LicenseUrl } if ($Metadata.IconUri) { $ModuleSettings.IconUri = $Metadata.IconUri } if ($Metadata.IconUrl) { $ModuleSettings.IconUri = $Metadata.IconUrl } if ($Metadata.ReleaseNotes) { $ModuleSettings.ReleaseNotes = $Metadata.ReleaseNotes } # Powershell < 6 does not know -Prerelease argument if ($PSVersionTable.PSVersion.Major -ge 6) { if ($Metadata.Prerelease) { $ModuleSettings.Prerelease = $Metadata.Prerelease } } else { $ModuleSettings.Remove('Prerelease') } if ($Metadata.HelpInfoUri) { $ModuleSettings.HelpInfoUri = $Metadata.HelpInfoUri } } return $ModuleSettings } End { } } <# .SYNOPSIS Create / Update module manifest. It is a wrapper of Microsoft's Update-ModuleManifest .DESCRIPTION The Update-ModuleManifestEx extends the Microsoft cmdlet Update-ModuleManifest. Actually, Update-ModuleManifestEx is a wrapper of Update-ModuleManifest (and New-ModuleManifest). It : * checks if files exists * update / create module manifest * export exact list of functions and aliases, not '*' * trims spaces at end of lines of module psm1 and psd1 files .PARAMETER FullyQualifiedName Full path to module file. Either an already existing manifest (.psd1), or a module content (.psm1) .PARAMETER Metadata Metadata of manifest to use. See lists @url https://docs.microsoft.com/en-us/powershell/module/powershellget/update-modulemanifest .PARAMETER PreserveMetadata If specified, instruct Update-ModuleManifestEx to not override Metadata already present in Module Manifest. It only refresh functions to export and aliases to export. .PARAMETER PassThru If specified, return the module object. If not specified, return the path of the module manifest file. .PARAMETER CreateOnly Only create new module manifest. If specified, ignore existing module manifest. They will not be updated. NOTE: if neither CreateOnly or UpdateOnly parameter are specified, then all module manifest will be created/updated, as if both -CreateOnly and -UpdateOnly would have been used. .PARAMETER UpdateOnly Only update existing manifest. If specified, ignore missing module manifest. They will not be created. NOTE: if neither CreateOnly or UpdateOnly parameter are specified, then all module manifest will be created/updated, as if both -CreateOnly and -UpdateOnly would have been used. .EXAMPLE Update-ModuleManifestEx -FullyQualifiedName /path/to/my/module.psm1 -Metadata $Project .OUTPUTS System.Management.Automation.PSModuleInfo This cmdlet returns objects that represent modules. .NOTES General notes #> function Update-ModuleManifestEx { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] [OutputType([Boolean], [String], [PSModuleInfo])]Param ( [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][string]$FullyQualifiedName, [AllowNull()] [Parameter(Mandatory = $false, ValueFromPipeLine = $true)][object]$Metadata, [switch]$PreserveMetadata, [switch]$CreateOnly, [switch]$UpdateOnly, [switch]$PassThru ) Begin { Write-EnterFunction if ($CreateOnly -eq $UpdateOnly) { $CreateOnly = $UpdateOnly = $true } } Process { # test and inspect FullyQualidiedName $rc = Test-Path -Path $FullyQualifiedName -PathType Leaf -ErrorAction SilentlyContinue if ($rc -eq $false) { Write-Error "'$FullyQualifiedName' does not exist." return $false } $file = Get-Item $FullyQualifiedName if (!($file)) { Write-Error "cannot get '$FullyQualifiedName' item." return $false } # Merge global metadata (if specified) with module metadata (if exists) if (Test-FileExist "$($file.DirectoryName)/../$($file.Basename)") { $yaml = Get-Content "$($file.DirectoryName)/../$($file.Basename)" -Raw | ConvertFrom-Yaml if ($Metadata) { $Metadata = Merge-Hashtables $Metadata $yaml } else { $Metadata = $yaml } } # fill / translate metadata to module settings if ($Metadata) { $ModuleSettings = ConvertTo-PowershellModuleSettings -Metadata $Metadata } # load dependencies before loading module itself if ($Metadata.depends) { # $Metadata.depends | Import-Module # Import-Module seems to no longer honor PSModulePath # Get-Module -ListAvailable still do tho, so try to import latest one $Metadata.depends | ForEach-Object { Get-Module -ListAvailable $_ | Sort-Object -Property Version | Select-Object -Last 1 } } # in case we import a module psm1 without psd1, we need to first import-it before getting-it # if we don't do that, $module will be empty $module = Import-Module -FullyQualifiedName "$FullyQualifiedName" -Force:$true -DisableNameChecking -PassThru if (!(Test-ModuleObject -Module $module)) { return $null } $rc = $? # Write-Info "Updating module $($module.Name)" if ($rc -eq $true) { switch ($file.Extension) { '.psd1' { $ACTION = "update" } '.psm1' { if (Test-Path "$($module.ModuleBase)/$($module.Name).psd1" -Type Leaf) { $ACTION = "update" } else { $ACTION = "create" } break } default { Throw "The file extension $($file.Extension) is not a Powershell module extension." } } # edevel("ACTION = $ACTION") Write-Debug "ACTION = $ACTION" Write-Output "$ACTION '$($module.ModuleBase)/$($module.Name).psd1'" # edevel ("Functions list :") $functionsList = Get-Command -Module $module.Name # $functionsList.Name | ForEach-Object { edevel $_} # edevel ("Aliases list :") $aliasesList = Get-Alias | Where-Object { $_.ModuleName -eq $module.Name } # $aliasesList.Name | ForEach-Object { edevel $_} if ($aliasesList.count -eq 0) { $aliasesList = '' } # handle privateData # Private metadata handling does not work, # @see issue with New-ModuleManifest @url https://github.com/PowerShell/PowerShell/issues/5922 # and issue with Update-ModuleManifest @url https://github.com/PowerShell/PowerShellGet/issues/294 # $PrivateData = @{} # if ($null -ne $Metadata) { # $PrivateData.PSData = @{} # $PrivateData.PSData.licenseUri = $Metadata.LICENSEURL # $PrivateData.PSData.projectUri = $Metadata.URL # $PrivateData.PSData.iconUri = $Metadata.ICONURL # $PrivateData.PSData.description = $Metadata.DESCRIPTION # $PrivateData.PSData.releaseNotes = $Metadata.RELEASENOTES # $PrivateData.PSData.tags = $Metadata.TAGS # } switch ($ACTION) { 'create' { if ($CreateOnly) { if ($PSCmdlet.ShouldProcess("ShouldProcess?")) { New-ModuleManifest -RootModule "$($module.Name).psm1" -Path "$($module.ModuleBase)/$($module.Name).psd1" -FunctionsToExport $functionsList -AliasesToExport $aliasesList @ModuleSettings $rc = $? } } } 'update' { if ($UpdateOnly) { if ($PSCmdlet.ShouldProcess("ShouldProcess?")) { if ($PreserveMetadata) { Update-ModuleManifest -Path "$($module.ModuleBase)/$($module.Name).psd1" -FunctionsToExport $functionsList -AliasesToExport $aliasesList } else { Update-ModuleManifest -Path "$($module.ModuleBase)/$($module.Name).psd1" -FunctionsToExport $functionsList -AliasesToExport $aliasesList @ModuleSettings } $rc = $? } } } default { Throw "ACTION '$ACTION' is not supported." } } # trim trailing spaces if (Test-Path "$($module.ModuleBase)/$($module.Name).psd1" -Type Leaf) { $content = Get-Content "$($module.ModuleBase)/$($module.Name).psd1" $content | ForEach-Object {$_.TrimEnd()} | Set-Content "$($module.ModuleBase)/$($module.Name).psd1" } else { Write-Host -ForegroundColor Red "Module '$($module.ModuleBase)/$($module.Name).psd1' manifest not found." } $content = Get-Content "$($module.ModuleBase)/$($module.Name).psm1" $content | ForEach-Object {$_.TrimEnd()} | Set-Content "$($module.ModuleBase)/$($module.Name).psm1" } # eend $? $module = Get-Module -ListAvailable -FullyQualifiedName "$($module.ModuleBase)/$($module.Name).psd1" if ($PassThru) { return $module } else { return $module.Path } } End { Write-LeaveFunction } } <# .SYNOPSIS Update a (meta) module manifest. This function is deprecated since PwSh.Fw.BuildHelpers v1.5.0. Use Update-MetaModuleManifest instead. .DESCRIPTION This function is deprecated since PwSh.Fw.BuildHelpers v1.5.0. Use Update-MetaModuleManifest instead. #> function Update-ModuleManifestRecurse { [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([Boolean], [String], [PSModuleInfo])]Param ( [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][string]$FullyQualifiedName, [AllowNull()] [Parameter(Mandatory = $false, ValueFromPipeLine = $true)][object]$Metadata, [switch]$CreateOnly, [switch]$UpdateOnly, [switch]$Recurse, [switch]$PassThru ) Begin { Write-EnterFunction Write-Warning "This function is deprecated since PwSh.Fw.BuidHelpers v1.5.0" Write-Warning "Please use Update-MetaModuleManifest instead" Write-Warning "The parameters are the same, so for the moment you just need to rename the function calls." } Process { return Update-MetaModuleManifest @PSBoundParameters } End { Write-LeaveFunction } } <# .SYNOPSIS Update a (meta) module manifest .DESCRIPTION A meta module is a module containing one or more child modules in subdirectories. It can embed nested modules. This function take care of this inclusion. It can recurse down directories to find nested modules to * create / update nested modules manifest * export functions and aliases to main module according to the subfolder name (see Recurse parameter for more informations) .PARAMETER FullyQualifiedName Full path to module file. Either an already existing manifest (.psd1), or a module content (.psm1) .PARAMETER Metadata Metadata of manifest to use. See lists @url https://docs.microsoft.com/en-us/powershell/module/powershellget/update-modulemanifest .PARAMETER Recurse If specified, instruct Update-ModuleManifestRecurse to recurse through well-known folders to find nested modules. Following policies apply : * modules found in Includes subdirectory will export their functions and alias to main module to be available publicly * modules found in Private sudirectory will not export functions to main module. Functions will remain private, but availables inside main module code. .PARAMETER PassThru If specified, return the module manifest object. If not specified, return the path of the module manifest file. .EXAMPLE Update-ModuleManifestRecurse -FullyQualifiedName /path/to/my/module.psm1 -Metadata $Project .OUTPUTS System.Management.Automation.PSModuleInfo This cmdlet returns objects that represent modules. .NOTES General notes #> function Update-MetaModuleManifest { [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([Boolean], [String], [PSModuleInfo])]Param ( [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][string]$FullyQualifiedName, [AllowNull()] [Parameter(Mandatory = $false, ValueFromPipeLine = $true)][object]$Metadata, [switch]$CreateOnly, [switch]$UpdateOnly, [switch]$Recurse, [switch]$PassThru ) Begin { Write-EnterFunction } Process { Write-Verbose "FullyQualifiedName = $FullyQualifiedName" try { $mainModule = Update-ModuleManifestEx -FullyQualifiedName $FullyQualifiedName -Metadata $Metadata -PassThru -CreateOnly:$CreateOnly -UpdateOnly:$UpdateOnly } catch { Write-Error $_ return $null } if (!($mainModule)) { Write-Error "An error occured while updating manifest of '$FullyQualifiedName'." return $false } $FunctionsToExport = $mainModule.ExportedFunctions.Values.Name $AliasesToExport = $mainModule.ExportedAliases.Values.Name if ($Recurse) { $NestedModules = @() # $RequiredModules = @() Get-ChildItem -Path $mainModule.ModuleBase -Recurse -Name "*.psm1" -Exclude $mainModule.RootModule | ForEach-Object { $m = $_ Write-Info "--> Found $m" # skip mainModule if ($mainModule.RootModule -eq $_) { continue } try { $subModule = Update-ModuleManifestEx -FullyQualifiedName "$($mainModule.ModuleBase)/$_" -Metadata $Metadata -PreserveMetadata -PassThru -CreateOnly:$CreateOnly -UpdateOnly:$UpdateOnly # $folder = ($m -split [io.path]::DirectorySeparatorChar)[0] $folder = Split-Path -Parent $m # treat Includes as RequiredModules # $psm = Get-Item -Path "$($mainModule.ModuleBase)/$($_)" Write-Info "folder = $folder" switch ($folder) { 'Includes' { # $RequiredModules += $m $NestedModules += @("." + [io.path]::DirectorySeparatorChar + "$m") $FunctionsToExport += $subModule.ExportedFunctions.Values.Name $AliasesToExport += $subModule.ExportedAliases.Values.Name } 'Private' { $NestedModules += @("." + [io.path]::DirectorySeparatorChar + "$m") } default { } } # clean tracks Remove-Module -Name $subModule.Name } catch { Write-Error $_ return $null } } # finally update the mainModule with functions and aliases found in NestedModules # if ($RequiredModules) { # # $RequiredModules | fl # Update-ModuleManifest -Path $mainModule.Path -RequiredModules $RequiredModules # } if ($NestedModules.Count -gt 0) { if ($PSCmdlet.ShouldProcess("ShouldProcess?")) { try { Update-ModuleManifest -Path $mainModule.Path -NestedModules $NestedModules -FunctionsToExport ($FunctionsToExport | Sort-Object) -AliasesToExport ($AliasesToExport | Sort-Object) } catch { Write-Error $_ return $null } } } } $mainModule = Get-Module -ListAvailable -FullyQualifiedName "$($mainModule.ModuleBase)/$($mainModule.Name).psd1" if ($PassThru) { return $mainModule } else { return $mainModule.Path } } End { Write-LeaveFunction } } <# .SYNOPSIS Create or update module manifest recursively, with simple syntax .DESCRIPTION Bulk create or update module manifest at once ! It can merge global metadata with user-defined module-specific ones. It can just create manifest if it does not exist, or just update manifest if it DOES exist, or both. Recursion is optional, but recommended. .EXAMPLE New-ModuleManifestRecurse -Path /Path/to/Modules .NOTES General notes #> function New-ModuleManifestRecurse { [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([void])]Param ( # Path to start looking for Modules [Parameter(Mandatory = $true, ValueFromPipeLine = $true)] [string]$Path, # Global Metadata, possibliy merged with module specific metadata if found [Parameter(Mandatory = $false, ValueFromPipeLine = $true)] [object]$Metadata, # Only create new manifest, do not update existing ones [switch]$CreateOnly, # Only update existing manifest, do not create new ones [switch]$UpdateOnly, # Limit depth of subdirectories searching for modules [UInt16]$MaxDepth = 99 ) Begin { Write-EnterFunction } Process { $modules = Get-ChildItem "$Path" -name "*.psm1" -recurse -depth $MaxDepth | ForEach-Object { Get-Item "$Path/$_" } ForEach ($module in $modules) { Write-Debug "Found $($module.Name)" $manifestFile = Update-MetaModuleManifest -FullyQualifiedName "$($module.DirectoryName)/$($module.BaseName).psm1" -Metadata $Metadata -CreateOnly:$CreateOnly -UpdateOnly:$UpdateOnly # Write-Devel "-> $($module.BaseName)" } } End { Write-LeaveFunction } } function Test-ModuleObject { [CmdletBinding()][OutputType([Boolean])]Param ( [AllowNull()] [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][PSModuleInfo]$Module ) Begin { Write-EnterFunction } Process { $rc = $true everbose "Testing module '$($Module.Name)'" if ($null -eq $Module) { Write-Host -ForegroundColor Red "[-] Module is null." $rc = $false } else { Write-Host -ForegroundColor Green "[+] Module is not null" } if ([string]::IsNullOrEmpty($Module.Name)) { Write-Host -ForegroundColor Red "[-] Module name is empty" $rc = $false } else { Write-Host -ForegroundColor Green "[+] Module name is not empty" } if ([string]::IsNullOrEmpty($Module.ModuleBase)) { Write-Host -ForegroundColor Red "[-] ModuleBase is empty (VERY DANGEROUS)" $rc = $false } else { Write-Host -ForegroundColor Green "[+] ModuleBase seems ok (part #1)" } if ($Module.ModuleBase -eq '/') { Write-Host -ForegroundColor Red "[-] ModuleBase is '/' (VERY DANGEROUS)" $rc = $false } else { Write-Host -ForegroundColor Green "[+] ModuleBase seems ok (part #2)" } if ($Module.ModuleBase -eq $env:SystemRoot) { Write-Host -ForegroundColor Red "[-] ModuleBase is '$($env:SystemRoot)' (VERY DANGEROUS)" $rc = $false } else { Write-Host -ForegroundColor Green "[+] ModuleBase seems ok (part #3)" } return $rc } End { Write-LeaveFunction } } |