KnowIT.Builder.psm1
|
#region === Public functions === ### Source file: 'Build-KnowITModule.ps1' ### function Build-KnowITModule { [CmdletBinding(DefaultParameterSetName = 'BuildNumber')] [Alias('build')] param( [Parameter(Position = 0)] [ValidateNotNullOrWhiteSpace()] [string]$Path, [Parameter(ParameterSetName = 'Version')] [ValidateScript({ ValidateVersion $_ })] [string]$Version, [Parameter(ParameterSetName = 'BuildNumber')] [int]$BuildNumber = -1, [string]$OutputFolder, [string[]]$ExtraContent, [hashtable]$Manifest, [switch]$MergePSM ) try { Update-CallerPreference $PSCmdlet Write-Build 'Loading module data file and processing parameters...' $moduleData = GetModuleFileData $Path switch ($PSCmdlet.ParameterSetName) { 'Version' { $moduleData.Version = $Version $moduleData.BuildNumber = -1 } 'BuildNumber' { $null = ValidateVersion $moduleData.Version $moduleData.BuildNumber = $BuildNumber } } switch ($PSBoundParameters.Keys) { 'OutputFolder' { $moduleData.OutputFolder = [IO.Path]::GetFullPath([IO.Path]::Combine($OutputFolder, $moduleData.ModuleName), $PWD.Path) } 'Manifest' { foreach($key in $Manifest.Keys) { if($key -eq 'PrivateData') { $moduleData.Manifest.PrivateData ??= @{} foreach($subkey in $Manifest.PrivateData.Keys) { $moduleData.Manifest.PrivateData[$subkey] = $Manifest.PrivateData[$subkey] } } else { $moduleData.Manifest[$key] = $Manifest[$key] } } } 'MergePSM' { $moduleData.MergePSM = $MergePSM.IsPresent } 'ExtraContent' { $moduleData.ExtraContent = $ExtraContent } } $projectFolder = $moduleData.ProjectFolder Write-Build " Module project folder: '$projectFolder'" Push-Location $projectFolder if(Test-Path src -PathType Container) { Set-Location src } Write-Build " Module build output path: '$($moduleData.OutputFolder)'" Write-Build ProcessSourceFolders $moduleData BuildPSM $moduleData if($extra = $moduleData.ExtraContent) { Write-Build "Copying extra content: ($($extra -join ', '))..." Copy-Item $extra -Destination $moduleData.OutputFolder -Recurse -Force } Write-Build BuildManifest $moduleData } catch { $PSCmdlet.WriteError($_) } finally { Pop-Location } } ### Source file: 'Get-KnowITModuleInfo.ps1' ### function Get-KnowITModuleInfo { [CmdletBinding()] [Alias('moduleinfo')] param( [string]$Path ) try { Update-CallerPreference $PSCmdlet $data = GetModuleFileData $Path [PSCustomObject]$data $null = ValidateVersion $data.Version } catch { $PSCmdlet.WriteError($_) } } ### Source file: 'New-KnowITModule.ps1' ### function New-KnowITModule { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Name, [string]$Path = $PWD.Path ) try { Update-CallerPreference $PSCmdlet $modulePath = Join-Path $Path $Name if(Test-Path $modulePath) { throw "Directory '$modulePath' already exists. Can't create a new module in an existing folder!" } $templatePath = Join-Path $PSModuleRoot 'template' Copy-Item $templatePath $modulePath -Recurse Rename-Item "$modulePath/src/Module.psm1" -NewName "$Name.psm1" Get-ChildItem $modulePath -Filter 'dot.*' | Rename-Item -NewName { $_.Name -replace 'dot.', '.' } $moduleData = Import-PowerShellDataFile $templatePath/module.psd1 $moduleData.ModuleName = $Name $moduleData.ModuleId = New-Guid $moduleFileContent = Get-Content $templatePath/module.psd1 -Raw ReplaceModuleData $moduleFileContent $moduleData | Set-Content $modulePath/module.psd1 -Encoding utf8BOM -Force $gitCommand = Get-Command git -CommandType Application -ErrorAction Ignore if($gitCommand -and !$SkipGitInit) { Push-Location $modulePath & $gitCommand init & $gitCommand add . 2>&1 | Where-Object { $_ -notlike 'warning: *' } Pop-Location } } catch { $PSCmdlet.WriteError($_) } } ### Source file: 'New-KnowITModuleFunction.ps1' ### function New-KnowITModuleFunction { [CmdletBinding()] [Alias('nfunc')] param( [Parameter(Mandatory)] [string]$Name ) try { Update-CallerPreference $PSCmdlet $moduleData = GetModuleFileData $srcFolder = Join-Path $moduleData.ProjectFolder 'src' if(!(Test-Path $srcFolder -PathType Container)) { $srcFolder = $moduleData.ProjectFolder } $publicFolder = $moduleData.PSPublicSource | Select-Object -First 1 $functionFile = Join-Path $srcFolder $publicFolder "$Name.ps1" if(Test-Path $functionFile) { throw "Function file '$Name.ps1' already exists!" } $templateFile = Join-Path $srcFolder $publicFolder "_function.template" if(!(Test-Path $templateFile)) { Write-Debug 'No existing template in current module, using default template.' $templateFile = Join-Path $PSModuleRoot 'template/src/public/_function.template' } $template = Get-Content $templateFile -Raw $template.Replace('{{FunctionName}}', $Name) | Set-Content $functionFile -Encoding utf8BOM Write-Build "New public function file created: $functionFile" if($env:TERM_PROGRAM -eq 'vscode') { if($env:TERM_PROGRAM_VERSION -like '*-insider') { code-insiders -r $functionFile } else { code -r $functionFile } } } catch { $PSCmdlet.WriteError($_) } } ### Source file: 'Write-KnowITBuild.ps1' ### function Write-KnowITBuild { [CmdletBinding()] [Alias('Out-Build','echo')] param( [Parameter(Position = 0, ParameterSetName = 'Message', ValueFromPipeline)] [string]$Message, [Parameter(Position = 1, ParameterSetName = 'Message')] [ConsoleColor]$Color = 'Blue', [Parameter(Mandatory, ParameterSetName = 'BlankLine')] [Alias('LineBreak')] [switch]$BlankLine ) begin { Update-CallerPreference $PSCmdlet if($BlankLine) { Write-Host } } process { try { if($PSCmdlet.ParameterSetName -eq 'BlankLine') { return } Write-Host "[BUILD] $Message" -ForegroundColor $Color } catch { $PSCmdlet.WriteError($_) } } } #endregion #region === Private functions === ### Source file: 'BuildPSM.ps1' ### function BuildPSM ($ModuleData) { $ErrorActionPreference = 'Stop' $moduleName = $ModuleData.ModuleName $output = $moduleData.OutputFolder Write-Build "Building module file: '$moduleName.psm1'..." if(Test-Path $output) { $null = Remove-Item $output -Recurse -Force } $null = New-Item $output -ItemType Directory -Force $sourceBuilder = [Text.StringBuilder]::new() $usings = [Collections.Generic.SortedSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $requires = [Collections.Generic.SortedSet[string]]::new([StringComparer]::OrdinalIgnoreCase) [void]$sourceBuilder.AppendLine("`n#region === Public functions ===") foreach($source in $ModuleData.PublicFiles) { [void]$sourceBuilder.AppendLine("`n### Source file: '$($source.Name)' ###") Get-Content $source | ParseSource $sourceBuilder $usings $requires } [void]$sourceBuilder.AppendLine("`n#endregion") [void]$sourceBuilder.AppendLine("`n#region === Private functions ===") foreach($source in $ModuleData.PrivateFiles) { [void]$sourceBuilder.AppendLine("`n### Source file: '$($source.Name)' ###") Get-Content $source | ParseSource $sourceBuilder $usings $requires } [void]$sourceBuilder.AppendLine("`n#endregion") if($ModuleData.MergePSM) { $sourcePSM = FindCurrentPSM $moduleName Write-Build " Merging '$sourcePSM' file..." [void]$sourceBuilder.AppendLine("`n#region === Source .psm1 file ===") Get-Content $sourcePSM | ParseSource $sourceBuilder $usings $requires -SkipRegion '=== .Source files ===' [void]$sourceBuilder.AppendLine("`n#endregion") } # using directives must be at the top of the file if($usings.Count -gt 0) { [void]$sourceBuilder.Insert(0, "$($usings -join "`n")`n") } if($requires.Count -gt 0) { Write-Build ' Procesing Required Modules...' $ModuleData.ExternalModules.ForEach({ [void]$requires.Add($_) }) $ModuleData.ExternalModules = $requires } $sourceCode = $sourceBuilder.ToString() #TODO:External help # if($script:HelpFile) { # $helpPattern = "(?ms)(\<#.*?\.SYNOPSIS.*?#>)" # $externalHelp = "# .ExternalHelp $ModuleName-help.xml`n" # $sourceCode = $sourceCode -replace $helpPattern, $externalHelp # } $sourceCode | Set-Content "$($ModuleData.OutputFolder)/$moduleName.psm1" -Encoding utf8BOM } filter ParseSource ($Builder, $Usings, $Requires, $SkipRegion) { begin { $skipPattern = [string]::IsNullOrWhiteSpace($SkipRegion) ? '^#region\ SKIP_BUILD' : "^#region\ (SKIP_BUILD|$([regex]::Escape($SkipRegion)))" $skipping = $false $lineNumber = 0 } process { $lineNumber++ switch -Regex ($_) { '^\s*using' { [void]$Usings.Add($_.Trim()) break } '^\s*#requires -Modules\s*(.*)' { $Matches[1].Split(','). ForEach({ [void]$Requires.Add($_.Trim()) }) break } $skipPattern { if($skipping) { throw "Nested skipped regions are not supported. Line: $lineNumber" } $skipping = $true break } '^#endregion' { if(!$skipping) { [void]$SourceBuilder.AppendLine($_) } else { $skipping = $false } } default { if(!$skipping) { [void]$SourceBuilder.AppendLine($_) } } } } } ### Source file: 'Common.ps1' ### ### Source file: 'KnowIT.ModuleHelpers.ps1' ### function Update-CallerPreference { # https://devblogs.microsoft.com/scripting/weekend-scripter-access-powershell-preference-variables/ param( [ValidateNotNull()] [PSTypeName('System.Management.Automation.PSScriptCmdlet')]$ScriptCmdlet = (Get-Variable PSCmdlet -Scope 1 -ValueOnly), [ValidateSet('ErrorAction', 'Warning', 'Verbose', 'Debug', 'Information', 'Progress', 'Confirm', 'WhatIf')] [string[]]$Skip ) $commonParameters = 'ErrorAction', 'Warning', 'Verbose', 'Debug', 'Information', 'Progress', 'Confirm', 'WhatIf' $invocation = $ScriptCmdlet.MyInvocation $commandDebug = $invocation.BoundParameters.ContainsKey('Debug') Write-Debug "Updating [$($invocation.MyCommand)] Preference variables:" -Debug:$commandDebug foreach($p in $commonParameters) { if($invocation.BoundParameters.ContainsKey($p)) { continue } $var = "${p}Preference" if($p -eq 'ErrorAction') { $val = 'Stop' $scope = 'Forced' } elseif($p -in $Skip) { $val = Get-Variable -Scope Global -Name $var -ValueOnly $scope = 'Global' } else { $val = $ScriptCmdlet.GetVariableValue($var) $scope = 'Caller' } Write-Debug " (From $scope scope) $var = $val " -Debug:$commandDebug Set-Variable -Scope 1 -Name $var -Value $val } } function Map-Object { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '', Justification = 'Internal functions')] param([scriptblock]$ScriptBlock) begin { $code = "& { process { $ScriptBlock } }" $pipeline = [scriptblock]::Create($code).GetSteppablePipeline() $pipeline.Begin($true) } process { $pipeline.Process($_) } end { $pipeline.End() } } ### Source file: 'Manifest.ps1' ### function BuildManifest ($ModuleData) { $moduleName = $ModuleData.ModuleName Write-Build "Building Module Manifest: '$moduleName.psd1'..." $manifest = $ModuleData.Manifest $allowedParams = (Get-Command New-ModuleManifest).Parameters.Keys $invalidKeys = $manifest.Keys.Where({ $_ -notin $allowedParams -or !$manifest[$_] }) if($invalidKeys.Count -gt 0) { Write-Warning " Removing invalid or empty keys in Manifest ($($invalidKeys -join ', '))..." $invalidKeys.ForEach({ $manifest.Remove($_) }) } # Manifest default values $manifest.FunctionsToExport = [array]$manifest.FunctionsToExport ?? @() $manifest.AliasesToExport = [array]$manifest.AliasesToExport ?? @() $manifest.CmdletsToExport ??= '' $manifest.VariablesToExport ??= '' $manifest.PrivateData ??= @{} $version, $prerelease = $ModuleData.Version.Split('-', 2) $buildVersion = GetBuildVersion $version $ModuleData.BuildNumber $manifest.ModuleVersion = $buildVersion if($prerelease) { $manifest.PreRelease = $prerelease $fullVersion = "$buildVersion-$prerelease" } else { $fullVersion = $buildVersion } Write-Build "Module final version: [$fullVersion]" $manifest.PrivateData.FullVersion = $fullVersion $manifest.PrivateData.Builder = 'KnowIT.Builder' $manifest.GUID = $ModuleData.ModuleId $manifest.Author = $ModuleData.Author $manifest.Description = $ModuleData.Description $manifest.RootModule = "$moduleName.psm1" $manifest.FunctionsToExport += $ModuleData.PublicFunctions ?? @() $manifest.AliasesToExport += $ModuleData.Aliases ?? @() #TODO: Review dependency handling if($ModuleData.ExternalModules) { $manifest.RequiredModules = $ModuleData.ExternalModules $manifest.ExternalModuleDependencies = $ModuleData.ExternalModules } $manifest.Path = Join-Path $ModuleData.OutputFolder "$moduleName.psd1" Write-Debug 'Module Manifest Parameters:' Write-Debug ($manifest | Out-String) New-ModuleManifest @manifest } ### Source file: 'ModuleData.ps1' ### function GetModuleFileData ([string]$RootFolder) { $ErrorActionPreference = 'Stop' $RootFolder = $RootFolder ? (Convert-Path $RootFolder) : (FindProjectRoot) $moduleFile = Join-Path $RootFolder 'module.psd1' if(!(Test-Path $moduleFile -PathType Leaf)) { throw "Not found 'module.psd1' file in project folder [$RootFolder]" } $data = Import-PowerShellDataFile $moduleFile -ErrorAction Stop $requiredKeys = 'ModuleName', 'ModuleId', 'Description', 'Author', 'Version', 'PSSourceFiles' $missingKeys = $requiredKeys.Where({ $_ -notin $data.Keys }) if($missingKeys) { throw "Missing required keys in module.psd1: ($($missingKeys -join ', '))" } $data.Manifest ??= @{} $data.ProjectFolder = $RootFolder $outDir = $data.OutputFolder ?? 'out' $data.OutputFolder = [IO.Path]::GetFullPath([IO.Path]::Combine($RootFolder, $outDir, $data.ModuleName)) if($data.PSSourceFiles -is [hashtable]) { $data.PSPublicSource = $data.PSSourceFiles['Public'] $sourceFiles = $data.PSSourceFiles.Values.ForEach({ $_ }) $data.PSSourceFiles = $sourceFiles } else { $data.PSPublicSource = 'public' } $data } function FindProjectRoot { $current = $PWD.Path Write-Debug 'Checkin folders for project root:' while ($current) { Write-Debug " $current" if(Test-Path "$current/module.psd1") { return $current } $current = Split-Path $current } throw 'Project Root Folder not found!' } function ReplaceModuleData ([string[]]$Content, [hashtable]$Data) { $Content -replace 'KnowIT.Module', $Data.ModuleName -replace '(?:\#+\s*)?(\w+\s*)=\s*(.+)', { $key = $_.Groups[1].Value.Trim() if(!$Data.ContainsKey($key) -or $_.Groups[2].Value -eq '@{') { return $_.Value } $value = switch ($Data[$key]) { { $_ -is [int] -or $_ -is [double] } { $_ } { $_ -is [bool] } { $_ ? '$true' : '$false' } default { "'$_'" } } '{0}= {1}' -f $_.Groups[1].Value, ($value -join ', ') } } ### Source file: 'Sources.ps1' ### function ProcessSourceFolders ($ModuleData) { Write-Build "Processing source files in folders: ($($ModuleData.PSSourceFiles -join ', '))..." $ModuleData.PublicFiles = [Collections.ArrayList]::new() $ModuleData.PrivateFiles = [Collections.ArrayList]::new() foreach($path in $ModuleData.PSSourceFiles) { [array]$files = Get-ChildItem -Filter $path -Directory | Get-ChildItem -Filter '*.ps1' -Recurse if(!$files) { continue } if($path -in $ModuleData.PSPublicSource) { $ModuleData.PublicFiles.AddRange($files) } else { $ModuleData.PrivateFiles.AddRange($files) } } $publicAst = foreach($source in $ModuleData.PublicFiles) { $ast = [Management.Automation.Language.Parser]::ParseFile($source.FullName, [ref]$null, [ref]$null) $ast.Find({ $args[0] -is [Management.Automation.Language.FunctionDefinitionAst] }, $false) } if($publicAst) { $ModuleData.PublicFunctions = $publicAst.Name $ModuleData.Aliases = $publicAst.Body.ParamBlock.Attributes. Where({ $_.TypeName.Name -eq 'Alias' }). PositionalArguments.Value } else { Write-Warning "No public functions found in source files! Check PSSourceFiles setting." } Write-Build " Found $($ModuleData.PublicFunctions.Count) public functions and $($ModuleData.Aliases.Count) aliases" Write-Build } function FindCurrentPSM ([string]$ModuleName) { $psm = "$ModuleName.psm1" if(Test-Path $psm -PathType Leaf) { return $psm } $files = Get-Item *.psm1 if(!$files) { throw 'No existing .psm1 file found!' } if($files.Count -gt 1) { throw "Found $($files.Count) .psm1 files! Can't merge multiple files." } return $files.Name } ### Source file: 'Version.ps1' ### function ValidateVersion ([string]$Version) { $regex = '^(0|[1-9]\d*)(\.(0|[1-9]\d*))?\.(?:x|X|0|[1-9]\d*)(?:-[A-Za-z0-9]+)?$' if($Version -notmatch $regex) { throw "Invalid version format ($Version). Please use 'Major[.Minor].Build' and an optional prerelease (only alphanumeric characters). Last segment can be 'x' to indicate an incremental build number." } return $true } function GetBuildVersion ([string]$Version, [int]$BuildNumber = -1) { # Fixed version whithout a build number in parameters return as.is if($BuildNumber -eq (-1) -and -not $Version.Contains('x')) { return [version]$Version } Write-Build ' Applying Build Number:' if($BuildNumber -eq -1) { $BuildNumber = [Math]::Floor([DateTimeOffset]::Now.ToUnixTimeSeconds() / 60) Write-Build " |$BuildNumber| (UnixMinutes)" } else { Write-Build " |$BuildNumber| (Parameter)" } $segments = $Version.Replace('.x', '.0').Split('.') [version]::new($segments[0], $segments[1], $BuildNumber) } #endregion #region === Source .psm1 file === #using namespace System.Management.Automation #norequires -Modules CimCmdlets #region === Module initialization === $script:PSModuleRoot = $PSScriptRoot # Alias for internal use only (not exported) Set-Alias -Name Write-Build -Value Write-KnowITBuild #endregion #endregion |