KnowIT.Builder.psm1
#region === Source 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, [switch]$MergePSM ) $ErrorActionPreference = 'Stop' try { Write-Build 'Procesing module data file...' $script:ModuleData = GetModuleFileData $Path if($Version) { $ModuleData.Version = $Version } else { $null = ValidateVersion $ModuleData.Version } Write-Build "Module output location: '$($ModuleData.OutputFolder)'" BuildPSM -Merge:$MergePSM BuildManifest $BuildNumber } catch { Write-Error $_ } } ### Source file: 'Get-KnowITModuleInfo.ps1' ### function Get-KnowITModuleInfo { [Alias('moduleinfo')] param( [string]$Path = '.' ) try { $rootFolder = Convert-Path $Path $data = GetModuleFileData $rootFolder [PSCustomObject]$data $null = ValidateVersion $data.Version } catch { Write-Error $_ } } ### Source file: 'BuildPSM.ps1' ### function BuildPSM ([switch]$Merge) { $ErrorActionPreference = 'Stop' try { $moduleName = $ModuleData.ModuleName Write-Build " Building module file: '$moduleName.psm1'..." Push-Location (Join-Path $ModuleData.ProjectFolder 'src') $output = $ModuleData.OutputFolder $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) $sourceFiles = ProcessSourceFolders [void]$sourceBuilder.AppendLine("`n#region === Source functions ===") foreach($source in $sourceFiles) { [void]$sourceBuilder.AppendLine("`n### Source file: '$($source.Name)' ###") Get-Content $source | ParseSource $sourceBuilder $usings $requires } [void]$sourceBuilder.AppendLine("`n#endregion") $currentPSM = "$moduleName.psm1" if($Merge -and (Test-Path $currentPSM)) { Write-Build ' Merging current PSM file...' [void]$sourceBuilder.AppendLine("`n#region === Source .psm1 file ===") Get-Content $currentPSM | 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 "$output/$moduleName.psm1" -Encoding utf8BOM if($extra = $ModuleData.ExtraContent) { Write-Build " Copying extra content: ($($extra -join ', '))..." Copy-Item $extra -Destination $output -Recurse -Force } } finally { Pop-Location } } function ProcessSourceFolders { Write-Build " Processing source files in folders: ($($ModuleData.PSSourceFiles -join ', '))..." foreach($path in $ModuleData.PSSourceFiles) { $files = Get-ChildItem -Filter $path -Directory | Get-ChildItem -Filter '*.ps1' -Recurse if($path -eq 'public') { $ModuleData.PublicFunctions = $files.BaseName } $files } } 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' ### function Write-Build ([string]$Message, $Color) { Write-Verbose $Message -Verbose } ### Source file: 'Manifest.ps1' ### function BuildManifest ([int]$BuildNumber = -1) { $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 }) if($invalidKeys.Count -gt 0) { Write-Warning "Found invalid keys in Manifest: ($($invalidKeys -join ', '))" $invalidKeys.ForEach({ $manifest.Remove($_) }) } $manifest.PrivateData ??= @{} $version, $prerelease = $ModuleData.Version.Split('-', 2) $buildVersion = GetBuildVersion $version $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 if($ModuleData.ExternalModules) { $manifest.RequiredModules = $ModuleData.ExternalModules $manifest.ExternalModuleDependencies = $ModuleData.ExternalModules } # $manifest.NestedModules = $NestedModules # $manifest.RequiredAssemblies = $Assemblies.ForEach({"bin/$_.dll"}) $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 = Convert-Path $RootFolder $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.ProjectFolder = $RootFolder $outDir = $data.OutputFolder ?? 'out' $data.OutputFolder = Join-Path $RootFolder $outDir $data.ModuleName $data } ### 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 |