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 $_ } $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 -gt 5) { if ($Metadata.Prerelease) { $ModuleSettings.Prerelease = $Metadata.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 Metada 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, [Parameter(Mandatory = $false, ValueFromPipeLine = $true)][object]$Metadata, [switch]$PreserveMetadata, [switch]$CreateOnly, [switch]$UpdateOnly, [switch]$PassThru ) Begin { # eenter($MyInvocation.MyCommand) if ($CreateOnly -eq $UpdateOnly) { $CreateOnly = $UpdateOnly = $true } } Process { # fill / translate metadata to module settings $ModuleSettings = ConvertTo-PowershellModuleSettings -Metadata $Metadata $rc = Test-Path -Path $FullyQualifiedName -PathType Leaf -ErrorAction SilentlyContinue if ($rc -eq $false) { Write-Host -ForegroundColor Red "'$FullyQualifiedName' does not exist." return $false } $file = Get-Item $FullyQualifiedName if (!($file)) { Write-Host -ForegroundColor Red "cannot get '$FullyQualifiedName' item." return $false } # load dependencies before loading module itself if ($Metadata.depends) { $Metadata.depends | Import-Module } # in case we import a module psm1 wthout 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-Information "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 { # eleave($MyInvocation.MyCommand) } } <# .SYNOPSIS Update a module manifest with recursivity .DESCRIPTION A module 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 .PARAMETER FullyQualifiedName Full path to module file. Either an already existing manifest (.psd1), or a module content (.psm1) .PARAMETER Metada Metada 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 * modules found in Private sudirectory will not export anything to main module .PARAMETER PassThru If specified, return the module 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-ModuleManifestRecurse { [CmdletBinding(SupportsShouldProcess = $true)][OutputType([String], [boolean])]Param ( [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][string]$FullyQualifiedName, [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 { eerror $_ 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-Information "--> Found $m" # skip mainModule if ($mainModule.RootModule -eq $_) { continue } try { $subModule = Update-ModuleManifestEx -FullyQualifiedName "$($mainModule.ModuleBase)/$_" -Metadata $Metadata -PreserveMetadata -PassThru -CreateOnly:$CreateOnly -UpdateOnly:$UpdateOnly } catch { eerror $_ return $null } # $folder = ($m -split [io.path]::DirectorySeparatorChar)[0] $folder = Split-Path -Parent $m # treat Includes as RequiredModules # $psm = Get-Item -Path "$($mainModule.ModuleBase)/$($_)" Write-Information "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 } # 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 { eerror $_ return $null } } } } $mainModule = Get-Module -ListAvailable -FullyQualifiedName "$($mainModule.ModuleBase)/$($mainModule.Name).psd1" if ($PassThru) { return $mainModule } else { return $mainModule.Path } } 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 } } |