From 07fe4e35dcbe7cfac0f6af707b6c4703f9321e05 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 9 Mar 2024 18:59:37 +0100 Subject: [PATCH 1/2] Add Build-PSmodule to Utilities --- .gitignore | 2 +- .../private/PSModule/Add-ContentFromItem.ps1 | 59 +++ .../private/PSModule/Build-PSModuleBase.ps1 | 35 ++ .../PSModule/Build-PSModuleDocumentation.ps1 | 41 +++ .../PSModule/Build-PSModuleManifest.ps1 | 341 ++++++++++++++++++ .../PSModule/Build-PSModuleRootModule.ps1 | 169 +++++++++ .../private/PSModule/ConvertTo-Hashtable.ps1 | 32 ++ .../PSModule/Get-PSModuleAliasesToExport.ps1 | 34 ++ .../PSModule/Get-PSModuleCmdletsToExport.ps1 | 34 ++ .../Get-PSModuleFunctionsToExport.ps1 | 40 ++ .../PSModule/Get-PSModuleRootModule.ps1 | 54 +++ .../Get-PSModuleVariablesToExport.ps1 | 35 ++ .../private/PSModule/Import-PSModule.ps1 | 46 +++ .../PSModule/PSScriptAnalyzer.Tests.psd1 | 56 +++ .../PSModule/Resolve-PSModuleDependency.ps1 | 60 +++ .../public/PSModule/Build-PSModule.ps1 | 47 +++ 16 files changed, 1084 insertions(+), 1 deletion(-) create mode 100644 src/Utilities/private/PSModule/Add-ContentFromItem.ps1 create mode 100644 src/Utilities/private/PSModule/Build-PSModuleBase.ps1 create mode 100644 src/Utilities/private/PSModule/Build-PSModuleDocumentation.ps1 create mode 100644 src/Utilities/private/PSModule/Build-PSModuleManifest.ps1 create mode 100644 src/Utilities/private/PSModule/Build-PSModuleRootModule.ps1 create mode 100644 src/Utilities/private/PSModule/ConvertTo-Hashtable.ps1 create mode 100644 src/Utilities/private/PSModule/Get-PSModuleAliasesToExport.ps1 create mode 100644 src/Utilities/private/PSModule/Get-PSModuleCmdletsToExport.ps1 create mode 100644 src/Utilities/private/PSModule/Get-PSModuleFunctionsToExport.ps1 create mode 100644 src/Utilities/private/PSModule/Get-PSModuleRootModule.ps1 create mode 100644 src/Utilities/private/PSModule/Get-PSModuleVariablesToExport.ps1 create mode 100644 src/Utilities/private/PSModule/Import-PSModule.ps1 create mode 100644 src/Utilities/private/PSModule/PSScriptAnalyzer.Tests.psd1 create mode 100644 src/Utilities/private/PSModule/Resolve-PSModuleDependency.ps1 create mode 100644 src/Utilities/public/PSModule/Build-PSModule.ps1 diff --git a/.gitignore b/.gitignore index 907bfea..c2d1f8d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,5 @@ # Local History for Visual Studio Code .history/* -# The Powershell build outputs folder +# PSModule framework outputs folder outputs/* diff --git a/src/Utilities/private/PSModule/Add-ContentFromItem.ps1 b/src/Utilities/private/PSModule/Add-ContentFromItem.ps1 new file mode 100644 index 0000000..55efc10 --- /dev/null +++ b/src/Utilities/private/PSModule/Add-ContentFromItem.ps1 @@ -0,0 +1,59 @@ +function Add-ContentFromItem { + <# + .SYNOPSIS + Add the content of a folder or file to the root module file. + + .DESCRIPTION + This function will add the content of a folder or file to the root module file. + + .EXAMPLE + Add-ContentFromItem -Path 'C:\MyModule\src\MyModule' -RootModuleFilePath 'C:\MyModule\src\MyModule.psm1' -RootPath 'C:\MyModule\src' + #> + param( + # The path to the folder or file to process. + [Parameter(Mandatory)] + [string] $Path, + + # The path to the root module file. + [Parameter(Mandatory)] + [string] $RootModuleFilePath, + + # The root path of the module. + [Parameter(Mandatory)] + [string] $RootPath + ) + $relativeFolderPath = $Path.Replace($RootPath, '').TrimStart($pathSeparator) + + Add-Content -Path $RootModuleFilePath -Force -Value @" +#region - From $relativeFolderPath +Write-Verbose "[`$scriptName] - [$relativeFolderPath] - Processing folder" + +"@ + + $subFolders = $Path | Get-ChildItem -Directory -Force | Sort-Object -Property Name + foreach ($subFolder in $subFolders) { + Add-ContentFromItem -Path $subFolder.FullName -RootModuleFilePath $RootModuleFilePath -RootPath $RootPath + } + + $files = $Path | Get-ChildItem -File -Force -Filter '*.ps1' | Sort-Object -Property FullName + foreach ($file in $files) { + $relativeFilePath = $file.FullName.Replace($RootPath, '').TrimStart($pathSeparator) + Add-Content -Path $RootModuleFilePath -Force -Value @" +#region - From $relativeFilePath +Write-Verbose "[`$scriptName] - [$relativeFilePath] - Importing" + +"@ + Get-Content -Path $file.FullName | Add-Content -Path $RootModuleFilePath -Force + Add-Content -Path $RootModuleFilePath -Value @" + +Write-Verbose "[`$scriptName] - [$relativeFilePath] - Done" +#endregion - From $relativeFilePath +"@ + } + Add-Content -Path $RootModuleFilePath -Force -Value @" + +Write-Verbose "[`$scriptName] - [$relativeFolderPath] - Done" +#endregion - From $relativeFolderPath + +"@ +} diff --git a/src/Utilities/private/PSModule/Build-PSModuleBase.ps1 b/src/Utilities/private/PSModule/Build-PSModuleBase.ps1 new file mode 100644 index 0000000..95f2da1 --- /dev/null +++ b/src/Utilities/private/PSModule/Build-PSModuleBase.ps1 @@ -0,0 +1,35 @@ +#Requires -Modules Utilities + +function Build-PSModuleBase { + <# + .SYNOPSIS + Compiles the base module files. + + .DESCRIPTION + This function will compile the base module files. + It will copy the source files to the output folder and remove the files that are not needed. + + .EXAMPLE + Build-PSModuleBase -SourceFolderPath 'C:\MyModule\src\MyModule' -OutputFolderPath 'C:\MyModule\build\MyModule' + #> + [CmdletBinding()] + param( + # Path to the folder where the module source code is located. + [Parameter(Mandatory)] + [System.IO.DirectoryInfo] $ModuleSourceFolder, + + # Path to the folder where the built modules are outputted. + [Parameter(Mandatory)] + [System.IO.DirectoryInfo] $ModuleOutputFolder + ) + + Start-LogGroup 'Build base' + + Write-Verbose "Copying files from [$ModuleSourceFolder] to [$ModuleOutputFolder]" + Copy-Item -Path "$ModuleSourceFolder\*" -Destination $ModuleOutputFolder -Recurse -Force -Verbose + Stop-LogGroup + + Start-LogGroup 'Build base - Result' + (Get-ChildItem -Path $ModuleOutputFolder -Recurse -Force).FullName | Sort-Object + Stop-LogGroup +} diff --git a/src/Utilities/private/PSModule/Build-PSModuleDocumentation.ps1 b/src/Utilities/private/PSModule/Build-PSModuleDocumentation.ps1 new file mode 100644 index 0000000..018346e --- /dev/null +++ b/src/Utilities/private/PSModule/Build-PSModuleDocumentation.ps1 @@ -0,0 +1,41 @@ +#Requires -Modules platyPS, Utilities + +function Build-PSModuleDocumentation { + <# + .SYNOPSIS + Compiles the module documentation. + + .DESCRIPTION + This function will compile the module documentation. + It will generate the markdown files for the module help and copy them to the output folder. + + .EXAMPLE + Build-PSModuleDocumentation -ModuleOutputFolder 'C:\MyModule\src\MyModule' -DocsOutputFolder 'C:\MyModule\build\MyModule' + #> + [CmdletBinding()] + param( + # Folder where the module source code is located. 'outputs/modules/MyModule' + [Parameter(Mandatory)] + [System.IO.DirectoryInfo] $ModuleOutputFolder, + + # Folder where the documentation for the modules should be outputted. 'outputs/docs/MyModule' + [Parameter(Mandatory)] + [System.IO.DirectoryInfo] $DocsOutputFolder + ) + + Start-LogGroup 'Docs - Dependencies' + $moduleName = Split-Path -Path $ModuleOutputFolder -Leaf + + Add-PSModulePath -Path (Split-Path -Path $ModuleOutputFolder -Parent) + Import-PSModule -Path $ModuleOutputFolder -ModuleName $moduleName + + Start-LogGroup 'Build documentation' + New-MarkdownHelp -Module $moduleName -OutputFolder $DocsOutputFolder -Force -Verbose + Stop-LogGroup + + Start-LogGroup 'Build documentation - Result' + Get-ChildItem -Path $DocsOutputFolder -Recurse -Force -Include '*.md' | ForEach-Object { + Write-Verbose "[$_] - [$(Get-FileHash -Path $_.FullName -Algorithm SHA256)]" + } + Stop-LogGroup +} diff --git a/src/Utilities/private/PSModule/Build-PSModuleManifest.ps1 b/src/Utilities/private/PSModule/Build-PSModuleManifest.ps1 new file mode 100644 index 0000000..26adef3 --- /dev/null +++ b/src/Utilities/private/PSModule/Build-PSModuleManifest.ps1 @@ -0,0 +1,341 @@ +#Requires -Modules PSScriptAnalyzer, Utilities + +function Build-PSModuleManifest { + <# + .SYNOPSIS + Compiles the module manifest. + + .DESCRIPTION + This function will compile the module manifest. + It will generate the module manifest file and copy it to the output folder. + + .EXAMPLE + Build-PSModuleManifest -SourceFolderPath 'C:\MyModule\src\MyModule' -OutputFolderPath 'C:\MyModule\build\MyModule' + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidLongLines', '', + Justification = 'No real reason. Just to get going.' + )] + param( + # Folder where the built modules are outputted. 'outputs/modules/MyModule' + [Parameter(Mandatory)] + [System.IO.DirectoryInfo] $ModuleOutputFolder + ) + + #region Build manifest file + Start-LogGroup 'Build manifest file' + $moduleName = Split-Path -Path $ModuleOutputFolder -Leaf + $manifestFileName = "$moduleName.psd1" + $manifestOutputPath = Join-Path -Path $ModuleOutputFolder -ChildPath $manifestFileName + $manifestFile = Get-Item -Path $manifestOutputPath + Write-Verbose ($manifestFile | Format-List | Out-String) + $manifest = Get-ModuleManifest -Path $manifestFile -Verbose:$false + + $rootModule = Get-PSModuleRootModule -SourceFolderPath $ModuleOutputFolder + $manifest.RootModule = $rootModule + $manifest.ModuleVersion = '0.0.1' + + $manifest.Author = $manifest.Keys -contains 'Author' ? ($manifest.Author | IsNotNullOrEmpty) ? $manifest.Author : $env:GITHUB_REPOSITORY_OWNER : $env:GITHUB_REPOSITORY_OWNER + Write-Verbose "[Author] - [$($manifest.Author)]" + + $manifest.CompanyName = $manifest.Keys -contains 'CompanyName' ? ($manifest.CompanyName | IsNotNullOrEmpty) ? $manifest.CompanyName : $env:GITHUB_REPOSITORY_OWNER : $env:GITHUB_REPOSITORY_OWNER + Write-Verbose "[CompanyName] - [$($manifest.CompanyName)]" + + $year = Get-Date -Format 'yyyy' + $copyRightOwner = $manifest.CompanyName -eq $manifest.Author ? $manifest.Author : "$($manifest.Author) | $($manifest.CompanyName)" + $copyRight = "(c) $year $copyRightOwner. All rights reserved." + $manifest.CopyRight = $manifest.Keys -contains 'CopyRight' ? -not [string]::IsNullOrEmpty($manifest.CopyRight) ? $manifest.CopyRight : $copyRight : $copyRight + Write-Verbose "[CopyRight] - [$($manifest.CopyRight)]" + + $repoDescription = gh repo view --json description | ConvertFrom-Json | Select-Object -ExpandProperty description + $manifest.Description = $manifest.Keys -contains 'Description' ? ($manifest.Description | IsNotNullOrEmpty) ? $manifest.Description : $repoDescription : $repoDescription + Write-Verbose "[Description] - [$($manifest.Description)]" + + $manifest.PowerShellHostName = $manifest.Keys -contains 'PowerShellHostName' ? -not [string]::IsNullOrEmpty($manifest.PowerShellHostName) ? $manifest.PowerShellHostName : $null : $null + Write-Verbose "[PowerShellHostName] - [$($manifest.PowerShellHostName)]" + + $manifest.PowerShellHostVersion = $manifest.Keys -contains 'PowerShellHostVersion' ? -not [string]::IsNullOrEmpty($manifest.PowerShellHostVersion) ? $manifest.PowerShellHostVersion : $null : $null + Write-Verbose "[PowerShellHostVersion] - [$($manifest.PowerShellHostVersion)]" + + $manifest.DotNetFrameworkVersion = $manifest.Keys -contains 'DotNetFrameworkVersion' ? -not [string]::IsNullOrEmpty($manifest.DotNetFrameworkVersion) ? $manifest.DotNetFrameworkVersion : $null : $null + Write-Verbose "[DotNetFrameworkVersion] - [$($manifest.DotNetFrameworkVersion)]" + + $manifest.ClrVersion = $manifest.Keys -contains 'ClrVersion' ? -not [string]::IsNullOrEmpty($manifest.ClrVersion) ? $manifest.ClrVersion : $null : $null + Write-Verbose "[ClrVersion] - [$($manifest.ClrVersion)]" + + $manifest.ProcessorArchitecture = $manifest.Keys -contains 'ProcessorArchitecture' ? -not [string]::IsNullOrEmpty($manifest.ProcessorArchitecture) ? $manifest.ProcessorArchitecture : 'None' : 'None' + Write-Verbose "[ProcessorArchitecture] - [$($manifest.ProcessorArchitecture)]" + + # Get the path separator for the current OS + $pathSeparator = [System.IO.Path]::DirectorySeparatorChar + + Write-Verbose '[FileList]' + $files = $ModuleOutputFolder | Get-ChildItem -File -ErrorAction SilentlyContinue | Where-Object -Property Name -NotLike '*.ps1' + $files += $ModuleOutputFolder | Get-ChildItem -Directory | Get-ChildItem -Recurse -File -ErrorAction SilentlyContinue + $files = $files | Select-Object -ExpandProperty FullName | ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) } + $fileList = $files | Where-Object { $_ -notLike 'public*' -and $_ -notLike 'private*' } + $manifest.FileList = $fileList.count -eq 0 ? @() : @($fileList) + $manifest.FileList | ForEach-Object { Write-Verbose "[FileList] - [$_]" } + + Write-Verbose '[RequiredAssemblies]' + $requiredAssembliesFolderPath = Join-Path $ModuleOutputFolder 'assemblies' + $requiredAssemblies = Get-ChildItem -Path $RequiredAssembliesFolderPath -Recurse -File -ErrorAction SilentlyContinue -Filter '*.dll' | + Select-Object -ExpandProperty FullName | + ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) } + $manifest.RequiredAssemblies = $requiredAssemblies.count -eq 0 ? @() : @($requiredAssemblies) + $manifest.RequiredAssemblies | ForEach-Object { Write-Verbose "[RequiredAssemblies] - [$_]" } + + Write-Verbose '[NestedModules]' + $nestedModulesFolderPath = Join-Path $ModuleOutputFolder 'modules' + $nestedModules = Get-ChildItem -Path $nestedModulesFolderPath -Recurse -File -ErrorAction SilentlyContinue -Include '*.psm1', '*.ps1' | + Select-Object -ExpandProperty FullName | + ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) } + $manifest.NestedModules = $nestedModules.count -eq 0 ? @() : @($nestedModules) + $manifest.NestedModules | ForEach-Object { Write-Verbose "[NestedModules] - [$_]" } + + Write-Verbose '[ScriptsToProcess]' + $allScriptsToProcess = @('scripts', 'classes') | ForEach-Object { + Write-Verbose "[ScriptsToProcess] - Processing [$_]" + $scriptsFolderPath = Join-Path $ModuleOutputFolder $_ + $scriptsToProcess = Get-ChildItem -Path $scriptsFolderPath -Recurse -File -ErrorAction SilentlyContinue -Include '*.ps1' | + Select-Object -ExpandProperty FullName | + ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) } + $scriptsToProcess + } + $manifest.ScriptsToProcess = $allScriptsToProcess.count -eq 0 ? @() : @($allScriptsToProcess) + $manifest.ScriptsToProcess | ForEach-Object { Write-Verbose "[ScriptsToProcess] - [$_]" } + + Write-Verbose '[TypesToProcess]' + $typesToProcess = Get-ChildItem -Path $ModuleOutputFolder -Recurse -File -ErrorAction SilentlyContinue -Include '*.Types.ps1xml' | + Select-Object -ExpandProperty FullName | + ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) } + $manifest.TypesToProcess = $typesToProcess.count -eq 0 ? @() : @($typesToProcess) + $manifest.TypesToProcess | ForEach-Object { Write-Verbose "[TypesToProcess] - [$_]" } + + Write-Verbose '[FormatsToProcess]' + $formatsToProcess = Get-ChildItem -Path $ModuleOutputFolder -Recurse -File -ErrorAction SilentlyContinue -Include '*.Format.ps1xml' | + Select-Object -ExpandProperty FullName | + ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) } + $manifest.FormatsToProcess = $formatsToProcess.count -eq 0 ? @() : @($formatsToProcess) + $manifest.FormatsToProcess | ForEach-Object { Write-Verbose "[FormatsToProcess] - [$_]" } + + Write-Verbose '[DscResourcesToExport]' + $dscResourcesToExportFolderPath = Join-Path $ModuleOutputFolder 'resources' + $dscResourcesToExport = Get-ChildItem -Path $dscResourcesToExportFolderPath -Recurse -File -ErrorAction SilentlyContinue -Include '*.psm1' | + Select-Object -ExpandProperty FullName | + ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) } + $manifest.DscResourcesToExport = $dscResourcesToExport.count -eq 0 ? @() : @($dscResourcesToExport) + $manifest.DscResourcesToExport | ForEach-Object { Write-Verbose "[DscResourcesToExport] - [$_]" } + + $manifest.FunctionsToExport = Get-PSModuleFunctionsToExport -SourceFolderPath $ModuleOutputFolder + $manifest.CmdletsToExport = Get-PSModuleCmdletsToExport -SourceFolderPath $ModuleOutputFolder + $manifest.AliasesToExport = Get-PSModuleAliasesToExport -SourceFolderPath $ModuleOutputFolder + $manifest.VariablesToExport = Get-PSModuleVariablesToExport -SourceFolderPath $ModuleOutputFolder + + Write-Verbose '[ModuleList]' + $moduleList = Get-ChildItem -Path $ModuleOutputFolder -Recurse -File -ErrorAction SilentlyContinue -Include '*.psm1' | Where-Object -Property Name -NE $rootModule | + Select-Object -ExpandProperty FullName | + ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) } + $manifest.ModuleList = $moduleList.count -eq 0 ? @() : @($moduleList) + $manifest.ModuleList | ForEach-Object { Write-Verbose "[ModuleList] - [$_]" } + + + Write-Verbose '[Gather]' + $capturedModules = @() + $capturedVersions = @() + $capturedPSEdition = @() + + $files = $ModuleOutputFolder | Get-ChildItem -Recurse -File -ErrorAction SilentlyContinue + Write-Verbose "[Gather] - Processing [$($files.Count)] files" + foreach ($file in $files) { + $relativePath = $file.FullName.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) + Write-Verbose "[Gather] - [$relativePath]" + + if ($file.extension -in '.psm1', '.ps1') { + $fileContent = Get-Content -Path $file + + switch -Regex ($fileContent) { + # RequiredModules -> REQUIRES -Modules | , @() if not provided + '^\s*#Requires -Modules (.+)$' { + # Add captured module name to array + $capturedMatches = $matches[1].Split(',').trim() + $capturedMatches | ForEach-Object { + Write-Verbose " - [#Requires -Modules] - [$_]" + $hashtable = '\@\s*\{[^\}]*\}' + if ($_ -match $hashtable) { + $modules = ConvertTo-Hashtable -InputString $_ + Write-Verbose " - [#Requires -Modules] - [$_] - Hashtable" + $modules.Keys | ForEach-Object { + Write-Verbose "$($modules[$_])]" + } + $capturedModules += $modules + } else { + Write-Verbose " - [#Requires -Modules] - [$_] - String" + $capturedModules += $_ + } + } + } + # PowerShellVersion -> REQUIRES -Version [.], $null if not provided + '^\s*#Requires -Version (.+)$' { + Write-Verbose " - [#Requires -Version] - [$($matches[1])]" + # Add captured module name to array + $capturedVersions += $matches[1] + } + #CompatiblePSEditions -> REQUIRES -PSEdition , $null if not provided + '^\s*#Requires -PSEdition (.+)$' { + Write-Verbose " - [#Requires -PSEdition] - [$($matches[1])]" + # Add captured module name to array + $capturedPSEdition += $matches[1] + } + } + } + } + + Write-Verbose '[RequiredModules]' + $capturedModules = $capturedModules + $manifest.RequiredModules = $capturedModules + $manifest.RequiredModules | ForEach-Object { Write-Verbose "[RequiredModules] - [$_]" } + + Write-Verbose '[RequiredModulesUnique]' + $manifest.RequiredModules = $manifest.RequiredModules | Sort-Object -Unique + $manifest.RequiredModules | ForEach-Object { Write-Verbose "[RequiredModulesUnique] - [$_]" } + + Write-Verbose '[PowerShellVersion]' + $capturedVersions = $capturedVersions | Sort-Object -Unique -Descending + $capturedVersions | ForEach-Object { Write-Verbose "[PowerShellVersion] - [$_]" } + $manifest.PowerShellVersion = $capturedVersions.count -eq 0 ? [version]'7.0' : [version]($capturedVersions | Select-Object -First 1) + Write-Verbose '[PowerShellVersion] - Selecting version' + Write-Verbose "[PowerShellVersion] - [$($manifest.PowerShellVersion)]" + + Write-Verbose '[CompatiblePSEditions]' + $capturedPSEdition = $capturedPSEdition | Sort-Object -Unique + if ($capturedPSEdition.count -eq 2) { + throw 'The module is requires both Desktop and Core editions.' + } + $manifest.CompatiblePSEditions = $capturedPSEdition.count -eq 0 ? @('Core', 'Desktop') : @($capturedPSEdition) + $manifest.CompatiblePSEditions | ForEach-Object { Write-Verbose "[CompatiblePSEditions] - [$_]" } + + Write-Verbose '[PrivateData]' + $privateData = $manifest.Keys -contains 'PrivateData' ? $null -ne $manifest.PrivateData ? $manifest.PrivateData : @{} : @{} + if ($manifest.Keys -contains 'PrivateData') { + $manifest.Remove('PrivateData') + } + + Write-Verbose '[HelpInfoURI]' + $manifest.HelpInfoURI = $privateData.Keys -contains 'HelpInfoURI' ? $null -ne $privateData.HelpInfoURI ? $privateData.HelpInfoURI : '' : '' + Write-Verbose "[HelpInfoURI] - [$($manifest.HelpInfoURI)]" + if ([string]::IsNullOrEmpty($manifest.HelpInfoURI)) { + $manifest.Remove('HelpInfoURI') + } + + Write-Verbose '[DefaultCommandPrefix]' + $manifest.DefaultCommandPrefix = $privateData.Keys -contains 'DefaultCommandPrefix' ? $null -ne $privateData.DefaultCommandPrefix ? $privateData.DefaultCommandPrefix : '' : '' + Write-Verbose "[DefaultCommandPrefix] - [$($manifest.DefaultCommandPrefix)]" + + $PSData = $privateData.Keys -contains 'PSData' ? $null -ne $privateData.PSData ? $privateData.PSData : @{} : @{} + + Write-Verbose '[Tags]' + try { + $repoLabels = gh repo view --json repositoryTopics | ConvertFrom-Json | Select-Object -ExpandProperty repositoryTopics | Select-Object -ExpandProperty name + } catch { + $repoLabels = @() + } + $manifestTags = @() + $manifestTags = $PSData.Keys -contains 'Tags' ? ($PSData.Tags).Count -gt 0 ? $PSData.Tags : $repoLabels : $repoLabels + # Add tags for compatability mode. https://docs.microsoft.com/en-us/powershell/scripting/developer/module/how-to-write-a-powershell-module-manifest?view=powershell-7.1#compatibility-tags + if ($manifest.CompatiblePSEditions -contains 'Desktop') { + if ($manifestTags -notcontains 'PSEdition_Desktop') { + $manifestTags += 'PSEdition_Desktop' + } + } + if ($manifest.CompatiblePSEditions -contains 'Core') { + if ($manifestTags -notcontains 'PSEdition_Core') { + $manifestTags += 'PSEdition_Core' + } + } + $manifestTags | ForEach-Object { Write-Verbose "[Tags] - [$_]" } + $manifest.Tags = $manifestTags + + if ($PSData.Tags -contains 'PSEdition_Core' -and $manifest.PowerShellVersion -lt '6.0') { + throw "[Tags] - Cannot be PSEdition = 'Core' and PowerShellVersion < 6.0" + } + <# + Windows: Packages that are compatible with the Windows Operating System + Linux: Packages that are compatible with Linux Operating Systems + MacOS: Packages that are compatible with the Mac Operating System + https://learn.microsoft.com/en-us/powershell/gallery/concepts/package-manifest-affecting-ui?view=powershellget-2.x#tag-details + #> + + Write-Verbose '[LicenseUri]' + $licenseUri = "https://github.com/$env:GITHUB_REPOSITORY_OWNER/$env:GITHUB_REPOSITORY_NAME/blob/main/LICENSE" + $manifest.LicenseUri = $PSData.Keys -contains 'LicenseUri' ? $null -ne $PSData.LicenseUri ? $PSData.LicenseUri : $licenseUri : $licenseUri + Write-Verbose "[LicenseUri] - [$($manifest.LicenseUri)]" + if ([string]::IsNullOrEmpty($manifest.LicenseUri)) { + $manifest.Remove('LicenseUri') + } + + Write-Verbose '[ProjectUri]' + $projectUri = gh repo view --json url | ConvertFrom-Json | Select-Object -ExpandProperty url + $manifest.ProjectUri = $PSData.Keys -contains 'ProjectUri' ? $null -ne $PSData.ProjectUri ? $PSData.ProjectUri : $projectUri : $projectUri + Write-Verbose "[ProjectUri] - [$($manifest.ProjectUri)]" + if ([string]::IsNullOrEmpty($manifest.ProjectUri)) { + $manifest.Remove('ProjectUri') + } + + Write-Verbose '[IconUri]' + $iconUri = "https://raw.githubusercontent.com/$env:GITHUB_REPOSITORY_OWNER/$env:GITHUB_REPOSITORY_NAME/main/icon/icon.png" + $manifest.IconUri = $PSData.Keys -contains 'IconUri' ? $null -ne $PSData.IconUri ? $PSData.IconUri : $iconUri : $iconUri + Write-Verbose "[IconUri] - [$($manifest.IconUri)]" + if ([string]::IsNullOrEmpty($manifest.IconUri)) { + $manifest.Remove('IconUri') + } + + Write-Verbose '[ReleaseNotes]' + $manifest.ReleaseNotes = $PSData.Keys -contains 'ReleaseNotes' ? $null -ne $PSData.ReleaseNotes ? $PSData.ReleaseNotes : '' : '' + Write-Verbose "[ReleaseNotes] - [$($manifest.ReleaseNotes)]" + if ([string]::IsNullOrEmpty($manifest.ReleaseNotes)) { + $manifest.Remove('ReleaseNotes') + } + + Write-Verbose '[PreRelease]' + # $manifest.PreRelease = "" + # Is managed by the publish action + + Write-Verbose '[RequireLicenseAcceptance]' + $manifest.RequireLicenseAcceptance = $PSData.Keys -contains 'RequireLicenseAcceptance' ? $null -ne $PSData.RequireLicenseAcceptance ? $PSData.RequireLicenseAcceptance : $false : $false + Write-Verbose "[RequireLicenseAcceptance] - [$($manifest.RequireLicenseAcceptance)]" + if ($manifest.RequireLicenseAcceptance -eq $false) { + $manifest.Remove('RequireLicenseAcceptance') + } + + Write-Verbose '[ExternalModuleDependencies]' + $manifest.ExternalModuleDependencies = $PSData.Keys -contains 'ExternalModuleDependencies' ? $null -ne $PSData.ExternalModuleDependencies ? $PSData.ExternalModuleDependencies : @() : @() + if (($manifest.ExternalModuleDependencies).count -eq 0) { + $manifest.Remove('ExternalModuleDependencies') + } else { + $manifest.ExternalModuleDependencies | ForEach-Object { Write-Verbose "[ExternalModuleDependencies] - [$_]" } + } + + Write-Verbose 'Creating new manifest file in outputs folder' + $outputManifestPath = Join-Path -Path $ModuleOutputFolder $manifestFileName + Write-Verbose "OutputManifestPath - [$outputManifestPath]" + New-ModuleManifest -Path $outputManifestPath @manifest + Stop-LogGroup + #endregion Build manifest file + + #region Format manifest file + Start-LogGroup 'Format manifest file - Before format' + Show-FileContent -Path $outputManifestPath + Stop-LogGroup + + Start-LogGroup 'Format manifest file - After' + Set-ModuleManifest -Path $outputManifestPath + Show-FileContent -Path $outputManifestPath + Stop-LogGroup + + Start-LogGroup 'Format manifest file - Result' + Show-FileContent -Path $outputManifestPath + Stop-LogGroup + #endregion Format manifest file +} diff --git a/src/Utilities/private/PSModule/Build-PSModuleRootModule.ps1 b/src/Utilities/private/PSModule/Build-PSModuleRootModule.ps1 new file mode 100644 index 0000000..2c46e8f --- /dev/null +++ b/src/Utilities/private/PSModule/Build-PSModuleRootModule.ps1 @@ -0,0 +1,169 @@ +#Requires -Modules PSScriptAnalyzer, Utilities + +function Build-PSModuleRootModule { + <# + .SYNOPSIS + Compiles the module root module files. + + .DESCRIPTION + This function will compile the modules root module from source files. + It will copy the source files to the output folder and start compiling the module. + During compilation, the source files are added to the root module file in the following order: + + 1. Module header from header.ps1 file. Usually to suppress code analysis warnings/errors and to add [CmdletBinding()] to the module. + 2. Data files are added from source files. These are also tracked based on visibility/exportability based on folder location: + 1. private + 2. public + 3. Combines *.ps1 files from the following folders in alphabetical order from each folder: + 1. init + 2. classes + 3. private + 4. public + 5. Any remaining *.ps1 on module root. + 3. Export-ModuleMember by using the functions, cmdlets, variables and aliases found in the source files. + - `Functions` will only contain functions that are from the `public` folder. + - `Cmdlets` will only contain cmdlets that are from the `public` folder. + - `Variables` will only contain variables that are from the `public` folder. + - `Aliases` will only contain aliases that are from the functions from the `public` folder. + + .EXAMPLE + Build-PSModuleRootModule -SourceFolderPath 'C:\MyModule\src\MyModule' -OutputFolderPath 'C:\MyModule\build\MyModule' + #> + [CmdletBinding()] + param( + # Folder where the built modules are outputted. 'outputs/modules/MyModule' + [Parameter(Mandatory)] + [System.IO.DirectoryInfo] $ModuleOutputFolder + ) + + #region Build root module + Start-LogGroup 'Build root module' + $moduleName = Split-Path -Path $ModuleOutputFolder -Leaf + $rootModuleFile = New-Item -Path $ModuleOutputFolder -Name "$moduleName.psm1" -Force + + #region - Analyze source files + $exports = @{ + Function = Get-PSModuleFunctionsToExport -SourceFolderPath $ModuleOutputFolder + Cmdlet = Get-PSModuleCmdletsToExport -SourceFolderPath $ModuleOutputFolder + Variable = Get-PSModuleVariablesToExport -SourceFolderPath $ModuleOutputFolder + Alias = Get-PSModuleAliasesToExport -SourceFolderPath $ModuleOutputFolder + } + + Write-Verbose ($exports | Out-String) + #endregion - Analyze source files + + #region - Module header + $headerFilePath = Join-Path -Path $ModuleOutputFolder -ChildPath 'header.ps1' + if (Test-Path -Path $headerFilePath) { + Get-Content -Path $headerFilePath -Raw | Add-Content -Path $rootModuleFile -Force + $headerFilePath | Remove-Item -Force + } else { + Add-Content -Path $rootModuleFile -Force -Value @' +[CmdletBinding()] +param() +'@ + } + #endregion - Module header + + #region - Module post-header + Add-Content -Path $rootModuleFile -Force -Value @' +$scriptName = $MyInvocation.MyCommand.Name +Write-Verbose "[$scriptName] Importing module" + +'@ + #endregion - Module post-header + + #region - Data and variables + Add-Content -Path $rootModuleFile.FullName -Force -Value @' +#region - Data import +Write-Verbose "[$scriptName] - [data] - Processing folder" +$dataFolder = (Join-Path $PSScriptRoot 'data') +Write-Verbose "[$scriptName] - [data] - [$dataFolder]" +Get-ChildItem -Path "$dataFolder" -Recurse -Force -Include '*.psd1' -ErrorAction SilentlyContinue | ForEach-Object { + Write-Verbose "[$scriptName] - [data] - [$($_.Name)] - Importing" + New-Variable -Name $_.BaseName -Value (Import-PowerShellDataFile -Path $_.FullName) -Force + Write-Verbose "[$scriptName] - [data] - [$($_.Name)] - Done" +} + +Write-Verbose "[$scriptName] - [data] - Done" +#endregion - Data import + +'@ + #endregion - Data and variables + + #region - Add content from subfolders + $scriptFoldersToProcess = @( + 'private', + 'public' + ) + + foreach ($scriptFolder in $scriptFoldersToProcess) { + $scriptFolder = Join-Path -Path $ModuleOutputFolder -ChildPath $scriptFolder + if (-not (Test-Path -Path $scriptFolder)) { + continue + } + Add-ContentFromItem -Path $scriptFolder -RootModuleFilePath $rootModuleFile -RootPath $ModuleOutputFolder + Remove-Item -Path $scriptFolder -Force -Recurse + } + #endregion - Add content from subfolders + + #region - Add content from *.ps1 files on module root + $files = $ModuleOutputFolder | Get-ChildItem -File -Force -Filter '*.ps1' + foreach ($file in $files) { + $relativePath = $file.FullName.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) + Add-Content -Path $rootModuleFile -Force -Value @" +#region - From $relativePath +Write-Verbose "[`$scriptName] - [$relativePath] - Importing" + +"@ + Get-Content -Path $file.FullName | Add-Content -Path $rootModuleFile -Force + + Add-Content -Path $rootModuleFile -Force -Value @" +Write-Verbose "[`$scriptName] - [$relativePath] - Done" +#endregion - From $relativePath + +"@ + $file | Remove-Item -Force + } + #endregion - Add content from *.ps1 files on module root + + #region - Export-ModuleMember + $exportsString = Convert-HashtableToString -Hashtable $exports + + Write-Verbose ($exportsString | Out-String) + + $params = @{ + Path = $rootModuleFile + Force = $true + Value = @" +`$exports = $exportsString +Export-ModuleMember @exports +"@ + } + Add-Content @params + #endregion - Export-ModuleMember + + Stop-LogGroup + #endregion Build root module + + #region Format root module + Start-LogGroup 'Build root module - Format root module - Before format' + Show-FileContent -Path $rootModuleFile + Stop-LogGroup + + Start-LogGroup 'Build root module - Format root module - Format' + $AllContent = Get-Content -Path $rootModuleFile -Raw + $settings = (Join-Path -Path $PSScriptRoot 'PSScriptAnalyzer.Tests.psd1') + Invoke-Formatter -ScriptDefinition $AllContent -Settings $settings | + Out-File -FilePath $rootModuleFile -Encoding utf8BOM -Force + Stop-LogGroup + + Start-LogGroup 'Build root module - Format root module - Result' + Show-FileContent -Path $rootModuleFile + Stop-LogGroup + #endregion Format root module + + Start-LogGroup 'Build root module - Result' + (Get-ChildItem -Path $ModuleOutputFolder -Recurse -Force).FullName | Sort-Object + Stop-LogGroup +} diff --git a/src/Utilities/private/PSModule/ConvertTo-Hashtable.ps1 b/src/Utilities/private/PSModule/ConvertTo-Hashtable.ps1 new file mode 100644 index 0000000..3356890 --- /dev/null +++ b/src/Utilities/private/PSModule/ConvertTo-Hashtable.ps1 @@ -0,0 +1,32 @@ +function ConvertTo-Hashtable { + <# + .SYNOPSIS + Converts a string to a hashtable. + + .DESCRIPTION + Converts a string to a hashtable. + + .EXAMPLE + ConvertTo-Hashtable -InputString "@{Key1 = 'Value1'; Key2 = 'Value2'}" + + Key Value + --- ----- + Key1 Value1 + Key2 Value2 + + Converts the string to a hashtable. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingInvokeExpression', '', Scope = 'Function', + Justification = 'Converting a string based hashtable to a hashtable.' + )] + [CmdletBinding()] + param ( + # The string to convert to a hashtable. + [Parameter(Mandatory = $true)] + [string]$InputString + ) + + Invoke-Expression $InputString + +} diff --git a/src/Utilities/private/PSModule/Get-PSModuleAliasesToExport.ps1 b/src/Utilities/private/PSModule/Get-PSModuleAliasesToExport.ps1 new file mode 100644 index 0000000..0d3a154 --- /dev/null +++ b/src/Utilities/private/PSModule/Get-PSModuleAliasesToExport.ps1 @@ -0,0 +1,34 @@ +function Get-PSModuleAliasesToExport { + <# + .SYNOPSIS + Gets the aliases to export from the module manifest. + + .DESCRIPTION + This function will get the aliases to export from the module manifest. + + .EXAMPLE + Get-PSModuleAliasesToExport -SourceFolderPath 'C:\MyModule\src\MyModule' + #> + [CmdletBinding()] + param( + # Path to the folder where the module source code is located. + [Parameter(Mandatory)] + [string] $SourceFolderPath + ) + + $manifestPropertyName = 'AliasesToExport' + + $moduleName = Split-Path -Path $SourceFolderPath -Leaf + $manifestFileName = "$moduleName.psd1" + $manifestFilePath = Join-Path -Path $SourceFolderPath $manifestFileName + + $manifest = Get-ModuleManifest -Path $manifestFilePath -Verbose:$false + + Write-Verbose "[$manifestPropertyName]" + $aliasesToExport = (($manifest.AliasesToExport).count -eq 0) -or ($manifest.AliasesToExport | IsNullOrEmpty) ? '*' : $manifest.AliasesToExport + $aliasesToExport | ForEach-Object { + Write-Verbose "[$manifestPropertyName] - [$_]" + } + + $aliasesToExport +} diff --git a/src/Utilities/private/PSModule/Get-PSModuleCmdletsToExport.ps1 b/src/Utilities/private/PSModule/Get-PSModuleCmdletsToExport.ps1 new file mode 100644 index 0000000..aa0b1e9 --- /dev/null +++ b/src/Utilities/private/PSModule/Get-PSModuleCmdletsToExport.ps1 @@ -0,0 +1,34 @@ +function Get-PSModuleCmdletsToExport { + <# + .SYNOPSIS + Gets the cmdlets to export from the module manifest. + + .DESCRIPTION + This function will get the cmdlets to export from the module manifest. + + .EXAMPLE + Get-PSModuleCmdletsToExport -SourceFolderPath 'C:\MyModule\src\MyModule' + #> + [CmdletBinding()] + param( + # Path to the folder where the module source code is located. + [Parameter(Mandatory)] + [string] $SourceFolderPath + ) + + $manifestPropertyName = 'CmdletsToExport' + + $moduleName = Split-Path -Path $SourceFolderPath -Leaf + $manifestFileName = "$moduleName.psd1" + $manifestFilePath = Join-Path -Path $SourceFolderPath $manifestFileName + + $manifest = Get-ModuleManifest -Path $manifestFilePath -Verbose:$false + + Write-Verbose "[$manifestPropertyName]" + $cmdletsToExport = (($manifest.CmdletsToExport).count -eq 0) -or ($manifest.CmdletsToExport | IsNullOrEmpty) ? '' : $manifest.CmdletsToExport + $cmdletsToExport | ForEach-Object { + Write-Verbose "[$manifestPropertyName] - [$_]" + } + + $cmdletsToExport +} diff --git a/src/Utilities/private/PSModule/Get-PSModuleFunctionsToExport.ps1 b/src/Utilities/private/PSModule/Get-PSModuleFunctionsToExport.ps1 new file mode 100644 index 0000000..89c57a7 --- /dev/null +++ b/src/Utilities/private/PSModule/Get-PSModuleFunctionsToExport.ps1 @@ -0,0 +1,40 @@ +function Get-PSModuleFunctionsToExport { + <# + .SYNOPSIS + Gets the functions to export from the module manifest. + + .DESCRIPTION + This function will get the functions to export from the module manifest. + + .EXAMPLE + Get-PSModuleFunctionsToExport -SourceFolderPath 'C:\MyModule\src\MyModule' + #> + [CmdletBinding()] + [OutputType([array])] + param( + # Path to the folder where the module source code is located. + [Parameter(Mandatory)] + [string] $SourceFolderPath + ) + + $manifestPropertyName = 'FunctionsToExport' + + Write-Verbose "[$manifestPropertyName]" + Write-Verbose "[$manifestPropertyName] - Checking path for functions and filters" + + $publicFolderPath = Join-Path $SourceFolderPath 'public' + Write-Verbose "[$manifestPropertyName] - [$publicFolderPath]" + $functionsToExport = [Collections.Generic.List[string]]::new() + $scriptFiles = Get-ChildItem -Path $publicFolderPath -Recurse -File -ErrorAction SilentlyContinue -Include '*.ps1' + Write-Verbose "[$manifestPropertyName] - [$($scriptFiles.Count)]" + foreach ($file in $scriptFiles) { + $fileContent = Get-Content -Path $file.FullName -Raw + $containsFunction = ($fileContent -match 'function ') -or ($fileContent -match 'filter ') + Write-Verbose "[$manifestPropertyName] - [$($file.BaseName)] - [$containsFunction]" + if ($containsFunction) { + $functionsToExport.Add($file.BaseName) + } + } + + [array]$functionsToExport +} diff --git a/src/Utilities/private/PSModule/Get-PSModuleRootModule.ps1 b/src/Utilities/private/PSModule/Get-PSModuleRootModule.ps1 new file mode 100644 index 0000000..cfb5375 --- /dev/null +++ b/src/Utilities/private/PSModule/Get-PSModuleRootModule.ps1 @@ -0,0 +1,54 @@ +function Get-PSModuleRootModule { + <# + .SYNOPSIS + Gets the root module to export from the module manifest. + + .DESCRIPTION + This function will get the root module to export from the module manifest. + + .EXAMPLE + Get-PSModuleRootModule -SourceFolderPath 'C:\MyModule\src\MyModule' + #> + [CmdletBinding()] + param( + # Path to the folder where the module source code is located. + [Parameter(Mandatory)] + [string] $SourceFolderPath + ) + + $rootModuleExtensions = '.psm1', '.ps1', '.dll', '.cdxml', '.xaml' + $candidateFiles = Get-ChildItem -Path $SourceFolderPath -File | Where-Object { ($_.BaseName -like $_.Directory.BaseName) -and ($_.Extension -in $rootModuleExtensions) } + + Write-Verbose "Looking for root modules, matching extensions in order [$($rootModuleExtensions -join ', ')]" -Verbose + :ext foreach ($ext in $rootModuleExtensions) { + Write-Verbose "Looking for [$ext] files" + foreach ($file in $candidateFiles) { + Write-Verbose " - [$($file.Name)]" + if ($file.Extension -eq $ext) { + Write-Verbose " - [$($file.Name)] - RootModule found!" + $rootModule = $file.Name + break ext + } + } + } + + if (-not $rootModule) { + Write-Verbose 'No RootModule found' + } + + $moduleType = switch -Regex ($RootModule) { + '\.(ps1|psm1)$' { 'Script' } + '\.dll$' { 'Binary' } + '\.cdxml$' { 'CIM' } + '\.xaml$' { 'Workflow' } + default { 'Manifest' } + } + Write-Verbose "[$manifestPropertyName] - [$moduleType]" + + $supportedModuleTypes = @('Script', 'Manifest') + if ($moduleType -notin $supportedModuleTypes) { + Write-Warning "[$moduleType] - Module type not supported" + } + + $rootModule +} diff --git a/src/Utilities/private/PSModule/Get-PSModuleVariablesToExport.ps1 b/src/Utilities/private/PSModule/Get-PSModuleVariablesToExport.ps1 new file mode 100644 index 0000000..63041ba --- /dev/null +++ b/src/Utilities/private/PSModule/Get-PSModuleVariablesToExport.ps1 @@ -0,0 +1,35 @@ +function Get-PSModuleVariablesToExport { + <# + .SYNOPSIS + Gets the variables to export from the module manifest. + + .DESCRIPTION + This function will get the variables to export from the module manifest. + + .EXAMPLE + Get-PSModuleVariablesToExport -SourceFolderPath 'C:\MyModule\src\MyModule' + #> + [CmdletBinding()] + param( + # Path to the folder where the module source code is located. + [Parameter(Mandatory)] + [string] $SourceFolderPath + ) + + $manifestPropertyName = 'VariablesToExport' + + $moduleName = Split-Path -Path $SourceFolderPath -Leaf + $manifestFileName = "$moduleName.psd1" + $manifestFilePath = Join-Path -Path $SourceFolderPath $manifestFileName + + $manifest = Get-ModuleManifest -Path $manifestFilePath -Verbose:$false + + Write-Verbose "[$manifestPropertyName]" + + $variablesToExport = (($manifest.VariablesToExport).count -eq 0) -or ($manifest.VariablesToExport | IsNullOrEmpty) ? '' : $manifest.VariablesToExport + $variablesToExport | ForEach-Object { + Write-Verbose "[$manifestPropertyName] - [$_]" + } + + $variablesToExport +} diff --git a/src/Utilities/private/PSModule/Import-PSModule.ps1 b/src/Utilities/private/PSModule/Import-PSModule.ps1 new file mode 100644 index 0000000..7470a9f --- /dev/null +++ b/src/Utilities/private/PSModule/Import-PSModule.ps1 @@ -0,0 +1,46 @@ +function Import-PSModule { + <# + .SYNOPSIS + Imports a build PS module. + + .DESCRIPTION + Imports a build PS module. + + .EXAMPLE + Import-PSModule -SourceFolderPath $ModuleFolderPath -ModuleName $ModuleName + + Imports a module located at $ModuleFolderPath with the name $ModuleName. + #> + [CmdletBinding()] + param( + # Path to the folder where the module source code is located. + [Parameter(Mandatory)] + [string] $Path, + + # Name of the module. + [Parameter(Mandatory)] + [string] $ModuleName + ) + + Start-LogGroup "Importing module [$ModuleName]" + + $moduleName = Split-Path -Path $Path -Leaf + $manifestFileName = "$moduleName.psd1" + $manifestFilePath = Join-Path -Path $Path $manifestFileName + $manifestFile = Get-ModuleManifest -Path $manifestFilePath -As FileInfo -Verbose + + Write-Verbose "Manifest file path: [$($manifestFile.FullName)]" -Verbose + Resolve-PSModuleDependencies -ManifestFilePath $manifestFile + Import-Module $ModuleName + + Write-Verbose 'List loaded modules' + $availableModules = Get-Module -ListAvailable -Refresh -Verbose:$false + $availableModules | Select-Object Name, Version, Path | Sort-Object Name | Format-Table -AutoSize + Write-Verbose 'List commands' + Write-Verbose (Get-Command -Module $moduleName | Format-Table -AutoSize | Out-String) + + if ($ModuleName -notin $availableModules.Name) { + throw 'Module not found' + } + Stop-LogGroup +} diff --git a/src/Utilities/private/PSModule/PSScriptAnalyzer.Tests.psd1 b/src/Utilities/private/PSModule/PSScriptAnalyzer.Tests.psd1 new file mode 100644 index 0000000..bd8fb92 --- /dev/null +++ b/src/Utilities/private/PSModule/PSScriptAnalyzer.Tests.psd1 @@ -0,0 +1,56 @@ +@{ + Rules = @{ + PSAlignAssignmentStatement = @{ + Enable = $true + CheckHashtable = $true + } + PSAvoidLongLines = @{ + Enable = $true + MaximumLineLength = 150 + } + PSAvoidSemicolonsAsLineTerminators = @{ + Enable = $true + } + PSPlaceCloseBrace = @{ + Enable = $true + NewLineAfter = $false + IgnoreOneLineBlock = $true + NoEmptyLineBefore = $false + } + PSPlaceOpenBrace = @{ + Enable = $true + OnSameLine = $true + NewLineAfter = $true + IgnoreOneLineBlock = $true + } + PSProvideCommentHelp = @{ + Enable = $true + ExportedOnly = $false + BlockComment = $true + VSCodeSnippetCorrection = $false + Placement = 'begin' + } + PSUseConsistentIndentation = @{ + Enable = $true + IndentationSize = 4 + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + Kind = 'space' + } + PSUseConsistentWhitespace = @{ + Enable = $true + CheckInnerBrace = $true + CheckOpenBrace = $true + CheckOpenParen = $true + CheckOperator = $true + CheckPipe = $true + CheckPipeForRedundantWhitespace = $true + CheckSeparator = $true + CheckParameter = $true + IgnoreAssignmentOperatorInsideHashTable = $true + } + } + ExcludeRules = @( + 'PSAvoidUsingCmdletAliases', + 'PSUseToExportFieldsInManifest' + ) +} diff --git a/src/Utilities/private/PSModule/Resolve-PSModuleDependency.ps1 b/src/Utilities/private/PSModule/Resolve-PSModuleDependency.ps1 new file mode 100644 index 0000000..b9ebf66 --- /dev/null +++ b/src/Utilities/private/PSModule/Resolve-PSModuleDependency.ps1 @@ -0,0 +1,60 @@ +function Resolve-PSModuleDependency { + <# + .SYNOPSIS + Resolve dependencies for a module based on the manifest file. + + .DESCRIPTION + Resolve dependencies for a module based on the manifest file, following PSModuleInfo structure + + .EXAMPLE + Resolve-PSModuleDependency -Path 'C:\MyModule\MyModule.psd1' + + Installs all modules defined in the manifest file, following PSModuleInfo structure. + + .NOTES + Should later be adapted to support both pre-reqs, and dependencies. + Should later be adapted to take 4 parameters sets: specific version ("requiredVersion" | "GUID"), latest version ModuleVersion, + and latest version within a range MinimumVersion - MaximumVersion. + #> + [Alias('Resolve-PSModuleDependencies')] + [CmdletBinding()] + param( + # The path to the manifest file. + [Parameter(Mandatory)] + [string] $ManifestFilePath + ) + + Write-Verbose 'Resolving dependencies' + + $manifest = Import-PowerShellDataFile -Path $ManifestFilePath + Write-Verbose "Reading [$ManifestFilePath]" + Write-Verbose "Found [$($manifest.RequiredModules.Count)] modules to install" + + foreach ($requiredModule in $manifest.RequiredModules) { + $installParams = @{} + + if ($requiredModule -is [string]) { + $installParams.Name = $requiredModule + } else { + $installParams.Name = $requiredModule.ModuleName + $installParams.MinimumVersion = $requiredModule.ModuleVersion + $installParams.RequiredVersion = $requiredModule.RequiredVersion + $installParams.MaximumVersion = $requiredModule.MaximumVersion + } + $installParams.Verbose = $false + $installParams.Force = $true + + Write-Verbose "[$($installParams.Name)] - Installing module" + $VerbosePreferenceOriginal = $VerbosePreference + $VerbosePreference = 'SilentlyContinue' + Install-Module @installParams + $VerbosePreference = $VerbosePreferenceOriginal + Write-Verbose "[$($installParams.Name)] - Importing module" + $VerbosePreferenceOriginal = $VerbosePreference + $VerbosePreference = 'SilentlyContinue' + Import-Module @installParams + $VerbosePreference = $VerbosePreferenceOriginal + Write-Verbose "[$($installParams.Name)] - Done" + } + Write-Verbose 'Resolving dependencies - Done' +} diff --git a/src/Utilities/public/PSModule/Build-PSModule.ps1 b/src/Utilities/public/PSModule/Build-PSModule.ps1 new file mode 100644 index 0000000..a2df2ab --- /dev/null +++ b/src/Utilities/public/PSModule/Build-PSModule.ps1 @@ -0,0 +1,47 @@ +#REQUIRES -Modules Utilities, PSScriptAnalyzer + +function Build-PSModule { + <# + .SYNOPSIS + Builds a module. + + .DESCRIPTION + Builds a module. + #> + [CmdletBinding()] + param( + # Path to the folder where the modules are located. + [Parameter(Mandatory)] + [string] $ModuleSourceFolderPath, + + # Path to the folder where the built modules are outputted. + [Parameter(Mandatory)] + [string] $ModulesOutputFolderPath, + + # Path to the folder where the documentation is outputted. + [Parameter(Mandatory)] + [string] $DocsOutputFolderPath + ) + + $moduleName = Split-Path -Path $ModuleSourceFolderPath -Leaf + + Start-LogGroup "Building module [$moduleName]" + Write-Verbose "Source path: [$ModuleSourceFolderPath]" + if (-not (Test-Path -Path $ModuleSourceFolderPath)) { + Write-Error "Source folder not found at [$ModuleSourceFolderPath]" + exit 1 + } + $moduleSourceFolder = Get-Item -Path $ModuleSourceFolderPath + + $moduleOutputFolder = New-Item -Path $ModulesOutputFolderPath -Name $moduleName -ItemType Directory -Force + Write-Verbose "Module output folder: [$ModulesOutputFolderPath]" + + $docsOutputFolder = New-Item -Path $DocsOutputFolderPath -Name $moduleName -ItemType Directory -Force + Write-Verbose "Docs output folder: [$DocsOutputFolderPath]" + Stop-LogGroup + + Build-PSModuleBase -ModuleSourceFolder $moduleSourceFolder -ModuleOutputFolder $moduleOutputFolder + Build-PSModuleManifest -ModuleOutputFolder $moduleOutputFolder + Build-PSModuleRootModule -ModuleOutputFolder $moduleOutputFolder + Build-PSModuleDocumentation -ModuleOutputFolder $moduleOutputFolder -DocsOutputFolder $docsOutputFolder +} From 7af9eaabb351ca4fdf89a8d2da010975a575f334 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 9 Mar 2024 19:19:27 +0100 Subject: [PATCH 2/2] Fixes --- src/Utilities/private/PSModule/Build-PSModuleBase.ps1 | 4 +--- .../private/PSModule/Build-PSModuleDocumentation.ps1 | 2 +- src/Utilities/private/PSModule/Build-PSModuleManifest.ps1 | 2 +- src/Utilities/private/PSModule/Build-PSModuleRootModule.ps1 | 2 +- src/Utilities/public/PSModule/Build-PSModule.ps1 | 4 +--- 5 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Utilities/private/PSModule/Build-PSModuleBase.ps1 b/src/Utilities/private/PSModule/Build-PSModuleBase.ps1 index 95f2da1..1204d3d 100644 --- a/src/Utilities/private/PSModule/Build-PSModuleBase.ps1 +++ b/src/Utilities/private/PSModule/Build-PSModuleBase.ps1 @@ -1,6 +1,4 @@ -#Requires -Modules Utilities - -function Build-PSModuleBase { +function Build-PSModuleBase { <# .SYNOPSIS Compiles the base module files. diff --git a/src/Utilities/private/PSModule/Build-PSModuleDocumentation.ps1 b/src/Utilities/private/PSModule/Build-PSModuleDocumentation.ps1 index 018346e..29d87a6 100644 --- a/src/Utilities/private/PSModule/Build-PSModuleDocumentation.ps1 +++ b/src/Utilities/private/PSModule/Build-PSModuleDocumentation.ps1 @@ -1,4 +1,4 @@ -#Requires -Modules platyPS, Utilities +#Requires -Modules platyPS function Build-PSModuleDocumentation { <# diff --git a/src/Utilities/private/PSModule/Build-PSModuleManifest.ps1 b/src/Utilities/private/PSModule/Build-PSModuleManifest.ps1 index 26adef3..580de2b 100644 --- a/src/Utilities/private/PSModule/Build-PSModuleManifest.ps1 +++ b/src/Utilities/private/PSModule/Build-PSModuleManifest.ps1 @@ -1,4 +1,4 @@ -#Requires -Modules PSScriptAnalyzer, Utilities +#Requires -Modules PSScriptAnalyzer function Build-PSModuleManifest { <# diff --git a/src/Utilities/private/PSModule/Build-PSModuleRootModule.ps1 b/src/Utilities/private/PSModule/Build-PSModuleRootModule.ps1 index 2c46e8f..c05b38e 100644 --- a/src/Utilities/private/PSModule/Build-PSModuleRootModule.ps1 +++ b/src/Utilities/private/PSModule/Build-PSModuleRootModule.ps1 @@ -1,4 +1,4 @@ -#Requires -Modules PSScriptAnalyzer, Utilities +#Requires -Modules PSScriptAnalyzer function Build-PSModuleRootModule { <# diff --git a/src/Utilities/public/PSModule/Build-PSModule.ps1 b/src/Utilities/public/PSModule/Build-PSModule.ps1 index a2df2ab..ab76b8e 100644 --- a/src/Utilities/public/PSModule/Build-PSModule.ps1 +++ b/src/Utilities/public/PSModule/Build-PSModule.ps1 @@ -1,6 +1,4 @@ -#REQUIRES -Modules Utilities, PSScriptAnalyzer - -function Build-PSModule { +function Build-PSModule { <# .SYNOPSIS Builds a module.