codaamok.build.psm1
#region Private functions function GetPSGalleryNextAvailableVersionNumber { param ( [Parameter(Mandatory)] [String]$ModuleName, [Parameter(Mandatory)] [Version]$VersionToBuild ) Write-Verbose "Qualifying the version number to build with is available in the PowerShell Gallery" -Verbose for ($i = $VersionToBuild.Build; $i -le 100; $i++) { if ($i -eq 100) { throw "You have 100 unlisted packages under the same build number? Sort your life out." } try { $PSGalleryModuleInfo = Find-Module -Name $ModuleName -RequiredVersion $VersionToBuild -ErrorAction "Stop" if ($PSGalleryModuleInfo) { Write-Verbose "Found module in the gallery with the same verison number, adding one to the Build number and will query the gallery again" $VersionToBuild = [System.Version]::New( $VersionToBuild.Major, $VersionToBuild.Minor, $VersionToBuild.Build + $i ) } else { throw "Unusually, there was no object returned or excpetion throw from Find-Module while sussing out unlisted packages" } } catch { if ($_.Exception.Message -match "No match was found for the specified search criteria") { Write-Verbose "Found the next available version number to build with" -Verbose break } else { throw $_ } } } return $VersionToBuild } #endregion #region Public functions function Get-BuildCommands { <# .SYNOPSIS Auxiliary Short description .DESCRIPTION Long description .EXAMPLE PS C:\> <example usage> Explanation of what the example does .INPUTS Inputs (if any) .OUTPUTS Output (if any) .NOTES General notes #> param ( ) $Commands = @{} Get-Command -Module "codaamok.build" | ForEach-Object { $Help = Get-Help -Name $_.Name $Synopsis = $Help.Synopsis if ([String]::IsNullOrWhiteSpace($Synopsis[0])) { $Commands["N/A"] += @($_.Name) } else { $Commands[($Synopsis -split '\n')[0]] += @($_.Name) } } foreach ($Key in $Commands.Keys) { Write-Host $Key -ForegroundColor Blue foreach ($Value in $Commands[$Key]) { Write-Host ("- {0}" -f $Value) -ForegroundColor Green } Write-Host "" } } function New-ModuleDirStructure { <# .SYNOPSIS Setup Short description .DESCRIPTION Long description .EXAMPLE PS C:\> <example usage> Explanation of what the example does .INPUTS Inputs (if any) .OUTPUTS Output (if any) .NOTES General notes #> param ( [Parameter(Mandatory)] [String]$Path, [Parameter(Mandatory)] [String]$ModuleName, [Parameter()] [String]$Author = "Adam Cook (@codaamok)", [Parameter(Mandatory)] [String]$Description, [Parameter()] [String[]]$Tags, [Parameter()] [String]$ProjectUri, [Parameter()] [Switch]$CreateFormatFile, [Parameter()] [Version]$PowerShellVersion = 5.1 ) # Create the module and private function directories @( "$Path\.github\workflows", "$Path\src", "$Path\src\ScriptsToProcess", "$Path\src\Files", "$Path\src\Private", "$Path\src\Public", "$Path\src\Types", "$Path\src\en-US", "$Path\tests", "$Path\build", "$Path\release", "$Path\docs" ) | ForEach-Object { New-Item -Path $_ -ItemType Directory -Force New-Item -Path $_\.gitkeep -ItemType File -Force } #Create the module and related files $GitIgnorePath = Join-Path -Path $Path -ChildPath ".gitignore" $ModuleScript = "{0}.psm1" -f $ModuleName $ModuleScriptPath = Join-Path -Path $Path -ChildPath $ModuleScript $ModuleManifest = "{0}.psd1" -f $ModuleName $ModuleManifestPath = Join-Path -Path $Path -ChildPath $ModuleManifest New-Item $ModuleManifestPath -ItemType File -Force @( '$Public = @( Get-ChildItem -Path $PSScriptRoot\Public -Recurse -Filter "*.ps1" )' '$Private = @( Get-ChildItem -Path $PSScriptRoot\Private -Recurse -Filter "*.ps1" )' 'foreach ($import in @($Public + $Private)) {' ' try {' ' . $import.fullname' ' }' ' catch {' ' Write-Error -Message "Failed to import function $($import.fullname): $_"' ' }' '}' 'Export-ModuleMember -Function $Public.Basename' ) | Set-Content -Path $ModuleManifestPath -Force @( 'build/*' 'release/*' '!*.gitkeep' ) | Set-Content -Path $GitIgnorePath $ModuleHelp = "about_{0}.help.txt" -f $ModuleName $ModuleHelpPath - "{0}\src\en-US\{2}" -f $Path, $ModuleName, $ModuleHelp New-Item $ModuleHelpPath -ItemType File -Force $NewModuleManifestSplat = @{ Path = Join-Path -Path $Path -ChildPath 'src' | Join-Path -ChildPath $ModuleManifest RootModule = $ModuleScript Description = $Description PowerShellVersion = $PowerShellVersion Author = $Author FunctionsToExport = '*' } if ($CreateFormatFile) { $ModuleFormat = "{0}.Format.ps1xml" -f $ModuleName $ModuleFormatPath = "{0}\src\{1}" -f $Path, $ModuleFormat New-Item $ModuleFormatPath -ItemType File -Force $NewModuleManifestSplat["FormatsToProcess"] = $ModuleFormat } if ($ProjectUri) { $NewModuleManifestSplat["ProjectUri"] = $ProjectUri } New-ModuleManifest @NewModuleManifestSplat # Copy the public/exported functions into the public folder, private functions into private folder } function New-ProjectDirStructure { [CmdletBinding()] param ( [Parameter(Mandatory)] [String]$Path, [Parameter(Mandatory)] [String]$Name, [Parameter()] [String]$Platform ) # TODO create CHANGELOG.md, copy github action workflow and build script, create module dir structure } function New-VSCodeTaskFile { <# .SYNOPSIS Setup Short description .DESCRIPTION Long description .EXAMPLE PS C:\> <example usage> Explanation of what the example does .INPUTS Inputs (if any) .OUTPUTS Output (if any) .NOTES General notes #> } function Update-BuildFiles { <# .SYNOPSIS Setup Copy the build files (script + GitHub Actiosn workflow) from the module's install directory to the specified directory .DESCRIPTION Copy the build files (script + GitHub Actiosn workflow) from the module's install directory to the specified directory .EXAMPLE PS C:\> <example usage> Explanation of what the example does #> [CmdletBinding()] param ( [Parameter(Mandatory)] [String]$DestinationPath ) $Module = Get-Module "codaamok.build" # FileList property could be empty if imported the non-released module manifest during development if ([String]::IsNullOrWhiteSpace($Module.FileList)) { $Module = [PSCustomObject]@{ FileList = Get-ChildItem -Path "$($Module.ModuleBase)\Files" -Force | Select-Object -ExpandProperty FullName } } $oldbuildyml = "{0}\.github\workflows\build.yml" -f $DestinationPath if (Test-Path $oldbuildyml) { Remove-Item -Path $oldbuildyml -Confirm } switch -Regex ($Module.FileList) { "pipeline\.yml$" { $Destination = "{0}\.github\workflows" -f $DestinationPath $File = "{0}\pipeline.yml" -f $Destination if (-not (Test-Path $Destination)) { $null = New-Item -Path $Destination -ItemType "Directory" -Force } elseif (Test-Path $File) { $TargetFirstLine = Get-Content $File -TotalCount 1 $SourceFirstLine = Get-Content $_ -TotalCount 1 if ($TargetFirstLine -ne $SourceFirstLine) { Write-Warning -Message 'Will not update pipeline.yml as it appears to be customised (indicated by reading the first line)' continue } } Copy-Item -Path $_ -Destination $Destination -Confirm } "gitignore$" { $Destination = "{0}\.gitignore" -f $DestinationPath Copy-Item -Path $_ -Destination $Destination -Confirm } default { Copy-Item -Path $_ -Destination $DestinationPath -Confirm } } } function Export-RootModule { <# .SYNOPSIS Build Get all of the function definition content for the module and create a single .psm1 with said content .DESCRIPTION Get all of the function definition content for the module and create a single .psm1 with said content .EXAMPLE PS C:\> <example usage> Explanation of what the example does #> [CmdletBinding()] param ( [Parameter(Mandatory)] [String[]]$DevModulePath, [Parameter(Mandatory)] [String[]]$RootModule ) $null = New-Item -Path $RootModule -ItemType "File" -Force foreach ($FunctionType in "Private","Public","Types") { '#region {0} functions' -f $FunctionType | Add-Content -Path $RootModule $Files = @(Get-ChildItem $DevModulePath\$FunctionType -Filter *.ps1 -Recurse) foreach ($File in $Files) { Get-Content -Path $File.FullName | Add-Content -Path $RootModule # Add new line only if the current file isn't the last one (minus 1 because array indexes from 0) if ($Files.IndexOf($File) -ne ($Files.Count - 1)) { Write-Output "" | Add-Content -Path $RootModule } } '#endregion' -f $FunctionType | Add-Content -Path $RootModule Write-Output "" | Add-Content -Path $RootModule } } function Export-ScriptsToProcess { <# .SYNOPSIS Build Create a single Process.ps1 script file for all script files under ScriptsToProcess\* .DESCRIPTION Create a single Process.ps1 script file for all script files under ScriptsToProcess\* .EXAMPLE PS C:\> <example usage> Explanation of what the example does #> [CmdletBinding()] param ( [Parameter(Mandatory)] [String[]]$Path ) $ProcessScript = New-Item -Path $BuildRoot\build\$Script:ModuleName\Process.ps1 -ItemType "File" -Force $Files = @(Get-ChildItem $Path -Filter *.ps1) foreach ($File in $Files) { Get-Content -Path $File.FullName | Add-Content -Path $ProcessScript # Add new line only if the current file isn't the last one (minus 1 because array indexes from 0) if ($Files.IndexOf($File) -ne ($Files.Count - 1)) { Write-Output "" | Add-Content -Path $ProcessScript } } } function Export-UnreleasedNotes { <# .SYNOPSIS Short description .DESCRIPTION Long description .EXAMPLE PS C:\> <example usage> Explanation of what the example does .INPUTS Inputs (if any) .OUTPUTS Output (if any) .NOTES General notes #> [CmdletBinding()] param ( [Parameter(Mandatory)] [String]$Path, [Parameter(Mandatory)] [PSCustomObject]$ChangeLogData, [Parameter()] [Bool]$NewRelease ) $EmptyChangeLog = $true $ReleaseNotes = foreach ($Property in $ChangeLogData.Unreleased[0].Data.PSObject.Properties.Name) { $Data = $ChangeLogData.Unreleased[0].Data.$Property if ($Data) { $EmptyChangeLog = $false Write-Output ("# {0}" -f $Property) foreach ($item in $Data) { Write-Output ("- {0}" -f $item) } } } if ($EmptyChangeLog -eq $true -Or $ReleaseNotes.Count -eq 0) { if ($NewRelease.IsPresent) { throw "Can not build with empty Unreleased section in the change log" } else { $ReleaseNotes = "None" } } Write-Verbose "Release notes:" -Verbose $ReleaseNotes | Write-Verbose -Verbose Set-Content -Value $ReleaseNotes -Path $Path -Force } function Get-BuildVersionNumber { <# .SYNOPSIS Build Qualify the next version number to build with .DESCRIPTION Qualify the next version number to build with .EXAMPLE PS C:\> Get-BuildVersionNumber -ModuleName "PSShlink" -ManifestData $ManifestData -ChangeLogData $ChangeLogData #> param ( [Parameter(Mandatory)] [String]$ModuleName, [Parameter(Mandatory, ParameterSetName='DetermineNextVersion')] [Hashtable]$ManifestData, [Parameter(Mandatory, ParameterSetName='DetermineNextVersion')] [PSCustomObject]$ChangeLogData, [Parameter(Mandatory, ParameterSetName='HardCodeNextVersion')] [Version]$VersionToBuild, [Parameter(ParameterSetName='DetermineNextVersion')] [Switch]$NewRelease ) # Get PowerShell Gallery current verison number (if published) try { $PSGalleryModuleInfo = Find-Module -Name $ModuleName -ErrorAction "Stop" } catch { if ($_.Exception.Message -notmatch "No match was found for the specified search criteria") { throw $_ } else { $PSGalleryModuleInfo = [PSCustomObject]@{ "Name" = $ModuleName "Version" = "0.0" } } } Write-Verbose ("PowerShell Gallery verison: {0}" -f $PSGalleryModuleInfo.Version) -Verbose Write-Verbose ("Changelog version: {0}" -f $ChangeLogData.Released[0].Version) -Verbose Write-Verbose ("Manifest version: {0}" -f $ManifestData.ModuleVersion) -Verbose if (-not $VersionToBuild) { if ($NewRelease.IsPresent) { # Try and piece together an understanding from the module manifest, PowerShell Gallery, and the change log, as to what the next version number should be # If the last released version in the change log and latest version available in the PowerShell gallery do not match, throw an exception - get them level! if ($null -ne $ChangeLogData.Released[0].Version -And $ChangeLogData.Released[0].Version -ne $PSGalleryModuleInfo.Version) { throw "The latest released version in the changelog does not match the latest released version in the PowerShell gallery" } # If module isn't yet published in the PowerShell gallery, and there's no Released section in the change log, set initial version as per the manifest elseif ($PSGalleryModuleInfo.Version -eq "0.0" -And $ChangeLogData.Released.Count -eq 0) { Write-Verbose "Module is not published to the PowerShell Gallery and there is not a Released section in the change log. Will use version from the module manifest." -Verbose $VersionToBuild = [System.Version]$ManifestData.ModuleVersion } # If module isn't yet published in the PowerShell gallery, and there is a Released section in the change log, update version elseif ($PSGalleryModuleInfo.Version -eq "0.0" -And $ChangeLogData.Released.Count -ge 1) { Write-Verbose "Module is not published to the PowerShell Gallery and there is a Released secton in the change log. Will +1 on the minor build from the changelog version." -Verbose $CurrentVersion = [System.Version]$ChangeLogData.Released[0].Version $VersionToBuild = [System.Version]::New( $CurrentVersion.Major, $CurrentVersion.Minor + 1, $CurrentVersion.Build ) } # If the module's PowerShell Gallery version and the last Released verison in the change log are in harmony, update version elseif ($ChangeLogData.Released[0].Version -eq $PSGalleryModuleInfo.Version) { Write-Verbose "Module is published to the PowerShell Gallery and its version matches the last Releases section in the changelog. Will +1 on the mintor build from the PowerShell Gallery version." -Verbose $CurrentVersion = [System.Version]$PSGalleryModuleInfo.Version $VersionToBuild = [System.Version]::New( $CurrentVersion.Major, $CurrentVersion.Minor + 1, $CurrentVersion.Build ) } else { Write-Output ("Latest release version from change log: {0}" -f $ChangeLogData.Released[0].Version) Write-Output ("Latest release version from PowerShell gallery: {0}" -f $PSGalleryModuleInfo.Version) throw "Can not determine next version number" } # Loop through and suss out any unlisted packages for the module in the PowerShell Gallery using the same version number # Keep looping and bumping the build version number by 1 until an available version number is found # Try this process up to 100 times and fail if can't find one # This can execute even if the module is not yet in the gallery because unlisted packages can still be present $VersionToBuild = GetPSGalleryNextAvailableVersionNumber -ModuleName $ModuleName -VersionToBuild $VersionToBuild } else { $VersionToBuild = [System.Version]::New( ([System.Version]$ManifestData.ModuleVersion).Major, ([System.Version]$ManifestData.ModuleVersion).Minor, ([System.Version]$ManifestData.ModuleVersion).Build + 1 ) } } else { Write-Verbose "Version to build with is hard coded" -Verbose if ($PSGalleryModuleInfo.Version -ne "0.0") { Write-Verbose "Module is published to the PowerShell Gallery" -Verbose $VersionToBuild = GetPSGalleryNextAvailableVersionNumber -ModuleName $ModuleName -VersionToBuild $VersionToBuild } else { Write-Verbose "Module not published to the PowerShell Gallery, will build with the given version number" -Verbose } } Write-Verbose ("Version to build: '{0}'" -f $VersionToBuild) -Verbose return $VersionToBuild } function Get-PublicFunctions { <# .SYNOPSIS Build Get a list of functions - as functions to export - defined in script files within the Public directory .DESCRIPTION Get a list of functions - as functions to export - defined in script files within the Public directory .EXAMPLE PS C:\> <example usage> Explanation of what the example does #> [CmdletBinding()] param ( [Parameter(Mandatory)] [String[]]$Path ) $Files = @(Get-ChildItem $Path -Filter *.ps1 -Recurse) foreach ($File in $Files) { $tokens = $errors = @() $Ast = [System.Management.Automation.Language.Parser]::ParseFile( $File.FullName, [ref]$tokens, [ref]$errors ) if ($errors[0].ErrorId -eq 'FileReadError') { throw [InvalidOperationException]::new($errors[0].Message) } Write-Output $Ast.EndBlock.Statements.Name } } function Invoke-BuildClean { <# .SYNOPSIS Build Empty the contents of the build and release directories. If not exist, create them. .DESCRIPTION Empty the contents of the build and release directories. If not exist, create them. .EXAMPLE PS C:\> <example usage> Explanation of what the example does #> [CmdletBinding()] param ( [Parameter(Mandatory)] [String[]]$Path ) foreach ($item in $Path) { if (Test-Path $item) { Remove-Item -Path $item\* -Exclude ".gitkeep" -Recurse -Force } else { $null = New-Item -Path $item -ItemType "Directory" -Force } } } function New-BuildEnvironmentVariable { <# .SYNOPSIS Build Set build and platform specific environment variables. .DESCRIPTION Set build and platform specific environment variables. .EXAMPLE PS C:\> New-BuildEnvironmentVariable -Variables @{ VersionToBuild = "1.2.3" } -Platform "GitHubActions" Writes to GitHub Action's environment variable file to create environment variable "VersionToBuild" with value of "1.2.3". #> param ( [Parameter(Mandatory)] [Hashtable]$Variable, [Parameter(Mandatory)] [ValidateSet("GitHubActions")] [String[]]$Platform ) switch ($Platform) { "GitHubActions" { foreach ($var in $Variable.GetEnumerator()) { Write-Output ("{0}={1}" -f $var.Key, $var.Value) | Add-Content -Path $env:GITHUB_ENV } } } } #endregion #region Types functions #endregion |