PSPublishModule.psm1
$LibraryName = 'PSPublishModule' $Library = "$LibraryName.dll" $Class = "$LibraryName.Initialize" $AssemblyFolders = Get-ChildItem -Path $PSScriptRoot\Lib -Directory -ErrorAction SilentlyContinue $Default = $false $Core = $false $Standard = $false foreach ($A in $AssemblyFolders.Name) { if ($A -eq 'Default') { $Default = $true } elseif ($A -eq 'Core') { $Core = $true } elseif ($A -eq 'Standard') { $Standard = $true } } if ($Standard -and $Core -and $Default) { $FrameworkNet = 'Default' $Framework = 'Standard' } elseif ($Standard -and $Core) { $Framework = 'Standard' $FrameworkNet = 'Standard' } elseif ($Core -and $Default) { $Framework = 'Core' $FrameworkNet = 'Default' } elseif ($Standard -and $Default) { $Framework = 'Standard' $FrameworkNet = 'Default' } elseif ($Standard) { $Framework = 'Standard' $FrameworkNet = 'Standard' } elseif ($Core) { $Framework = 'Core' $FrameworkNet = '' } elseif ($Default) { $Framework = '' $FrameworkNet = 'Default' } else { Write-Error -Message 'No assemblies found' } if ($PSEdition -eq 'Core') { $LibFolder = $Framework } else { $LibFolder = $FrameworkNet } try { $ImportModule = Get-Command -Name Import-Module -Module Microsoft.PowerShell.Core if (-not ($Class -as [type])) { & $ImportModule ([IO.Path]::Combine($PSScriptRoot, 'Lib', $LibFolder, $Library)) -ErrorAction Stop } else { $Type = "$Class" -as [Type] & $importModule -Force -Assembly ($Type.Assembly) } } catch { if ($ErrorActionPreference -eq 'Stop') { throw } else { Write-Warning -Message "Importing module $Library failed. Fix errors before continuing. Error: $($_.Exception.Message)" } } . $PSScriptRoot\PSPublishModule.Libraries.ps1 function Add-Artefact { [CmdletBinding()] param( [string] $ModuleName, [string] $ModuleVersion, [string] $ArtefactName, [alias('IncludeTagName')][nullable[bool]] $IncludeTag, [nullable[bool]] $LegacyName, [nullable[bool]] $CopyMainModule, [nullable[bool]] $CopyRequiredModules, [string] $ProjectPath, [string] $Destination, [string] $DestinationMainModule, [string] $DestinationRequiredModules, [nullable[bool]] $DestinationFilesRelative, [alias('DestinationDirectoriesRelative')][nullable[bool]] $DestinationFoldersRelative, [alias('FilesOutput')][System.Collections.IDictionary] $Files, [alias('DirectoryOutput')][System.Collections.IDictionary] $Folders, [array] $RequiredModules, # [string] $TagName, #[string] $FileName, [nullable[bool]] $ZipIt, [string] $DestinationZip, [bool] $ConvertToScript, [string] $ScriptName, [string] $PreScriptMerge, [string] $PostScriptMerge, [System.Collections.IDictionary] $Configuration, [string] $ID, [switch] $DoNotClear ) $DestinationMainModule = Initialize-ReplacePath -ReplacementPath $DestinationMainModule -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration $DestinationRequiredModules = Initialize-ReplacePath -ReplacementPath $DestinationRequiredModules -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration $DestinationZip = Initialize-ReplacePath -ReplacementPath $DestinationZip -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration $Destination = Initialize-ReplacePath -ReplacementPath $Destination -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration $ResolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Destination) if (-not $DoNotClear) { if (Test-Path -LiteralPath $ResolvedDestination) { Write-TextWithTime -Text "Removing files/folders from $ResolvedDestination before copying artefacts" -SpacesBefore ' ' -PreAppend Minus { Remove-ItemAlternative -Path $ResolvedDestination -SkipFolder -Exclude '*.zip' -ErrorAction Stop } -ColorBefore Yellow -ColorTime Green -ColorError Red -Color Yellow } } if ($ConvertToScript) { Write-TextWithTime -Text "Converting merged release to script" -PreAppend Plus -SpacesBefore ' ' { $convertToScriptSplat = @{ Enabled = $true IncludeTagName = $IncludeTag ModuleName = $ModuleName Destination = $DestinationMainModule PreScriptMerge = $PreScriptMerge PostScriptMerge = $PostScriptMerge ScriptName = $ScriptName Configuration = $Configuration ModuleVersion = $ModuleVersion } Remove-EmptyValue -Hashtable $convertToScriptSplat Copy-ArtefactToScript @convertToScriptSplat $copyRequiredModuleSplat = @{ Enabled = $CopyRequiredModules RequiredModules = $RequiredModules Destination = $DestinationRequiredModules } Copy-ArtefactRequiredModule @copyRequiredModuleSplat $copyArtefactRequiredFoldersSplat = @{ FoldersInput = $Folders ProjectPath = $ProjectPath Destination = $Destination DestinationRelative = $DestinationFoldersRelative } Copy-ArtefactRequiredFolders @copyArtefactRequiredFoldersSplat $copyArtefactRequiredFilesSplat = @{ FilesInput = $Files ProjectPath = $ProjectPath Destination = $Destination DestinationRelative = $DestinationFilesRelative } Copy-ArtefactRequiredFiles @copyArtefactRequiredFilesSplat } } else { Write-TextWithTime -Text "Copying merged release to $ResolvedDestination" -PreAppend Addition -SpacesBefore ' ' { $copyMainModuleSplat = @{ Enabled = $true IncludeTagName = $IncludeTag ModuleName = $ModuleName Destination = $DestinationMainModule } Copy-ArtefactMainModule @copyMainModuleSplat $copyRequiredModuleSplat = @{ Enabled = $CopyRequiredModules RequiredModules = $RequiredModules Destination = $DestinationRequiredModules } Copy-ArtefactRequiredModule @copyRequiredModuleSplat $copyArtefactRequiredFoldersSplat = @{ FoldersInput = $Folders ProjectPath = $ProjectPath Destination = $Destination DestinationRelative = $DestinationFoldersRelative } Copy-ArtefactRequiredFolders @copyArtefactRequiredFoldersSplat $copyArtefactRequiredFilesSplat = @{ FilesInput = $Files ProjectPath = $ProjectPath Destination = $Destination DestinationRelative = $DestinationFilesRelative } Copy-ArtefactRequiredFiles @copyArtefactRequiredFilesSplat } } if ($ZipIt -and $DestinationZip) { $ResolvedDestinationZip = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationZip) Write-TextWithTime -Text "Zipping merged release to $ResolvedDestinationZip" -PreAppend Information -SpacesBefore ' ' { $zipSplat = @{ Source = $ResolvedDestination Destination = $ResolvedDestinationZip Configuration = $Configuration LegacyName = if ($Configuration.Steps.BuildModule.Releases -is [bool]) { $true } else { $false } ModuleName = $ModuleName ModuleVersion = $ModuleVersion IncludeTag = $IncludeTag ArtefactName = $ArtefactName ID = $ID } Compress-Artefact @zipSplat } Write-TextWithTime -Text "Removing temporary files from $ResolvedDestination" -SpacesBefore ' ' -PreAppend Minus { Remove-ItemAlternative -Path $ResolvedDestination -SkipFolder -Exclude '*.zip' -ErrorAction Stop } -ColorBefore Yellow -ColorTime Green -ColorError Red -Color Yellow } } function Add-BinaryImportModule { <# .SYNOPSIS Add code into PSM1 that will import binary modules based on the edition .DESCRIPTION Add code into PSM1 that will import binary modules based on the edition .PARAMETER LibrariesStandard Parameter description .PARAMETER LibrariesCore Parameter description .PARAMETER LibrariesDefault Parameter description .PARAMETER Configuration Parameter description .EXAMPLE Add-BinaryImportModule -Configuration $Configuration -LibrariesStandard $LibrariesStandard -LibrariesCore $LibrariesCore -LibrariesDefault $LibrariesDefault .NOTES .OUTPUT # adds code into PSM1 file similar to this one if ($PSEdition -eq 'Core') { Import-Module -Name "$PSScriptRoot\Lib\Standard\PSEventViewer.PowerShell.dll" -Force -ErrorAction Stop } else { Import-Module -Name "$PSScriptRoot\Lib\Default\PSEventViewer.PowerShell.dll" -Force -ErrorAction Stop } #> [CmdletBinding()] param( [string[]] $LibrariesStandard, [string[]] $LibrariesCore, [string[]] $LibrariesDefault, [System.Collections.IDictionary] $Configuration ) if ($null -ne $Configuration.Steps.BuildLibraries.BinaryModule) { foreach ($BinaryModule in $Configuration.Steps.BuildLibraries.BinaryModule) { if ($LibrariesStandard.Count -gt 0) { foreach ($Library in $LibrariesStandard) { if ($Library -like "*\$BinaryModule") { "Import-Module -Name `"`$PSScriptRoot\$Library`" -Force -ErrorAction Stop" } } } elseif ($LibrariesCore.Count -gt 0 -and $LibrariesDefault.Count -gt 0) { 'if ($PSEdition -eq ''Core'') {' if ($LibrariesCore.Count -gt 0) { foreach ($Library in $LibrariesCore) { if ($Library -like "*\$BinaryModule") { "Import-Module -Name `"`$PSScriptRoot\$Library`" -Force -ErrorAction Stop" } } } '} else {' if ($LibrariesDefault.Count -gt 0) { foreach ($Library in $LibrariesDefault) { if ($Library -like "*\$BinaryModule") { "Import-Module -Name `"`$PSScriptRoot\$Library`" -Force -ErrorAction Stop" } } } '}' } else { if ($LibrariesCore.Count -gt 0) { if ($LibrariesCore.Count -gt 0) { foreach ($Library in $LibrariesCore) { if ($Library -like "*\$BinaryModule") { "Import-Module -Name `"`$PSScriptRoot\$Library`" -Force -ErrorAction Stop" } } } } if ($LibrariesDefault.Count -gt 0) { foreach ($Library in $LibrariesDefault) { if ($Library -like "*\$BinaryModule") { "Import-Module -Name `"`$PSScriptRoot\$Library`" -Force -ErrorAction Stop" } } } } } } } function Add-Directory { [CmdletBinding()] param( [string] $Directory ) $exists = Test-Path -Path $Directory if ($exists -eq $false) { $null = New-Item -Path $Directory -ItemType Directory -Force } } function Approve-RequiredModules { [CmdletBinding()] param( [Array] $ApprovedModules, [Array] $ModulesToCheck, [Array] $RequiredModules, [Array] $DependantRequiredModules, [System.Collections.IDictionary] $MissingFunctions, [System.Collections.IDictionary] $Configuration, [Array] $CommandsWithoutModule ) $TerminateEarly = $false Write-TextWithTime -Text "Pre-Verification of approved modules" { foreach ($ApprovedModule in $ApprovedModules) { $ApprovedModuleStatus = Get-Module -Name $ApprovedModule -ListAvailable if ($ApprovedModuleStatus) { Write-Text " [>] Approved module $ApprovedModule exists - can be used for merging." -Color Green } else { Write-Text " [>] Approved module $ApprovedModule doesn't exists. Potentially issue with merging." -Color Red } } } -PreAppend Plus Write-TextWithTime -Text "Analyze required, approved modules" { foreach ($Module in $ModulesToCheck.Source | Sort-Object) { if ($Module -in $RequiredModules -and $Module -in $ApprovedModules) { Write-Text " [+] Module $Module is in required modules with ability to merge." -Color DarkYellow $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module }) foreach ($F in $MyFunctions) { if ($F.IsPrivate) { Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsPrivate: $($F.IsPrivate))" -Color Magenta } else { Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsPrivate: $($F.IsPrivate))" -Color DarkYellow } } } elseif ($Module -in $DependantRequiredModules -and $Module -in $ApprovedModules) { Write-Text " [+] Module $Module is in dependant required module within required modules with ability to merge." -Color DarkYellow $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module }) foreach ($F in $MyFunctions) { Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color DarkYellow } } elseif ($Module -in $DependantRequiredModules) { Write-Text " [+] Module $Module is in dependant required module within required modules." -Color DarkGray $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module }) foreach ($F in $MyFunctions) { Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color DarkGray } } elseif ($Module -in $RequiredModules) { Write-Text " [+] Module $Module is in required modules." -Color Green $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module }) foreach ($F in $MyFunctions) { Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color Green } } elseif ($Module -notin $RequiredModules -and $Module -in $ApprovedModules) { Write-Text " [+] Module $Module is missing in required module, but it's in approved modules." -Color Magenta $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module }) foreach ($F in $MyFunctions) { Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color Magenta } } else { [Array] $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module }) if ($Configuration.Options.Merge.ModuleSkip.Force -eq $true) { Write-Text " [-] Module $Module is missing in required modules. Non-critical issue as per configuration (force used)." -Color Gray foreach ($F in $MyFunctions) { Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate)). Ignored by configuration." -Color Gray } } else { if ($Module -in $Configuration.Options.Merge.ModuleSkip.IgnoreModuleName) { Write-Text " [-] Module $Module is missing in required modules. Non-critical issue as per configuration (skipped module)." -Color Gray foreach ($F in $MyFunctions) { Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate)). Ignored by configuration." -Color Gray } } else { $FoundProblem = $false foreach ($F in $MyFunctions) { if ($F.Name -notin $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) { $FoundProblem = $true } } if (-not $FoundProblem) { Write-Text " [-] Module $Module is missing in required modules. Non-critical issue as per configuration (skipped functions)." -Color Gray foreach ($F in $MyFunctions) { if ($F.Name -in $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) { Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate)). Ignored by configuration." -Color Gray } else { Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color Red } } } else { $TerminateEarly = $true Write-Text " [-] Module $Module is missing in required modules. Potential issue. Fix configuration required." -Color Red foreach ($F in $MyFunctions) { if ($F.Name -in $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) { Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate)). Ignored by configuration." -Color Gray } else { Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color Red } } } } } } } if ($CommandsWithoutModule.Count -gt 0) { $FoundProblem = $false foreach ($F in $CommandsWithoutModule) { if ($F.Name -notin $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) { $FoundProblem = $true } } if ($FoundProblem) { Write-Text " [-] Some commands couldn't be resolved to functions (private function maybe?). Potential issue." -Color Red foreach ($F in $CommandsWithoutModule) { if ($F.Name -notin $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) { $TerminateEarly = $true Write-Text " [>] Command affected $($F.Name) (Command Type: Unknown / IsAlias: $($F.IsAlias))" -Color Red } else { Write-Text " [>] Command affected $($F.Name) (Command Type: Unknown / IsAlias: $($F.IsAlias)). Ignored by configuration." -Color Gray } } } else { Write-Text " [-] Some commands couldn't be resolved to functions (private function maybe?). Non-critical issue as per configuration (skipped functions)." -Color Gray foreach ($F in $CommandsWithoutModule) { if ($F.Name -in $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) { Write-Text " [>] Command affected $($F.Name) (Command Type: Unknown / IsAlias: $($F.IsAlias)). Ignored by configuration." -Color Gray } else { Write-Text " [>] Command affected $($F.Name) (Command Type: Unknown / IsAlias: $($F.IsAlias))" -Color Red } } } } if ($TerminateEarly) { Write-Text " [-] Some commands are missing in required modules. Fix this issue or use New-ConfigurationModuleSkip to skip verification." -Color Red return $false } } -PreAppend Plus } function Compress-Artefact { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [string] $Source, [string] $Destination, [string] $ModuleName, [string] $ModuleVersion, [nullable[bool]] $IncludeTag, [nullable[bool]] $LegacyName, [string] $ArtefactName, [string] $ID ) if ($Configuration.CurrentSettings.PreRelease) { $ModuleVersionWithPreRelease = "$($ModuleVersion)-$($Configuration.CurrentSettings.PreRelease)" $TagModuleVersionWithPreRelease = "v$($ModuleVersionWithPreRelease)" } else { $ModuleVersionWithPreRelease = $ModuleVersion $TagModuleVersionWithPreRelease = "v$($ModuleVersion)" } if ($LegacyName) { $FileName = -join ("v$($ModuleVersion)", '.zip') } elseif ($ArtefactName) { $TagName = "v$($ModuleVersion)" $FileName = $ArtefactName $FileName = $FileName.Replace('{ModuleName}', $ModuleName) $FileName = $FileName.Replace('<ModuleName>', $ModuleName) $FileName = $FileName.Replace('{ModuleVersion}', $ModuleVersion) $FileName = $FileName.Replace('<ModuleVersion>', $ModuleVersion) $FileName = $FileName.Replace('{ModuleVersionWithPreRelease}', $ModuleVersionWithPreRelease) $FileName = $FileName.Replace('<ModuleVersionWithPreRelease>', $ModuleVersionWithPreRelease) $FileName = $FileName.Replace('{TagModuleVersionWithPreRelease}', $TagModuleVersionWithPreRelease) $FileName = $FileName.Replace('<TagModuleVersionWithPreRelease>', $TagModuleVersionWithPreRelease) $FileName = $FileName.Replace('{TagName}', $TagName) $FileName = $FileName.Replace('<TagName>', $TagName) $FileName = if ($FileName.EndsWith(".zip")) { $FileName } else { -join ($FileName, '.zip') } } else { if ($IncludeTag) { $TagName = "v$($ModuleVersion)" } else { $TagName = '' } if ($TagName) { $FileName = -join ($ModuleName, ".$TagName", '.zip') } else { $FileName = -join ($ModuleName, '.zip') } } $ZipPath = [System.IO.Path]::Combine($Destination, $FileName) $ZipPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ZipPath) $Configuration.CurrentSettings.ArtefactZipName = $FileName $Configuration.CurrentSettings.ArtefactZipPath = $ZipPath if ($ID) { $ZipPackage = [ordered] @{ 'Id' = $ID 'ZipName' = $FileName 'ZipPath' = $ZipPath } $Configuration.CurrentSettings['Artefact'] += $ZipPackage } else { if (-not $Configuration.CurrentSettings['ArtefactDefault']) { $Configuration.CurrentSettings['ArtefactDefault'] = [ordered] @{ 'Id' = 'Default' 'ZipName' = $FileName 'ZipPath' = $ZipPath } } } $Success = Write-TextWithTime -Text "Compressing final merged release $ZipPath" { $null = New-Item -ItemType Directory -Path $Destination -Force [Array] $DirectoryToCompress = Get-ChildItem -Path $Source -Directory -ErrorAction SilentlyContinue $FilesToCompressSource = [System.IO.Path]::Combine($Source, "*") [Array] $FilesToCompress = Get-ChildItem -Path $FilesToCompressSource -File -Exclude '*.zip' -ErrorAction SilentlyContinue if ($DirectoryToCompress.Count -gt 0 -and $FilesToCompress.Count -gt 0) { Compress-Archive -Path @( $DirectoryToCompress.FullName $FilesToCompress.FullName ) -DestinationPath $ZipPath -Force -ErrorAction Stop } elseif ($DirectoryToCompress.Count -gt 0) { Compress-Archive -Path $DirectoryToCompress.FullName -DestinationPath $ZipPath -Force -ErrorAction Stop } elseif ($FilesToCompress.Count -gt 0) { Compress-Archive -Path $FilesToCompress.FullName -DestinationPath $ZipPath -Force -ErrorAction Stop } } -PreAppend Addition -SpacesBefore ' ' -Color Yellow -ColorTime Green -ColorBefore Yellow -ColorError Red if ($Success -eq $false) { return $false } } function Convert-FileEncoding { <# .SYNOPSIS Converts files from one encoding to another. .DESCRIPTION Reads a single file or all files within a directory and rewrites them using a new encoding. Useful for converting files from UTF8 with BOM to UTF8 without BOM or any other supported encoding. Files are only converted when their detected encoding matches the provided SourceEncoding unless -Force is used. If a file already uses the target encoding it is skipped. After conversion the content is verified to ensure it matches the original string. If the content differs the change is rolled back by default unless -NoRollbackOnMismatch is specified. Supports -WhatIf for previewing changes. .PARAMETER Path Specifies the file or directory to process. .PARAMETER Filter Filters which files are processed when Path is a directory. Wildcards are supported. .PARAMETER SourceEncoding Encoding used when reading files. The default is UTF8BOM. .PARAMETER TargetEncoding Encoding used when writing files. The default is UTF8. .PARAMETER Recurse When Path is a directory, process files in all subdirectories as well. .PARAMETER Force Convert files even when their detected encoding does not match SourceEncoding. .PARAMETER NoRollbackOnMismatch Skip rolling back files when the verification step detects that content changed during conversion. .EXAMPLE Convert-FileEncoding -Path 'C:\Scripts' -Filter '*.ps1' -SourceEncoding UTF8BOM -TargetEncoding UTF8 Converts all PowerShell scripts under C:\Scripts from UTF8 with BOM to UTF8. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string] $Path, [string] $Filter = '*.*', [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')] [string] $SourceEncoding = 'UTF8BOM', [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')] [string] $TargetEncoding = 'UTF8', [switch] $Recurse, [switch] $Force, [switch] $NoRollbackOnMismatch ) $source = Resolve-Encoding -Name $SourceEncoding $target = Resolve-Encoding -Name $TargetEncoding if (Test-Path -LiteralPath $Path -PathType Leaf) { $files = Get-Item -LiteralPath $Path } elseif (Test-Path -LiteralPath $Path -PathType Container) { $gciParams = @{ LiteralPath = $Path; File = $true; Filter = $Filter } if ($Recurse) { $gciParams.Recurse = $true } $files = Get-ChildItem @gciParams } else { throw "Path $Path not found" } foreach ($file in $files) { $result = Convert-FileEncodingSingle -FilePath $file.FullName -SourceEncoding $source -TargetEncoding $target -Force:$Force -NoRollbackOnMismatch:$NoRollbackOnMismatch -WhatIf:$WhatIfPreference if ($result) { Write-Verbose "File: $($result.FilePath) - Status: $($result.Status) - Reason: $($result.Reason)" if ($result.Warning) { Write-Warning $result.Warning } } } } function Convert-FileEncodingSingle { <# .SYNOPSIS Converts a single file from one encoding to another with validation and rollback protection. .DESCRIPTION Internal helper function that converts a single file's encoding with comprehensive validation. Includes content verification and automatic rollback on mismatch to prevent data corruption. .PARAMETER FilePath Full path to the file to convert. .PARAMETER SourceEncoding Expected source encoding of the file. .PARAMETER TargetEncoding Target encoding to convert the file to. .PARAMETER Force Convert file even if detected encoding doesn't match SourceEncoding. .PARAMETER NoRollbackOnMismatch Skip rolling back changes when content verification fails. .PARAMETER CreateBackup Create a backup file before conversion for additional safety. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string] $FilePath, [Parameter(Mandatory)] [System.Text.Encoding] $SourceEncoding, [Parameter(Mandatory)] [System.Text.Encoding] $TargetEncoding, [switch] $Force, [switch] $NoRollbackOnMismatch, [switch] $CreateBackup ) $bytesBefore = $null $backupPath = $null try { $detectedObj = Get-FileEncoding -Path $FilePath -AsObject $detected = $detectedObj.Encoding $detectedName = $detectedObj.EncodingName $sourceExpected = if ($SourceEncoding -is [System.Text.UTF8Encoding] -and $SourceEncoding.GetPreamble().Length -eq 3) { 'UTF8BOM' } elseif ($SourceEncoding -is [System.Text.UTF8Encoding]) { 'UTF8' } elseif ($SourceEncoding -is [System.Text.UnicodeEncoding]) { 'Unicode' } elseif ($SourceEncoding -is [System.Text.UTF7Encoding]) { 'UTF7' } elseif ($SourceEncoding -is [System.Text.UTF32Encoding]) { 'UTF32' } elseif ($SourceEncoding -is [System.Text.ASCIIEncoding]) { 'ASCII' } elseif ($SourceEncoding -is [System.Text.BigEndianUnicodeEncoding]) { 'BigEndianUnicode' } else { $SourceEncoding.WebName } if ($detectedName -ne $sourceExpected -and -not $Force) { Write-Verbose "Skipping $FilePath because detected encoding '$detectedName' does not match expected '$sourceExpected'." return @{ FilePath = $FilePath Status = 'Skipped' Reason = "Encoding mismatch: detected '$detectedName', expected '$sourceExpected'" DetectedEncoding = $detectedName } } $targetExpected = if ($TargetEncoding -is [System.Text.UTF8Encoding] -and $TargetEncoding.GetPreamble().Length -eq 3) { 'UTF8BOM' } elseif ($TargetEncoding -is [System.Text.UTF8Encoding]) { 'UTF8' } elseif ($TargetEncoding -is [System.Text.UnicodeEncoding]) { 'Unicode' } elseif ($TargetEncoding -is [System.Text.UTF7Encoding]) { 'UTF7' } elseif ($TargetEncoding -is [System.Text.UTF32Encoding]) { 'UTF32' } elseif ($TargetEncoding -is [System.Text.ASCIIEncoding]) { 'ASCII' } elseif ($TargetEncoding -is [System.Text.BigEndianUnicodeEncoding]) { 'BigEndianUnicode' } else { $TargetEncoding.WebName } if ($detectedName -eq $targetExpected) { Write-Verbose "Skipping $FilePath because encoding is already '$targetExpected'." return @{ FilePath = $FilePath Status = 'Skipped' Reason = "Already target encoding '$targetExpected'" DetectedEncoding = $detectedName } } if ($PSCmdlet.ShouldProcess($FilePath, "Convert from '$detectedName' to '$targetExpected'")) { $content = [System.IO.File]::ReadAllText($FilePath, $detected) $bytesBefore = [System.IO.File]::ReadAllBytes($FilePath) if ($CreateBackup) { $backupPath = "$FilePath.backup" $counter = 1 while (Test-Path $backupPath) { $backupPath = "$FilePath.backup$counter" $counter++ } [System.IO.File]::WriteAllBytes($backupPath, $bytesBefore) Write-Verbose "Created backup at: $backupPath" } [System.IO.File]::WriteAllText($FilePath, $content, $TargetEncoding) $convertedContent = [System.IO.File]::ReadAllText($FilePath, $TargetEncoding) if ($convertedContent -ne $content) { $warningMsg = "Content verification failed for $FilePath - characters may have been lost during conversion" Write-Warning $warningMsg if (-not $NoRollbackOnMismatch) { [System.IO.File]::WriteAllBytes($FilePath, $bytesBefore) Write-Warning "Reverted changes to $FilePath due to content mismatch" return @{ FilePath = $FilePath Status = 'Failed' Reason = 'Content verification failed - reverted' DetectedEncoding = $detectedName BackupPath = $backupPath } } else { return @{ FilePath = $FilePath Status = 'Converted' Reason = 'Content verification failed but conversion kept' DetectedEncoding = $detectedName TargetEncoding = $targetExpected BackupPath = $backupPath Warning = $warningMsg } } } Write-Verbose "Successfully converted $FilePath from '$detectedName' to '$targetExpected'" return @{ FilePath = $FilePath Status = 'Converted' Reason = 'Successfully converted' DetectedEncoding = $detectedName TargetEncoding = $targetExpected BackupPath = $backupPath } } } catch { $errorMsg = "Failed to convert ${FilePath}: $_" Write-Warning $errorMsg if (-not $NoRollbackOnMismatch -and $bytesBefore) { try { [System.IO.File]::WriteAllBytes($FilePath, $bytesBefore) Write-Verbose "Rolled back $FilePath due to conversion error" } catch { Write-Warning "Failed to rollback $FilePath after error: $_" } } return @{ FilePath = $FilePath Status = 'Error' Reason = $errorMsg DetectedEncoding = $detectedName BackupPath = $backupPath } } } function Convert-FolderEncoding { <# .SYNOPSIS Converts files in folders to a target encoding based on file extensions. .DESCRIPTION A user-friendly wrapper around Convert-FileEncoding that makes it easy to target specific file types by their extensions across one or more folders. This function is ideal for scenarios where you want to convert encoding for specific file types across directories without needing to know filter syntax. The function supports both single and multiple file extensions, with smart defaults for PowerShell compatibility. It provides comprehensive feedback and safety features including WhatIf support and backup creation. .PARAMETER Path The directory path to search for files. Can be a single directory or an array of directories. Use '.' for the current directory. .PARAMETER Extensions File extensions to target for conversion. Can be specified with or without the leading dot. Examples: 'ps1', '.ps1', @('ps1', 'psm1'), @('.cs', '.vb') Common presets available via -FileType parameter for convenience. .PARAMETER FileType Predefined file type groups for common scenarios: - PowerShell: .ps1, .psm1, .psd1, .ps1xml - CSharp: .cs, .csx - Web: .html, .css, .js, .json, .xml - Scripts: .ps1, .py, .rb, .sh, .bat, .cmd - Text: .txt, .md, .log, .config - All: Processes all common text file types .PARAMETER SourceEncoding Expected source encoding of files. Default is 'UTF8BOM'. .PARAMETER TargetEncoding Target encoding for conversion. Default is 'UTF8BOM' for PowerShell compatibility (prevents PS 5.1 ASCII misinterpretation). .PARAMETER ExcludeDirectories Directory names to exclude from processing (e.g., '.git', 'bin', 'obj', 'node_modules'). Default excludes common build and version control directories. .PARAMETER Recurse Process files in subdirectories recursively. Default is $true. .PARAMETER CreateBackups Create backup files before conversion for additional safety. Backups are created with .bak extension in the same directory. .PARAMETER Force Convert files even when their detected encoding doesn't match SourceEncoding. .PARAMETER NoRollbackOnMismatch Skip rolling back files when verification detects content changes during conversion. .PARAMETER MaxDepth Maximum directory depth to recurse when -Recurse is enabled. Default is unlimited. .PARAMETER PassThru Return conversion results for further processing. .EXAMPLE Convert-FolderEncoding -Path . -Extensions 'ps1' -WhatIf Preview what PowerShell files in the current directory would be converted. .EXAMPLE Convert-FolderEncoding -Path @('.\Scripts', '.\Modules') -FileType PowerShell -CreateBackups Convert all PowerShell files in Scripts and Modules directories to UTF8BOM with backups. .EXAMPLE Convert-FolderEncoding -Path . -Extensions @('cs', 'vb') -TargetEncoding UTF8 -Recurse Convert all C# and VB.NET files recursively to UTF8 (without BOM). .EXAMPLE Convert-FolderEncoding -Path .\Source -FileType Web -ExcludeDirectories @('node_modules', 'dist') -Force Convert web files, excluding build directories, forcing conversion regardless of detected encoding. .NOTES Author: PowerShell Encoding Tools This function provides a more user-friendly interface than Convert-FileEncoding for common scenarios. For complex filtering requirements, use Convert-FileEncoding directly. PowerShell Encoding Notes: - UTF8BOM is recommended for PowerShell files to ensure PS 5.1 compatibility - UTF8 without BOM can cause PS 5.1 to misinterpret files as ASCII - Always test with -WhatIf first and consider using -CreateBackups #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Extensions')] param( [Parameter(Mandatory)] [Alias('Directory', 'Folder')] [string[]] $Path, [Parameter(ParameterSetName = 'Extensions', Mandatory)] [string[]] $Extensions, [Parameter(ParameterSetName = 'FileType', Mandatory)] [ValidateSet('PowerShell', 'CSharp', 'Web', 'Scripts', 'Text', 'All')] [string] $FileType, [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')] [string] $SourceEncoding = 'UTF8BOM', [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')] [string] $TargetEncoding = 'UTF8BOM', [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode', 'dist', 'build'), [bool] $Recurse = $true, [switch] $CreateBackups, [switch] $Force, [switch] $NoRollbackOnMismatch, [int] $MaxDepth, [switch] $PassThru ) foreach ($singlePath in $Path) { if (-not (Test-Path -LiteralPath $singlePath -PathType Container)) { throw "Directory path '$singlePath' not found or is not a directory" } } $fileTypeMappings = @{ 'PowerShell' = @('.ps1', '.psm1', '.psd1', '.ps1xml') 'CSharp' = @('.cs', '.csx', '.csproj') 'Web' = @('.html', '.htm', '.css', '.js', '.json', '.xml', '.xsl', '.xslt') 'Scripts' = @('.ps1', '.py', '.rb', '.sh', '.bash', '.bat', '.cmd') 'Text' = @('.txt', '.md', '.log', '.config', '.ini', '.conf', '.yaml', '.yml') 'All' = @('.ps1', '.psm1', '.psd1', '.ps1xml', '.cs', '.csx', '.html', '.htm', '.css', '.js', '.json', '.xml', '.txt', '.md', '.py', '.rb', '.sh', '.bat', '.cmd', '.config', '.ini', '.yaml', '.yml') } if ($PSCmdlet.ParameterSetName -eq 'FileType') { $targetExtensions = $fileTypeMappings[$FileType] Write-Verbose "Using $FileType file type: $($targetExtensions -join ', ')" } else { $targetExtensions = $Extensions | ForEach-Object { if ($_.StartsWith('.')) { $_ } else { ".$_" } } Write-Verbose "Target extensions: $($targetExtensions -join ', ')" } $allFiles = @() $summary = @{ TotalDirectories = $Path.Count ProcessedDirectories = 0 TotalFiles = 0 ProcessedFiles = 0 ConvertedFiles = 0 SkippedFiles = 0 ErrorFiles = 0 StartTime = Get-Date } Write-Verbose "Scanning directories for files..." foreach ($singlePath in $Path) { Write-Verbose "Processing directory: $singlePath" $summary.ProcessedDirectories++ try { $gciParams = @{ LiteralPath = $singlePath File = $true } if ($Recurse) { $gciParams.Recurse = $true if ($MaxDepth) { $gciParams.Depth = $MaxDepth } } $directoryFiles = Get-ChildItem @gciParams | Where-Object { $extension = $_.Extension.ToLower() $extensionMatch = $targetExtensions -contains $extension $directoryExcluded = $false if ($ExcludeDirectories -and $_.DirectoryName) { $relativePath = $_.DirectoryName.Replace($singlePath, '').TrimStart('\', '/') $directoryExcluded = $ExcludeDirectories | Where-Object { $relativePath -like "*$_*" } } return $extensionMatch -and -not $directoryExcluded } $allFiles += $directoryFiles $summary.TotalFiles += $directoryFiles.Count Write-Verbose "Found $($directoryFiles.Count) matching files in $singlePath" } catch { Write-Error "Error processing directory '$singlePath': $($_.Exception.Message)" continue } } if ($allFiles.Count -eq 0) { Write-Warning "No files found matching the specified criteria." Write-Verbose "Extensions searched: $($targetExtensions -join ', ')" Write-Verbose "Paths searched: $($Path -join ', ')" return } Write-Verbose "Found $($allFiles.Count) files across $($summary.ProcessedDirectories) directories" Write-Verbose "Target extensions: $($targetExtensions -join ', ')" Write-Verbose "Converting: $SourceEncoding → $TargetEncoding" if ($PSCmdlet.ShouldProcess("$($allFiles.Count) files", "Convert encoding from $SourceEncoding to $TargetEncoding")) { $results = @() $progressCounter = 0 foreach ($file in $allFiles) { $progressCounter++ $percentComplete = [math]::Round(($progressCounter / $allFiles.Count) * 100, 1) Write-Progress -Activity "Converting file encodings" -Status "Processing $($file.Name) ($progressCounter of $($allFiles.Count))" -PercentComplete $percentComplete try { if ($CreateBackups) { $backupPath = "$($file.FullName).bak" Copy-Item -LiteralPath $file.FullName -Destination $backupPath -Force Write-Verbose "Created backup: $backupPath" } $convertParams = @{ Path = $file.FullName SourceEncoding = $SourceEncoding TargetEncoding = $TargetEncoding Force = $Force NoRollbackOnMismatch = $NoRollbackOnMismatch WhatIf = $WhatIfPreference } Convert-FileEncoding @convertParams $summary.ProcessedFiles++ $summary.ConvertedFiles++ if ($PassThru) { $results += [PSCustomObject]@{ FilePath = $file.FullName Extension = $file.Extension Status = 'Converted' BackupCreated = $CreateBackups } } } catch { $summary.ErrorFiles++ Write-Error "Error converting '$($file.FullName)': $($_.Exception.Message)" if ($PassThru) { $results += [PSCustomObject]@{ FilePath = $file.FullName Extension = $file.Extension Status = 'Error' Error = $_.Exception.Message } } } } Write-Progress -Activity "Converting file encodings" -Completed } $summary.EndTime = Get-Date $summary.Duration = $summary.EndTime - $summary.StartTime Write-Verbose "Conversion Summary:" Write-Verbose " Directories processed: $($summary.ProcessedDirectories)" Write-Verbose " Files found: $($summary.TotalFiles)" Write-Verbose " Files processed: $($summary.ProcessedFiles)" Write-Verbose " Files converted: $($summary.ConvertedFiles)" Write-Verbose " Files with errors: $($summary.ErrorFiles)" Write-Verbose " Duration: $($summary.Duration.TotalSeconds.ToString('F2')) seconds" if ($CreateBackups -and $summary.ConvertedFiles -gt 0) { Write-Verbose " Backups created with .bak extension" } if ($PassThru) { return $results } } function Convert-HashTableToNicelyFormattedString { [CmdletBinding()] param( [System.Collections.IDictionary] $hashTable ) [string] $nicelyFormattedString = $hashTable.Keys | ForEach-Object ` { $key = $_ $value = $hashTable.$key " $key = $value$NewLine" } return $nicelyFormattedString } function Convert-LineEndingSingle { param( [string] $FilePath, [string] $TargetLineEnding, [hashtable] $CurrentInfo, [bool] $CreateBackup, [bool] $EnsureFinalNewline ) try { $content = [System.IO.File]::ReadAllText($FilePath) if ([string]::IsNullOrEmpty($content)) { return @{ Status = 'Skipped' Reason = 'Empty file' } } $backupPath = $null if ($CreateBackup) { $backupPath = "$FilePath.backup" $counter = 1 while (Test-Path $backupPath) { $backupPath = "$FilePath.backup$counter" $counter++ } $originalBytes = [System.IO.File]::ReadAllBytes($FilePath) [System.IO.File]::WriteAllBytes($backupPath, $originalBytes) } $normalizedContent = $content -replace "`r`n", "`n" -replace "`r", "`n" $convertedContent = if ($TargetLineEnding -eq 'CRLF') { $normalizedContent -replace "`n", "`r`n" } else { $normalizedContent } if ($EnsureFinalNewline -and -not [string]::IsNullOrEmpty($convertedContent)) { $targetNewline = if ($TargetLineEnding -eq 'CRLF') { "`r`n" } else { "`n" } if (-not $convertedContent.EndsWith($targetNewline)) { $convertedContent += $targetNewline } } $encoding = Get-FileEncoding -Path $FilePath -AsObject [System.IO.File]::WriteAllText($FilePath, $convertedContent, $encoding.Encoding) $changesMade = @() if ($CurrentInfo.LineEnding -ne $TargetLineEnding -and $CurrentInfo.LineEnding -ne 'None') { $changesMade += "line endings ($($CurrentInfo.LineEnding) → $TargetLineEnding)" } if ($EnsureFinalNewline -and -not $CurrentInfo.HasFinalNewline) { $changesMade += "added final newline" } return @{ Status = 'Converted' Reason = "Converted: $($changesMade -join ', ')" BackupPath = $backupPath } } catch { return @{ Status = 'Error' Reason = "Failed to convert: $_" BackupPath = $backupPath } } } function Convert-RequiredModules { <# .SYNOPSIS Converts the RequiredModules section of the manifest to the correct format .DESCRIPTION Converts the RequiredModules section of the manifest to the correct format Fixes the ModuleVersion and Guid if set to 'Latest' or 'Auto' .PARAMETER Configuration The configuration object of the module .EXAMPLE Convert-RequiredModules -Configuration $Configuration .NOTES General notes #> [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration ) $Manifest = $Configuration.Information.Manifest $Failures = $false if ($Manifest.Contains('RequiredModules')) { foreach ($SubModule in $Manifest.RequiredModules) { if ($SubModule -is [string]) { } else { [Array] $AvailableModule = Get-Module -ListAvailable $SubModule.ModuleName -Verbose:$false if ($SubModule.ModuleVersion -in 'Latest', 'Auto') { if ($AvailableModule) { $SubModule.ModuleVersion = $AvailableModule[0].Version.ToString() } else { Write-Text -Text "[-] Module $($SubModule.ModuleName) is not available (Version), but defined as required with last version. Terminating." -Color Red $Failures = $true } } if ($SubModule.Guid -in 'Latest', 'Auto') { if ($AvailableModule) { $SubModule.Guid = $AvailableModule[0].Guid.ToString() } else { Write-Text -Text "[-] Module $($SubModule.ModuleName) is not available (GUID), but defined as required with last version. Terminating." -Color Red $Failures = $true } } } } } if ($Failures -eq $true) { $false } } function Copy-ArtefactMainModule { [CmdletBinding()] param( [switch] $Enabled, [nullable[bool]] $IncludeTagName, [string] $ModuleName, [string] $Destination ) if (-not $Enabled) { return } if ($IncludeTagName) { $NameOfDestination = [io.path]::Combine($Destination, $ModuleName, $TagName) } else { $NameOfDestination = [io.path]::Combine($Destination, $ModuleName) } $ResolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($NameOfDestination) Write-TextWithTime -PreAppend Addition -Text "Copying main module to $ResolvedDestination" -Color Yellow { if (Test-Path -Path $NameOfDestination) { Remove-ItemAlternative -LiteralPath $NameOfDestination -ErrorAction Stop } $null = New-Item -ItemType Directory -Path $Destination -Force if ($DestinationPaths.Desktop) { Copy-Item -LiteralPath $DestinationPaths.Desktop -Recurse -Destination $ResolvedDestination -Force } elseif ($DestinationPaths.Core) { Copy-Item -LiteralPath $DestinationPaths.Core -Recurse -Destination $ResolvedDestination -Force } } -SpacesBefore ' ' } function Copy-ArtefactRequiredFiles { [CmdletBinding()] param( [System.Collections.IDictionary] $FilesInput, [string] $ProjectPath, [string] $Destination, [nullable[bool]] $DestinationRelative ) foreach ($File in $FilesInput.Keys) { if ($FilesInput[$File] -is [string]) { $FullFilePath = [System.IO.Path]::Combine($ProjectPath, $File) if (Test-Path -Path $FullFilePath) { if ($DestinationRelative) { $DestinationPath = [System.IO.Path]::Combine($Destination, $FilesInput[$File]) } else { $DestinationPath = $FilesInput[$File] } $ResolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath) Write-TextWithTime -Text "Copying file $FullFilePath to $ResolvedDestination" { $DirectoryPath = [Io.Path]::GetDirectoryName($ResolvedDestination) $null = New-Item -ItemType Directory -Force -ErrorAction Stop -Path $DirectoryPath Copy-Item -LiteralPath $FullFilePath -Destination $ResolvedDestination -Force -ErrorAction Stop } -PreAppend Addition -SpacesBefore ' ' -Color Yellow } else { Write-TextWithTime -Text "File $FullFilePath does not exist" -PreAppend Plus -SpacesBefore ' ' -Color Red -ColorTime Red -ColorBefore Red return $false } } elseif ($FilesInput[$File] -is [System.Collections.IDictionary]) { if ($FilesInput[$File].Enabled -eq $true) { if ($FilesInput[$File].Source) { $FullFilePath = [System.IO.Path]::Combine($ProjectPath, $FilesInput[$File].Source) if (Test-Path -Path $FullFilePath) { if ($FilesInput[$File].DestinationRelative) { $DestinationPath = [System.IO.Path]::Combine($Destination, $FilesInput[$File].Destination) } else { $DestinationPath = $FilesInput[$File].Destination } $ResolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath) Write-TextWithTime -Text "Copying file $FullFilePath to $ResolvedDestination" { $DirectoryPath = [Io.Path]::GetDirectoryName($ResolvedDestination) $null = New-Item -ItemType Directory -Force -ErrorAction Stop -Path $DirectoryPath Copy-Item -LiteralPath $FullFilePath -Destination $ResolvedDestination -Force -ErrorAction Stop } -PreAppend Addition -SpacesBefore ' ' -Color Yellow } else { Write-TextWithTime -Text "File $FullFilePath does not exist" -PreAppend Plus -SpacesBefore ' ' -Color Red -ColorTime Red -ColorBefore Red return $false } } } } } } function Copy-ArtefactRequiredFolders { [CmdletBinding()] param( [System.Collections.IDictionary] $FoldersInput, [string] $ProjectPath, [string] $Destination, [nullable[bool]] $DestinationRelative ) } function Copy-ArtefactRequiredModule { [CmdletBinding()] param( [switch] $Enabled, [Array] $RequiredModules, [string] $Destination ) if (-not $Enabled) { return } if (-not (Test-Path -LiteralPath $Destination)) { New-Item -ItemType Directory -Path $Destination -Force } foreach ($Module in $RequiredModules) { if ($Module.ModuleName) { Write-TextWithTime -PreAppend Addition -Text "Copying required module $($Module.ModuleName)" -Color Yellow { $ModulesFound = Get-Module -ListAvailable -Name $Module.ModuleName if ($ModulesFound.Count -gt 0) { $PathToPSD1 = if ($Module.ModuleVersion -eq 'Latest') { $ModulesFound[0].Path } else { $FoundModule = foreach ($M in $ModulesFound) { if ($M.Version -eq $Module.ModuleVersion) { $M.Path break } } if (-not $FoundModule) { $ModulesFound[0].Path } else { $FoundModule } } $FolderToCopy = [System.IO.Path]::GetDirectoryName($PathToPSD1) $ItemInformation = Get-Item -LiteralPath $FolderToCopy if ($ItemInformation.Name -ne $Module.ModuleName) { $NewPath = [io.path]::Combine($Destination, $Module.ModuleName) if (Test-Path -LiteralPath $NewPath) { Remove-Item -LiteralPath $NewPath -Recurse -Force -ErrorAction Stop } Copy-Item -LiteralPath $FolderToCopy -Destination $NewPath -Recurse -Force -ErrorAction Stop } else { Copy-Item -LiteralPath $FolderToCopy -Destination $Destination -Recurse -Force } } } -SpacesBefore ' ' } } } function Copy-ArtefactToScript { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [string] $ModuleVersion, [switch] $Enabled, [nullable[bool]] $IncludeTagName, [string] $ModuleName, [string] $Destination, [string] $PreScriptMerge, [string] $PostScriptMerge, [string] $ScriptName ) if (-not $Enabled) { return } if ($PSVersionTable.PSVersion.Major -gt 5) { $Encoding = 'UTF8BOM' } else { $Encoding = 'UTF8' } if ($IncludeTagName) { $NameOfDestination = [io.path]::Combine($Destination, $TagName) } else { $NameOfDestination = [io.path]::Combine($Destination) } $ResolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($NameOfDestination) Write-TextWithTime -PreAppend Addition -Text "Copying main module to $ResolvedDestination" -Color Yellow { if (Test-Path -Path $NameOfDestination) { Remove-ItemAlternative -LiteralPath $NameOfDestination -ErrorAction Stop } $null = New-Item -ItemType Directory -Path $Destination -Force if ($DestinationPaths.Desktop) { $DestinationToUse = [System.IO.Path]::Combine($DestinationPaths.Desktop, "*") Copy-Item -Path $DestinationToUse -Recurse -Destination "$ResolvedDestination" -Force } elseif ($DestinationPaths.Core) { $DestinationToUse = [System.IO.Path]::Combine($DestinationPaths.Core, "*") Copy-Item -Path $DestinationToUse -Recurse -Destination "$ResolvedDestination" -Force } } -SpacesBefore ' ' Write-TextWithTime -PreAppend Addition -Text "Cleaning up main module" -Color Yellow { $PSD1 = [io.path]::Combine($ResolvedDestination, "$ModuleName.psd1") Remove-Item -LiteralPath $PSD1 -Force -ErrorAction Stop $PSM1 = [io.path]::Combine($ResolvedDestination, "$ModuleName.psm1") if ($ScriptName) { $TagName = "v$($ModuleVersion)" if ($Configuration.CurrentSettings.PreRelease) { $ModuleVersionWithPreRelease = "$($ModuleVersion)-$($Configuration.CurrentSettings.PreRelease)" $TagModuleVersionWithPreRelease = "v$($ModuleVersionWithPreRelease)" } else { $ModuleVersionWithPreRelease = $ModuleVersion $TagModuleVersionWithPreRelease = "v$($ModuleVersion)" } $ScriptName = $ScriptName.Replace('{ModuleName}', $ModuleName) $ScriptName = $ScriptName.Replace('<ModuleName>', $ModuleName) $ScriptName = $ScriptName.Replace('{ModuleVersion}', $ModuleVersion) $ScriptName = $ScriptName.Replace('<ModuleVersion>', $ModuleVersion) $ScriptName = $ScriptName.Replace('{ModuleVersionWithPreRelease}', $ModuleVersionWithPreRelease) $ScriptName = $ScriptName.Replace('<ModuleVersionWithPreRelease>', $ModuleVersionWithPreRelease) $ScriptName = $ScriptName.Replace('{TagModuleVersionWithPreRelease}', $TagModuleVersionWithPreRelease) $ScriptName = $ScriptName.Replace('<TagModuleVersionWithPreRelease>', $TagModuleVersionWithPreRelease) $ScriptName = $ScriptName.Replace('{TagName}', $TagName) $ScriptName = $ScriptName.Replace('<TagName>', $TagName) if ($ScriptName.EndsWith(".ps1")) { $PS1 = [io.path]::Combine($ResolvedDestination, "$ScriptName") Rename-Item -LiteralPath $PSM1 -NewName "$ScriptName" -Force -ErrorAction Stop } else { $PS1 = [io.path]::Combine($ResolvedDestination, "$ScriptName.ps1") Rename-Item -LiteralPath $PSM1 -NewName "$ScriptName.ps1" -Force -ErrorAction Stop } } else { $PS1 = [io.path]::Combine($ResolvedDestination, "$ModuleName.ps1") Rename-Item -LiteralPath $PSM1 -NewName "$ModuleName.ps1" -Force -ErrorAction Stop } $Content = Get-Content -LiteralPath $PS1 -ErrorAction Stop -Encoding UTF8 if ($Content -contains "# Export functions and aliases as required") { $Index = ($Content | Select-String -Pattern "# Export functions and aliases as required" -SimpleMatch | Select-Object -Last 1).LineNumber $Content = $Content | Select-Object -First ($Index - 1) } else { $index = ($Content | Select-String -Pattern "Export-ModuleMember " -SimpleMatch | Select-Object -Last 1).LineNumber $Content = $Content | Select-Object -First ($Index - 1) } if ($PreScriptMerge) { $Content = @( $PreScriptMerge.Trim() $Content ) } if ($PostScriptMerge) { $Content = @( $Content $PostScriptMerge.Trim() ) } Set-Content -LiteralPath $PS1 -Value $Content -Force -ErrorAction Stop -Encoding $Encoding } -SpacesBefore ' ' } function Copy-DictionaryManual { [CmdletBinding()] param( [System.Collections.IDictionary] $Dictionary ) $clone = @{} foreach ($Key in $Dictionary.Keys) { $value = $Dictionary.$Key $clonedValue = switch ($Dictionary.$Key) { { $null -eq $_ } { $null continue } { $_ -is [System.Collections.IDictionary] } { Copy-DictionaryManual -Dictionary $_ continue } { $type = $_.GetType() $type.IsPrimitive -or $type.IsValueType -or $_ -is [string] } { $_ continue } default { $_ | Select-Object -Property * } } if ($value -is [System.Collections.IList]) { $clone[$Key] = @($clonedValue) } else { $clone[$Key] = $clonedValue } } $clone } function Copy-InternalDictionary { [cmdletbinding()] param( [System.Collections.IDictionary] $Dictionary ) $ms = [System.IO.MemoryStream]::new() $bf = [System.Runtime.Serialization.Formatters.Binary.BinaryFormatter]::new() $bf.Serialize($ms, $Dictionary) $ms.Position = 0 $clone = $bf.Deserialize($ms) $ms.Close() $clone } function Copy-InternalFiles { [CmdletBinding()] param( [string[]] $LinkFiles, [string] $FullModulePath, [string] $FullProjectPath, [switch] $Delete ) foreach ($File in $LinkFiles) { [string] $Path = [System.IO.Path]::Combine($FullModulePath, $File) [string] $Path2 = [System.IO.Path]::Combine($FullProjectPath, $File) if ($Delete) { if (Test-ReparsePoint -path $Path) { Remove-Item $Path -Confirm:$false } } Copy-Item -Path $Path2 -Destination $Path -Force -Recurse -Confirm:$false } } function Export-PSData { <# .Synopsis Exports property bags into a data file .Description Exports property bags and the first level of any other object into a ps data file (.psd1) .Link https://github.com/StartAutomating/Pipeworks Import-PSData .Example Get-Web -Url http://www.youtube.com/watch?v=xPRC3EDR_GU -AsMicrodata -ItemType http://schema.org/VideoObject | Export-PSData .\PipeworksQuickstart.video.psd1 #> [OutputType([IO.FileInfo])] [cmdletbinding()] param( # The data that will be exported [Parameter(Mandatory = $true, ValueFromPipeline = $true)][PSObject[]]$InputObject, # The path to the data file [Parameter(Mandatory = $true, Position = 0)][string] $DataFile, [switch] $Sort ) begin { $AllObjects = [System.Collections.Generic.List[object]]::new() } process { $AllObjects.AddRange($InputObject) } end { if ($PSVersionTable.PSVersion.Major -gt 5) { $Encoding = 'UTF8BOM' } else { $Encoding = 'UTF8' } $Text = $AllObjects | Write-PowerShellHashtable -Sort:$Sort.IsPresent $Text | Out-File -FilePath $DataFile -Encoding $Encoding } } function Find-NetFramework { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER RequireVersion Parameter description .EXAMPLE Find-NetFramework -RequireVersion 4.8 .NOTES General notes #> [cmdletBinding()] param( [string] $RequireVersion ) if ($RequireVersion) { $Framework = [ordered] @{ '4.5' = 378389 '4.5.1' = 378675 '4.5.2' = 379893 '4.6' = 393295 '4.6.1' = 394254 '4.6.2' = 394802 '4.7' = 460798 '4.7.1' = 461308 '4.7.2' = 461808 '4.8' = 528040 } $DetectVersion = $Framework[$RequireVersion] "if (`$PSVersionTable.PSEdition -eq 'Desktop' -and (Get-ItemProperty `"HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full`").Release -lt $DetectVersion) { Write-Warning `"This module requires .NET Framework $RequireVersion or later.`"; return } " } } function Find-RequiredModules { [cmdletbinding()] param( [string] $Name ) $Module = Get-Module -ListAvailable $Name -ErrorAction SilentlyContinue -Verbose:$false $AllModules = if ($Module) { [Array] $RequiredModules = $Module.RequiredModules.Name if ($null -ne $RequiredModules) { $null } $RequiredModules foreach ($_ in $RequiredModules) { Find-RequiredModules -Name $_ } } [Array] $ListModules = $AllModules | Where-Object { $null -ne $_ } if ($null -ne $ListModules) { [array]::Reverse($ListModules) } $CleanedModules = [System.Collections.Generic.List[string]]::new() foreach ($_ in $ListModules) { if ($CleanedModules -notcontains $_) { $CleanedModules.Add($_) } } $CleanedModules } function Format-Code { [cmdletbinding()] param( [string] $FilePath, [System.Collections.IDictionary] $FormatCode ) if ($PSVersionTable.PSVersion.Major -gt 5) { $Encoding = 'UTF8BOM' } else { $Encoding = 'UTF8' } if ($FormatCode.Enabled) { if ($FormatCode.RemoveComments -or $FormatCode.RemoveCommentsInParamBlock -or $FormatCode.RemoveCommentsBeforeParamBlock) { $Output = Write-TextWithTime -Text "[+] Removing Comments - $FilePath" { $removeCommentsSplat = @{ SourceFilePath = $FilePath RemoveCommentsInParamBlock = $FormatCode.RemoveCommentsInParamBlock RemoveCommentsBeforeParamBlock = $FormatCode.RemoveCommentsBeforeParamBlock RemoveAllEmptyLines = $FormatCode.RemoveAllEmptyLines RemoveEmptyLines = $FormatCode.RemoveEmptyLines } Remove-Comments @removeCommentsSplat } } elseif ($FormatCode.RemoveAllEmptyLines -or $FormatCode.RemoveEmptyLines) { $Output = Write-TextWithTime -Text "[+] Removing Empty Lines - $FilePath" { $removeEmptyLinesSplat = @{ SourceFilePath = $FilePath RemoveAllEmptyLines = $FormatCode.RemoveAllEmptyLines RemoveEmptyLines = $FormatCode.RemoveEmptyLines } Remove-EmptyLines @removeEmptyLinesSplat } } else { $Output = Write-TextWithTime -Text "Reading file content - $FilePath" { Get-Content -LiteralPath $FilePath -Raw -Encoding UTF8 -ErrorAction Stop } -PreAppend Plus -SpacesBefore ' ' } if ($Output -eq $false) { return $false } if ($null -eq $FormatCode.FormatterSettings) { $FormatCode.FormatterSettings = $Script:FormatterSettings } $Data = Write-TextWithTime -Text "Formatting file - $FilePath" { try { Invoke-Formatter -ScriptDefinition $Output -Settings $FormatCode.FormatterSettings -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message Write-Host Write-Text " [-] Format-Code - Formatting on file $FilePath failed." -Color Red Write-Text " [-] Format-Code - Error: $ErrorMessage" -Color Red Write-Text " [-] Format-Code - This is most likely related to a bug in PSScriptAnalyzer running inside VSCode. Please try running outside of VSCode when using formatting." -Color Red return $false } } -PreAppend Plus -SpacesBefore ' ' if ($Data -eq $false) { return $false } Write-TextWithTime -Text "Saving file - $FilePath" { $Final = foreach ($O in $Data) { if ($O.Trim() -ne '') { $O.Trim() } } try { $Final | Out-File -LiteralPath $FilePath -NoNewline -Encoding $Encoding -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message Write-Host Write-Text "[-] Format-Code - Resaving file $FilePath failed. Error: $ErrorMessage" -Color Red return $false } } -PreAppend Plus -SpacesBefore ' ' } } function Format-UsingNamespace { [CmdletBinding()] param( [string] $FilePath, [string] $FilePathSave, [string] $FilePathUsing ) if ($PSVersionTable.PSVersion.Major -gt 5) { $Encoding = 'UTF8BOM' } else { $Encoding = 'UTF8' } if ($FilePathSave -eq '') { $FilePathSave = $FilePath } if ($FilePath -ne '' -and (Test-Path -Path $FilePath) -and (Get-Item -LiteralPath $FilePath).Length -gt 0kb) { $FileStream = New-Object -TypeName IO.FileStream -ArgumentList ($FilePath), ([System.IO.FileMode]::Open), ([System.IO.FileAccess]::Read), ([System.IO.FileShare]::ReadWrite); $ReadFile = New-Object -TypeName System.IO.StreamReader -ArgumentList ($FileStream, [System.Text.Encoding]::UTF8, $true); $UsingNamespaces = [System.Collections.Generic.List[string]]::new() $Content = while (!$ReadFile.EndOfStream) { $Line = $ReadFile.ReadLine() if ($Line -like 'using namespace*') { $UsingNamespaces.Add($Line) } else { $Line } } $ReadFile.Close() $null = New-Item -Path $FilePathSave -ItemType file -Force if ($UsingNamespaces) { $null = New-Item -Path $FilePathUsing -ItemType file -Force $UsingNamespaces = $UsingNamespaces.Trim() | Sort-Object -Unique $UsingNamespaces | Out-File -Append -LiteralPath $FilePathUsing -Encoding $Encoding $Content | Out-File -Append -LiteralPath $FilePathSave -Encoding $Encoding return $true } else { $Content | Out-File -Append -LiteralPath $FilePathSave -Encoding $Encoding return $False } } } function Get-AstTokens { [cmdletBinding()] param( [System.Management.Automation.Language.Token[]] $ASTTokens, [System.Collections.Generic.List[Object]] $Commands, [System.Management.Automation.Language.Ast] $FileAst ) $ListOfFuncionsAst = $FileAst.FindAll( { param([System.Management.Automation.Language.Ast] $ast) end { if ($ast -isnot [System.Management.Automation.Language.CommandAst]) { return $false } for ($node = $ast.Parent; $null -ne $node; $node = $node.Parent) { if ($node -isnot [System.Management.Automation.Language.CommandAst]) { continue } if ($node.GetCommandName() -in 'Get-ADComputer', 'Get-ADUser', 'Get-ADObject', 'Get-ADDomainController', 'Get-ADReplicationSubnet') { return $false } } return $true } }, $true ) $List = foreach ($Function in $ListOfFuncionsAst) { $Line = $Function.CommandElements[0] if ($Line.Value) { $Line.Value } } $List } function Get-CurrentLineEnding { param([string] $FilePath) try { $bytes = [System.IO.File]::ReadAllBytes($FilePath) if ($bytes.Length -eq 0) { return @{ LineEnding = 'None' HasFinalNewline = $true } } $crlfCount = 0 $lfOnlyCount = 0 $crOnlyCount = 0 $hasFinalNewline = $false $lastByte = $bytes[$bytes.Length - 1] if ($lastByte -eq 10) { $hasFinalNewline = $true } elseif ($lastByte -eq 13) { $hasFinalNewline = $true } for ($i = 0; $i -lt $bytes.Length - 1; $i++) { if ($bytes[$i] -eq 13 -and $bytes[$i + 1] -eq 10) { $crlfCount++ $i++ } elseif ($bytes[$i] -eq 10) { $lfOnlyCount++ } elseif ($bytes[$i] -eq 13) { if ($i + 1 -lt $bytes.Length -and $bytes[$i + 1] -ne 10) { $crOnlyCount++ } } } if ($bytes.Length -gt 0) { $lastByte = $bytes[$bytes.Length - 1] if ($lastByte -eq 10 -and ($bytes.Length -eq 1 -or $bytes[$bytes.Length - 2] -ne 13)) { $lfOnlyCount++ } elseif ($lastByte -eq 13) { $crOnlyCount++ } } $typesFound = @() if ($crlfCount -gt 0) { $typesFound += 'CRLF' } if ($lfOnlyCount -gt 0) { $typesFound += 'LF' } if ($crOnlyCount -gt 0) { $typesFound += 'CR' } $lineEndingType = if ($typesFound.Count -eq 0) { 'None' } elseif ($typesFound.Count -eq 1) { $typesFound[0] } else { 'Mixed' } return @{ LineEnding = $lineEndingType HasFinalNewline = $hasFinalNewline } } catch { return @{ LineEnding = 'Error' HasFinalNewline = $false } } } function Get-CurrentVersionFromBuildScript { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$ScriptFile ) if (!(Test-Path -Path $ScriptFile)) { Write-Warning "Build script file not found: $ScriptFile" return $null } try { $content = Get-Content -Path $ScriptFile -Raw if ($content -match 'ModuleVersion\s*=\s*[''"\"]?([\d\.]+)[''"\"]?') { return $matches[1] } return $null } catch { Write-Warning "Error reading build script $ScriptFile`: $_" return $null } } function Get-CurrentVersionFromCsProj { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$ProjectFile ) if (!(Test-Path -Path $ProjectFile)) { Write-Warning "Project file not found: $ProjectFile" return $null } try { $content = Get-Content -Path $ProjectFile -Raw if ($content -match '<VersionPrefix>([\d\.]+)<\/VersionPrefix>') { return $matches[1] } return $null } catch { Write-Warning "Error reading project file $ProjectFile`: $_" return $null } } function Get-CurrentVersionFromPsd1 { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$ManifestFile ) if (!(Test-Path -Path $ManifestFile)) { Write-Warning "Module manifest file not found: $ManifestFile" return $null } try { $manifest = Import-PowerShellDataFile -Path $ManifestFile return $manifest.ModuleVersion } catch { Write-Warning "Error reading module manifest $ManifestFile`: $_" return $null } } function Get-FileEncoding { <# .SYNOPSIS Get the encoding of a file (ASCII, UTF8, UTF8BOM, Unicode, BigEndianUnicode, UTF7, UTF32). .DESCRIPTION Detects the encoding of a file using its byte order mark or by scanning for non‑ASCII characters. Encoding is determined by the first few bytes of the file (BOM) or by the presence of non-ASCII characters. Returns a string with the encoding name or a custom object when -AsObject is used. .PARAMETER Path Path to the file to check. Supports pipeline input and can accept FullName property from Get-ChildItem. .PARAMETER AsObject Returns a custom object with Path, Encoding, and EncodingName properties instead of just the encoding name string. .EXAMPLE Get-FileEncoding -Path 'C:\temp\test.txt' Returns the encoding name as a string (e.g., 'UTF8BOM', 'ASCII', 'Unicode') .EXAMPLE Get-FileEncoding -Path 'C:\temp\test.txt' -AsObject Returns a custom object with detailed encoding information .EXAMPLE Get-ChildItem -Path 'C:\temp\*.txt' | Get-FileEncoding Gets encoding for all text files in the directory via pipeline .NOTES Supported encodings: ASCII, UTF8, UTF8BOM, Unicode (UTF-16LE), BigEndianUnicode (UTF-16BE), UTF7, UTF32 #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)] [Alias('FullName')] [string] $Path, [switch] $AsObject ) process { if (-not (Test-Path -LiteralPath $Path)) { $msg = "Get-FileEncoding - File not found: $Path" if ($ErrorActionPreference -eq 'Stop') { throw $msg } Write-Warning $msg return } $fs = [System.IO.FileStream]::new($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) try { $bom = [byte[]]::new(4) $null = $fs.Read($bom, 0, 4) $enc = [System.Text.Encoding]::ASCII if ($bom[0] -eq 0x00 -and $bom[1] -eq 0x00 -and $bom[2] -eq 0xfe -and $bom[3] -eq 0xff) { $enc = [System.Text.Encoding]::UTF32 } elseif ($bom[0] -eq 0xef -and $bom[1] -eq 0xbb -and $bom[2] -eq 0xbf) { $enc = [System.Text.UTF8Encoding]::new($true) } elseif ($bom[0] -eq 0xff -and $bom[1] -eq 0xfe) { $enc = [System.Text.Encoding]::Unicode } elseif ($bom[0] -eq 0xfe -and $bom[1] -eq 0xff) { $enc = [System.Text.Encoding]::BigEndianUnicode } elseif ($bom[0] -eq 0x2b -and $bom[1] -eq 0x2f -and $bom[2] -eq 0x76) { $enc = [System.Text.Encoding]::UTF7 } else { $fs.Position = 0 $byte = [byte[]]::new(1) while ($fs.Read($byte, 0, 1) -gt 0) { if ($byte[0] -gt 0x7F) { $enc = [System.Text.UTF8Encoding]::new($false) break } } } } finally { $fs.Close() $fs.Dispose() } $encName = if ($enc -is [System.Text.UTF8Encoding] -and $enc.GetPreamble().Length -eq 3) { 'UTF8BOM' } elseif ($enc -is [System.Text.UTF8Encoding]) { 'UTF8' } elseif ($enc -is [System.Text.UnicodeEncoding]) { 'Unicode' } elseif ($enc -is [System.Text.UTF7Encoding]) { 'UTF7' } elseif ($enc -is [System.Text.UTF32Encoding]) { 'UTF32' } elseif ($enc -is [System.Text.ASCIIEncoding]) { 'ASCII' } elseif ($enc -is [System.Text.BigEndianUnicodeEncoding]) { 'BigEndianUnicode' } else { $enc.WebName } if ($AsObject) { [PSCustomObject]@{ Path = $Path Encoding = $enc EncodingName = $encName } } else { $encName } } } function Get-FilteredScriptCommands { [CmdletBinding()] param( [Array] $Commands, [switch] $NotCmdlet, [switch] $NotUnknown, [switch] $NotApplication, [string[]] $Functions, [string] $FilePath, [string[]] $ApprovedModules ) if ($Functions.Count -eq 0) { $Functions = Get-FunctionNames -Path $FilePath } $Commands = $Commands | Where-Object { $_ -notin $Functions } $Commands = $Commands | Sort-Object -Unique $Scan = foreach ($Command in $Commands) { try { $IsAlias = $false $Data = Get-Command -Name $Command -ErrorAction Stop if ($Data.Source -eq 'PSPublishModule') { if ($Data.Source -notin $ApprovedModules) { throw } } if ($Data.CommandType -eq 'Alias') { $Data = Get-Command -Name $Data.Definition $IsAlias = $true } [PSCustomObject] @{ Name = $Data.Name Source = $Data.Source CommandType = $Data.CommandType IsAlias = $IsAlias IsPrivate = $false Error = '' ScriptBlock = $Data.ScriptBlock } } catch { $CurrentOutput = [PSCustomObject] @{ Name = $Command Source = '' CommandType = '' IsAlias = $IsAlias IsPrivate = $false Error = $_.Exception.Message ScriptBlock = '' } foreach ($ApprovedModule in $ApprovedModules) { try { $ImportModuleWithPrivateCommands = Import-Module -PassThru -Name $ApprovedModule -ErrorAction Stop -Verbose:$false $Data = & $ImportModuleWithPrivateCommands { param($command); Get-Command $command -Verbose:$false -ErrorAction Stop } $command $CurrentOutput = [PSCustomObject] @{ Name = $Data.Name Source = $Data.Source CommandType = $Data.CommandType IsAlias = $IsAlias IsPrivate = $true Error = '' ScriptBlock = $Data.ScriptBlock } break } catch { $CurrentOutput = [PSCustomObject] @{ Name = $Command Source = '' CommandType = '' IsAlias = $IsAlias IsPrivate = $false Error = $_.Exception.Message ScriptBlock = '' } } } $CurrentOutput } } $Filtered = foreach ($Command in $Scan) { if ($Command.Source -eq 'Microsoft.PowerShell.Core') { continue } if ($NotCmdlet -and $NotUnknown -and $NotApplication) { if ($Command.CommandType -ne 'Cmdlet' -and $Command.Source -ne '' -and $Command.CommandType -ne 'Application') { $Command } } elseif ($NotCmdlet -and $NotUnknown) { if ($Command.CommandType -ne 'Cmdlet' -and $Command.Source -ne '') { $Command } } elseif ($NotCmdlet) { if ($Command.CommandType -ne 'Cmdlet') { $Command } } elseif ($NotUnknown) { if ($Command.Source -ne '') { $Command } } elseif ($NotApplication) { if ($Command.CommandType -ne 'Application') { $Command } } else { $Command } } $Filtered } function Get-FolderEncoding { <# .SYNOPSIS Analyzes file encodings in folders based on file extensions. .DESCRIPTION A user-friendly wrapper that analyzes encoding distribution across files in one or more folders, filtered by file extensions. This function is ideal for understanding the current encoding state of specific file types before performing conversions. The function supports both single and multiple file extensions, with predefined file type groups for common scenarios. It provides comprehensive analysis including encoding distribution, inconsistencies, and recommendations. .PARAMETER Path The directory path to analyze. Can be a single directory or an array of directories. Use '.' for the current directory. .PARAMETER Extensions File extensions to analyze. Can be specified with or without the leading dot. Examples: 'ps1', '.ps1', @('ps1', 'psm1'), @('.cs', '.vb') Common presets available via -FileType parameter for convenience. .PARAMETER FileType Predefined file type groups for common scenarios: - PowerShell: .ps1, .psm1, .psd1, .ps1xml - CSharp: .cs, .csx - Web: .html, .css, .js, .json, .xml - Scripts: .ps1, .py, .rb, .sh, .bat, .cmd - Text: .txt, .md, .log, .config - All: Analyzes all common text file types .PARAMETER ExcludeDirectories Directory names to exclude from analysis (e.g., '.git', 'bin', 'obj', 'node_modules'). Default excludes common build and version control directories. .PARAMETER Recurse Process files in subdirectories recursively. Default is $true. .PARAMETER MaxDepth Maximum directory depth to recurse when -Recurse is enabled. Default is unlimited. .PARAMETER GroupByExtension Group results by file extension to show encoding distribution per file type. .PARAMETER ShowFiles Include individual file details in the output. Default is $true. Use -ShowFiles:$false to disable if you only want summary statistics. .PARAMETER RecommendTarget Provide encoding recommendations based on file types (e.g., UTF8BOM for PowerShell files). Default is $true. Use -RecommendTarget:$false to disable recommendations. .EXAMPLE Get-FolderEncoding -Path . -Extensions 'ps1' Analyze PowerShell files in the current directory with file details and recommendations (default behavior). .EXAMPLE Get-FolderEncoding -Path @('.\Scripts', '.\Modules') -FileType PowerShell -GroupByExtension Analyze all PowerShell files in Scripts and Modules directories, grouped by extension. .EXAMPLE Get-FolderEncoding -Path . -Extensions @('cs', 'vb') -ShowFiles:$false Analyze C# and VB.NET files showing only summary statistics, no individual file details. .EXAMPLE Get-FolderEncoding -Path .\Source -FileType Web -ExcludeDirectories @('node_modules', 'dist') Analyze web files, excluding build directories, with full details and recommendations. .OUTPUTS PSCustomObject with the following properties: - Summary: Overall statistics and recommendations - EncodingDistribution: Hashtable of encoding counts - ExtensionAnalysis: Analysis grouped by file extension (if -GroupByExtension) - Files: Individual file details (default: included) - Recommendations: Encoding recommendations (default: included) .NOTES Author: PowerShell Encoding Tools This function provides analysis capabilities to complement Convert-FolderEncoding. Use this to understand your current encoding state before performing conversions. PowerShell Encoding Notes: - UTF8BOM is recommended for PowerShell files to ensure PS 5.1 compatibility - Mixed encodings within a project can cause issues - ASCII files are compatible with UTF8 but may need BOM for PowerShell #> [CmdletBinding(DefaultParameterSetName = 'Extensions')] param( [Parameter(Mandatory)] [Alias('Directory', 'Folder')] [string[]] $Path, [Parameter(ParameterSetName = 'Extensions', Mandatory)] [string[]] $Extensions, [Parameter(ParameterSetName = 'FileType', Mandatory)] [ValidateSet('PowerShell', 'CSharp', 'Web', 'Scripts', 'Text', 'All')] [string] $FileType, [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode', 'dist', 'build'), [bool] $Recurse = $true, [int] $MaxDepth, [switch] $GroupByExtension, [bool] $ShowFiles = $true, [bool] $RecommendTarget = $true ) foreach ($singlePath in $Path) { if (-not (Test-Path -LiteralPath $singlePath -PathType Container)) { throw "Directory path '$singlePath' not found or is not a directory" } } $fileTypeMappings = @{ 'PowerShell' = @('.ps1', '.psm1', '.psd1', '.ps1xml') 'CSharp' = @('.cs', '.csx', '.csproj') 'Web' = @('.html', '.htm', '.css', '.js', '.json', '.xml', '.xsl', '.xslt') 'Scripts' = @('.ps1', '.py', '.rb', '.sh', '.bash', '.bat', '.cmd') 'Text' = @('.txt', '.md', '.log', '.config', '.ini', '.conf', '.yaml', '.yml') 'All' = @('.ps1', '.psm1', '.psd1', '.ps1xml', '.cs', '.csx', '.html', '.htm', '.css', '.js', '.json', '.xml', '.txt', '.md', '.py', '.rb', '.sh', '.bat', '.cmd', '.config', '.ini', '.yaml', '.yml') } $encodingRecommendations = @{ '.ps1' = 'UTF8BOM' '.psm1' = 'UTF8BOM' '.psd1' = 'UTF8BOM' '.ps1xml' = 'UTF8BOM' '.cs' = 'UTF8' '.csx' = 'UTF8' '.html' = 'UTF8' '.htm' = 'UTF8' '.css' = 'UTF8' '.js' = 'UTF8' '.json' = 'UTF8' '.xml' = 'UTF8' '.txt' = 'UTF8' '.md' = 'UTF8' '.py' = 'UTF8' '.rb' = 'UTF8' '.sh' = 'UTF8' '.bat' = 'UTF8' '.cmd' = 'UTF8' '.config' = 'UTF8' '.ini' = 'UTF8' '.yaml' = 'UTF8' '.yml' = 'UTF8' } if ($PSCmdlet.ParameterSetName -eq 'FileType') { $targetExtensions = $fileTypeMappings[$FileType] Write-Verbose "Analyzing $FileType file type: $($targetExtensions -join ', ')" } else { $targetExtensions = $Extensions | ForEach-Object { if ($_.StartsWith('.')) { $_ } else { ".$_" } } Write-Verbose "Target extensions: $($targetExtensions -join ', ')" } if (-not (Get-Command Get-FileEncoding -ErrorAction SilentlyContinue)) { throw "Get-FileEncoding function not found. Please ensure the encoding detection module is loaded." } $allFiles = @() $summary = @{ TotalDirectories = $Path.Count ProcessedDirectories = 0 TotalFiles = 0 StartTime = Get-Date } Write-Verbose "Scanning directories for files..." foreach ($singlePath in $Path) { Write-Verbose "Processing directory: $singlePath" $summary.ProcessedDirectories++ try { $gciParams = @{ LiteralPath = $singlePath File = $true } if ($Recurse) { $gciParams.Recurse = $true if ($MaxDepth) { $gciParams.Depth = $MaxDepth } } $directoryFiles = Get-ChildItem @gciParams | Where-Object { $extension = $_.Extension.ToLower() $extensionMatch = $targetExtensions -contains $extension $directoryExcluded = $false if ($ExcludeDirectories -and $_.DirectoryName) { $relativePath = $_.DirectoryName.Replace($singlePath, '').TrimStart('\', '/') $directoryExcluded = $ExcludeDirectories | Where-Object { $relativePath -like "*$_*" } } return $extensionMatch -and -not $directoryExcluded } $allFiles += $directoryFiles $summary.TotalFiles += $directoryFiles.Count Write-Verbose "Found $($directoryFiles.Count) matching files in $singlePath" } catch { Write-Error "Error processing directory '$singlePath': $($_.Exception.Message)" continue } } if ($allFiles.Count -eq 0) { Write-Warning "No files found matching the specified criteria." Write-Verbose "Extensions searched: $($targetExtensions -join ', ')" Write-Verbose "Paths searched: $($Path -join ', ')" return } Write-Verbose "Analyzing $($allFiles.Count) files across $($summary.ProcessedDirectories) directories" $encodingDistribution = @{} $extensionAnalysis = @{} $fileDetails = @() $inconsistentExtensions = @() $progressCounter = 0 foreach ($file in $allFiles) { $progressCounter++ $percentComplete = [math]::Round(($progressCounter / $allFiles.Count) * 100, 1) Write-Progress -Activity "Analyzing file encodings" -Status "Processing $($file.Name) ($progressCounter of $($allFiles.Count))" -PercentComplete $percentComplete try { $encoding = Get-FileEncoding -Path $file.FullName $extension = $file.Extension.ToLower() if ($encodingDistribution.ContainsKey($encoding)) { $encodingDistribution[$encoding]++ } else { $encodingDistribution[$encoding] = 1 } if (-not $extensionAnalysis.ContainsKey($extension)) { $extensionAnalysis[$extension] = @{} } if ($extensionAnalysis[$extension].ContainsKey($encoding)) { $extensionAnalysis[$extension][$encoding]++ } else { $extensionAnalysis[$extension][$encoding] = 1 } if ($ShowFiles) { $recommendedEncoding = if ($RecommendTarget -and $encodingRecommendations.ContainsKey($extension)) { $encodingRecommendations[$extension] } else { $null } $fileDetails += [PSCustomObject]@{ FullPath = $file.FullName RelativePath = $file.FullName.Replace((Get-Location).Path, '.').TrimStart('\', '/') Extension = $extension CurrentEncoding = $encoding RecommendedEncoding = $recommendedEncoding NeedsConversion = $recommendedEncoding -and ($encoding -ne $recommendedEncoding) Size = $file.Length LastModified = $file.LastWriteTime } } } catch { Write-Warning "Could not analyze encoding for '$($file.FullName)': $($_.Exception.Message)" continue } } Write-Progress -Activity "Analyzing file encodings" -Completed foreach ($ext in $extensionAnalysis.Keys) { if ($extensionAnalysis[$ext].Keys.Count -gt 1) { $inconsistentExtensions += $ext } } $recommendations = @() if ($RecommendTarget) { foreach ($ext in $targetExtensions) { $recommendedEncoding = $encodingRecommendations[$ext] if ($recommendedEncoding -and $extensionAnalysis.ContainsKey($ext)) { $currentEncodings = $extensionAnalysis[$ext] $needsConversion = $currentEncodings.Keys | Where-Object { $_ -ne $recommendedEncoding } if ($needsConversion) { $totalFiles = ($currentEncodings.Values | Measure-Object -Sum).Sum $nonCompliantFiles = $needsConversion | ForEach-Object { $currentEncodings[$_] } | Measure-Object -Sum | Select-Object -ExpandProperty Sum $recommendations += [PSCustomObject]@{ Extension = $ext RecommendedEncoding = $recommendedEncoding TotalFiles = $totalFiles CompliantFiles = $totalFiles - $nonCompliantFiles NonCompliantFiles = $nonCompliantFiles CurrentEncodings = $currentEncodings } } } } } $summary.EndTime = Get-Date $summary.Duration = $summary.EndTime - $summary.StartTime $summary.UniqueEncodings = $encodingDistribution.Keys.Count $summary.MostCommonEncoding = ($encodingDistribution.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 1).Key $summary.HasMixedEncodings = $inconsistentExtensions.Count -gt 0 $summary.InconsistentExtensions = $inconsistentExtensions Write-Verbose "Analysis completed: $($summary.TotalFiles) files, $($summary.UniqueEncodings) unique encodings" Write-Verbose "Most common encoding: $($summary.MostCommonEncoding)" if ($summary.HasMixedEncodings) { Write-Verbose "Extensions with mixed encodings: $($inconsistentExtensions -join ', ')" } $result = [PSCustomObject]@{ Summary = [PSCustomObject]@{ TotalDirectories = $summary.TotalDirectories ProcessedDirectories = $summary.ProcessedDirectories TotalFiles = $summary.TotalFiles UniqueEncodings = $summary.UniqueEncodings MostCommonEncoding = $summary.MostCommonEncoding HasMixedEncodings = $summary.HasMixedEncodings InconsistentExtensions = $summary.InconsistentExtensions Duration = $summary.Duration StartTime = $summary.StartTime EndTime = $summary.EndTime } EncodingDistribution = $encodingDistribution Files = if ($ShowFiles) { $fileDetails } else { $null } Recommendations = if ($RecommendTarget) { $recommendations } else { $null } } if ($GroupByExtension) { $result | Add-Member -NotePropertyName 'ExtensionAnalysis' -NotePropertyValue $extensionAnalysis } $result | Add-Member -MemberType ScriptMethod -Name 'DisplaySummary' -Value { Write-Host "" Write-Host "📊 Folder Encoding Analysis Summary" -ForegroundColor Green Write-Host "==================================" Write-Host "📁 Directories analyzed: $($this.Summary.ProcessedDirectories)" -ForegroundColor Cyan Write-Host "📄 Total files found: $($this.Summary.TotalFiles)" -ForegroundColor Cyan Write-Host "🔤 Unique encodings: $($this.Summary.UniqueEncodings)" -ForegroundColor Cyan Write-Host "⭐ Most common encoding: $($this.Summary.MostCommonEncoding)" -ForegroundColor Green Write-Host "⚠️ Mixed encodings: $($this.Summary.HasMixedEncodings)" -ForegroundColor $(if ($this.Summary.HasMixedEncodings) { 'Yellow' } else { 'Green' }) if ($this.Summary.HasMixedEncodings) { Write-Host "📝 Inconsistent extensions: $($this.Summary.InconsistentExtensions -join ', ')" -ForegroundColor Yellow } Write-Host "" Write-Host "📈 Encoding Distribution:" -ForegroundColor Blue $this.EncodingDistribution.GetEnumerator() | Sort-Object Value -Descending | ForEach-Object { $percentage = [math]::Round(($_.Value / $this.Summary.TotalFiles) * 100, 1) Write-Host " $($_.Key): $($_.Value) files ($percentage%)" -ForegroundColor White } if ($this.Recommendations -and $this.Recommendations.Count -gt 0) { Write-Host "" Write-Host "💡 Recommendations:" -ForegroundColor Magenta foreach ($rec in $this.Recommendations) { Write-Host " $($rec.Extension) files:" -ForegroundColor White Write-Host " Target encoding: $($rec.RecommendedEncoding)" -ForegroundColor Green Write-Host " Files needing conversion: $($rec.NonCompliantFiles)/$($rec.TotalFiles)" -ForegroundColor Yellow } } Write-Host "" Write-Host "⏱️ Analysis duration: $($this.Summary.Duration.TotalSeconds.ToString('F2')) seconds" -ForegroundColor Gray } $result | Add-Member -MemberType ScriptMethod -Name 'ToString' -Value { return "Folder Encoding Analysis: $($this.Summary.TotalFiles) files, $($this.Summary.UniqueEncodings) encodings, Most common: $($this.Summary.MostCommonEncoding)" } -Force return $result } function Get-FunctionAliases { [cmdletbinding()] param ( [Alias('PSPath', 'FullName')][Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)][string[]]$Path, [string] $Content, [switch] $RecurseFunctionNames, [switch] $AsHashtable ) process { if ($Content) { $ProcessData = $Content $Code = $true } else { $ProcessData = $Path $Code = $false } foreach ($File in $ProcessData) { $Ast = $null if ($Code) { $FileAst = [System.Management.Automation.Language.Parser]::ParseInput($File, [ref]$null, [ref]$null) } else { $FileAst = [System.Management.Automation.Language.Parser]::ParseFile($File , [ref]$null, [ref]$null) } $ListOfFuncionsAst = $FileAst.FindAll( { param ($ast) $ast -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $RecurseFunctionNames ) if ($AsHashtable) { $OutputList = [ordered] @{} foreach ($Function in $ListOfFuncionsAst) { $AliasDefinitions = $Function.FindAll( { param ( $ast ) $ast -is [System.Management.Automation.Language.AttributeAst] -and $ast.TypeName.Name -eq 'Alias' -and $ast.Parent -is [System.Management.Automation.Language.ParamBlockAst] }, $true) $AliasTarget = @( $AliasDefinitions.PositionalArguments.Value foreach ($_ in $AliasDefinitions.Parent.CommandElements) { if ($_.StringConstantType -eq 'BareWord' -and $null -ne $_.Value -and $_.Value -notin ('New-Alias', 'Set-Alias', $Function.Name)) { $_.Value } } ) $OutputList[$Function.Name] = $AliasTarget } $OutputList } else { $Ast = $Null $AliasDefinitions = $FileAst.FindAll( { param ( $ast ) $ast -is [System.Management.Automation.Language.AttributeAst] -and $ast.TypeName.Name -eq 'Alias' -and $ast.Parent -is [System.Management.Automation.Language.ParamBlockAst] }, $true) $AliasTarget = @( $AliasDefinitions.PositionalArguments.Value foreach ($_ in $AliasDefinitions.Parent.CommandElements) { if ($_.StringConstantType -eq 'BareWord' -and $null -ne $_.Value -and $_.Value -notin ('New-Alias', 'Set-Alias', $FunctionName)) { $_.Value } } ) [PsCustomObject]@{ Name = $ListOfFuncionsAst.Name Alias = $AliasTarget } } } } } function Get-FunctionAliasesFromFolder { [cmdletbinding()] param( [string] $FullProjectPath, [string[]] $Folder, [Array] $Files, [string] $FunctionsToExport, [string] $AliasesToExport ) [Array] $FilesPS1 = foreach ($File in $Files) { if ($FunctionsToExport) { $PathFunctions = [io.path]::Combine($FullProjectPath, $FunctionsToExport, '*') if ($File.FullName -like $PathFunctions) { if ($File.Extension -eq '.ps1' -or $File.Extension -eq '*.psm1') { $File } } } if ($AliasesToExport -and $AliasesToExport -ne $FunctionsToExport) { $PathAliases = [io.path]::Combine($FullProjectPath, $AliasesToExport, '*') if ($File.FullName -like $PathAliases) { if ($File.Extension -eq '.ps1' -or $File.Extension -eq '*.psm1') { $File } } } } [Array] $Content = foreach ($File in $FilesPS1 | Sort-Object -Unique) { '' Get-Content -LiteralPath $File.FullName -Raw -Encoding UTF8 } $Code = $Content -join [System.Environment]::NewLine $OutputAliasesToExport = Get-FunctionAliases -Content $Code -AsHashtable $OutputAliasesToExport } function Get-FunctionNames { [cmdletbinding()] param( [string] $Path, [switch] $Recurse ) if ($Path -ne '' -and (Test-Path -LiteralPath $Path)) { $FilePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) [System.Management.Automation.Language.Parser]::ParseFile(($FilePath), [ref]$null, [ref]$null).FindAll( { param($c)$c -is [Management.Automation.Language.FunctionDefinitionAst] }, $Recurse).Name } } function Get-GitLog { # Source https://gist.github.com/thedavecarroll/3245449f5ff893e51907f7aa13e33ebe # Author: thedavecarroll/Get-GitLog.ps1 [CmdLetBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(ParameterSetName = 'Default', Mandatory)] [Parameter(ParameterSetName = 'SourceTarget', Mandatory)] [ValidateScript({ Resolve-Path -Path $_ | Test-Path })] [string]$GitFolder, [Parameter(ParameterSetName = 'SourceTarget', Mandatory)] [string]$StartCommitId, [Parameter(ParameterSetName = 'SourceTarget')] [string]$EndCommitId = 'HEAD' ) Push-Location try { Set-Location -Path $GitFolder $GitCommand = Get-Command -Name git -ErrorAction Stop } catch { $PSCmdlet.ThrowTerminatingError($_) } if ($StartCommitId) { $GitLogCommand = '"{0}" log --oneline --format="%H`t%h`t%ai`t%an`t%ae`t%ci`t%cn`t%ce`t%s`t%f" {1}...{2} 2>&1' -f $GitCommand.Source, $StartCommitId, $EndCommitId } else { $GitLogCommand = '"{0}" log --oneline --format="%H`t%h`t%ai`t%an`t%ae`t%ci`t%cn`t%ce`t%s`t%f" 2>&1' -f $GitCommand.Source } Write-Verbose -Message $GitLogCommand $GitLog = Invoke-Expression -Command "& $GitLogCommand" -ErrorAction SilentlyContinue Pop-Location if ($GitLog[0] -notmatch 'fatal:') { $GitLog | ConvertFrom-Csv -Delimiter "`t" -Header 'CommitId', 'ShortCommitId', 'AuthorDate', 'AuthorName', 'AuthorEmail', 'CommitterDate', 'CommitterName', 'ComitterEmail', 'CommitMessage', 'SafeCommitMessage' } else { if ($GitLog[0] -like "fatal: ambiguous argument '*...*'*") { Write-Warning -Message 'Unknown revision. Please check the values for StartCommitId or EndCommitId; omit the parameters to retrieve the entire log.' } else { Write-Error -Category InvalidArgument -Message ($GitLog -join "`n") } } } function Get-LineEndingType { param([string] $FilePath) try { $bytes = [System.IO.File]::ReadAllBytes($FilePath) if ($bytes.Length -eq 0) { return @{ LineEnding = 'None' HasFinalNewline = $true FileSize = 0 } } $crlfCount = 0 $lfOnlyCount = 0 $crOnlyCount = 0 $hasFinalNewline = $false $lastByte = $bytes[$bytes.Length - 1] if ($lastByte -eq 10) { $hasFinalNewline = $true if ($bytes.Length -gt 1 -and $bytes[$bytes.Length - 2] -eq 13) { } } elseif ($lastByte -eq 13) { $hasFinalNewline = $true } for ($i = 0; $i -lt $bytes.Length - 1; $i++) { if ($bytes[$i] -eq 13 -and $bytes[$i + 1] -eq 10) { $crlfCount++ $i++ } elseif ($bytes[$i] -eq 10) { $lfOnlyCount++ } elseif ($bytes[$i] -eq 13) { if ($i + 1 -lt $bytes.Length -and $bytes[$i + 1] -ne 10) { $crOnlyCount++ } } } if ($bytes.Length -gt 0) { $lastByte = $bytes[$bytes.Length - 1] if ($lastByte -eq 10 -and ($bytes.Length -eq 1 -or $bytes[$bytes.Length - 2] -ne 13)) { $lfOnlyCount++ } elseif ($lastByte -eq 13) { $crOnlyCount++ } } $typesFound = @() if ($crlfCount -gt 0) { $typesFound += 'CRLF' } if ($lfOnlyCount -gt 0) { $typesFound += 'LF' } if ($crOnlyCount -gt 0) { $typesFound += 'CR' } $lineEndingType = if ($typesFound.Count -eq 0) { 'None' } elseif ($typesFound.Count -eq 1) { $typesFound[0] } else { 'Mixed' } return @{ LineEnding = $lineEndingType HasFinalNewline = $hasFinalNewline FileSize = $bytes.Length } } catch { return @{ LineEnding = 'Error' HasFinalNewline = $false FileSize = 0 } } } function Get-RelativePath { <# .SYNOPSIS Gets the relative path from one path to another, compatible with PowerShell 5.1. .DESCRIPTION Provides PowerShell 5.1 compatible relative path calculation that works like [System.IO.Path]::GetRelativePath() which is only available in .NET Core 2.0+. .PARAMETER From The base path to calculate the relative path from. .PARAMETER To The target path to calculate the relative path to. .EXAMPLE Get-RelativePath -From 'C:\Projects' -To 'C:\Projects\MyProject\file.txt' Returns: MyProject\file.txt #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $From, [Parameter(Mandatory)] [string] $To ) if ([System.IO.Path].GetMethods() | Where-Object { $_.Name -eq 'GetRelativePath' -and $_.IsStatic }) { return [System.IO.Path]::GetRelativePath($From, $To) } try { $fromPath = [System.IO.Path]::GetFullPath($From) if (-not $fromPath.EndsWith([System.IO.Path]::DirectorySeparatorChar)) { $fromPath += [System.IO.Path]::DirectorySeparatorChar } $fromUri = New-Object System.Uri $fromPath $toUri = New-Object System.Uri ([System.IO.Path]::GetFullPath($To)) $relativeUri = $fromUri.MakeRelativeUri($toUri) $relativePath = [System.Uri]::UnescapeDataString($relativeUri.ToString()) if ([System.IO.Path]::DirectorySeparatorChar -eq '\') { $relativePath = $relativePath.Replace('/', '\') } return $relativePath } catch { return [System.IO.Path]::GetFileName($To) } } function Get-RequiredModule { [cmdletbinding()] param( [string] $Path, [string] $Name ) $LiteralPath = [System.IO.Path]::Combine($Path, $Name) $PrimaryModule = Get-ChildItem -LiteralPath $LiteralPath -Filter '*.psd1' -Recurse -ErrorAction SilentlyContinue -Depth 1 if ($PrimaryModule) { $Module = Get-Module -ListAvailable $PrimaryModule.FullName -ErrorAction SilentlyContinue -Verbose:$false if ($Module) { [Array] $RequiredModules = $Module.RequiredModules.Name if ($null -ne $RequiredModules) { $null } $RequiredModules foreach ($_ in $RequiredModules) { Get-RequiredModule -Path $Path -Name $_ } } } else { Write-Warning "Initialize-ModulePortable - Modules to load not found in $Path" } } function Get-RestMethodExceptionDetailsOrNull { [CmdletBinding()] param( [Exception] $restMethodException ) try { $responseDetails = @{ ResponseUri = $exception.Response.ResponseUri StatusCode = $exception.Response.StatusCode StatusDescription = $exception.Response.StatusDescription ErrorMessage = $exception.Message } [string] $responseDetailsAsNicelyFormattedString = Convert-HashTableToNicelyFormattedString $responseDetails [string] $errorInfo = "Request Details:" + $NewLine + $requestDetailsAsNicelyFormattedString $errorInfo += $NewLine $errorInfo += "Response Details:" + $NewLine + $responseDetailsAsNicelyFormattedString return $errorInfo } catch { return $null } } function Get-ScriptCommands { [CmdletBinding()] param( [string] $FilePath, [alias('ScriptBlock')][scriptblock] $Code, [switch] $CommandsOnly ) $astTokens = $null $astErr = $null if ($FilePath) { $FileAst = [System.Management.Automation.Language.Parser]::ParseFile($FilePath, [ref]$astTokens, [ref]$astErr) } else { $FileAst = [System.Management.Automation.Language.Parser]::ParseInput($Code, [ref]$astTokens, [ref]$astErr) } $Commands = Get-AstTokens -ASTTokens $astTokens -Commands $Commands -FileAst $FileAst $Commands | Sort-Object -Unique } function Get-ScriptsContentAndTryReplace { <# .SYNOPSIS Gets script content and replaces $PSScriptRoot\..\..\ with $PSScriptRoot\ .DESCRIPTION Gets script content and replaces $PSScriptRoot\..\..\ with $PSScriptRoot\ .PARAMETER Files Parameter description .PARAMETER OutputPath Parameter description .EXAMPLE Get-ScriptsContentAndTryReplace -Files 'C:\Support\GitHub\PSWriteHTML\Private\Get-HTMLLogos.ps1' -OutputPath "C:\Support\GitHub\PSWriteHTML\Private\Get-HTMLLogos1.ps1" .NOTES Often in code people would use relative paths to get to the root of the module. This is all great but the path changes during merge. So we fix this by replacing $PSScriptRoot\..\..\ with $PSScriptRoot\ While in best case they should always use $MyInvocation.MyCommand.Module.ModuleBase It's not always possible. So this is a workaround. Very bad workaround, but it works, but may have unintended consequences. $Content = @( '$PSScriptRoot\..\..\Build\Manage-PSWriteHTML.ps1' '$PSScriptRoot\..\Build\Manage-PSWriteHTML.ps1' '$PSScriptRoot\Build\Manage-PSWriteHTML.ps1' "[IO.Path]::Combine(`$PSScriptRoot, '..', 'Images')" "[IO.Path]::Combine(`$PSScriptRoot,'..','Images')" ) $Content = $Content -replace [regex]::Escape('$PSScriptRoot\..\..\'), '$PSScriptRoot\' -replace [regex]::Escape('$PSScriptRoot\..\'), '$PSScriptRoot\' $Content = $Content -replace [regex]::Escape("`$PSScriptRoot, '..',"), '$PSScriptRoot,' -replace [regex]::Escape("`$PSScriptRoot,'..',"), '$PSScriptRoot,' $Content #> [cmdletbinding()] param( [string[]] $Files, [string] $OutputPath, [switch] $DoNotAttemptToFixRelativePaths ) if ($PSVersionTable.PSVersion.Major -gt 5) { $Encoding = 'UTF8BOM' } else { $Encoding = 'UTF8' } if ($DoNotAttemptToFixRelativePaths) { Write-TextWithTime -Text "Without expanding variables (`$PSScriptRoot\..\.. etc.)" { foreach ($FilePath in $Files) { $Content = Get-Content -Path $FilePath -Raw -Encoding utf8 if ($Content.Count -gt 0) { try { $Content | Out-File -Append -LiteralPath $OutputPath -Encoding $Encoding } catch { $ErrorMessage = $_.Exception.Message Write-Text "[-] Get-ScriptsContentAndTryReplace - Merge on file $FilePath failed. Error: $ErrorMessage" -Color Red return $false } } } } -PreAppend Plus -Color Green -SpacesBefore " " -ColorTime Green } else { Write-TextWithTime -Text "Replacing expandable variables (`$PSScriptRoot\..\.. etc.)" { foreach ($FilePath in $Files) { $Content = Get-Content -Path $FilePath -Raw -Encoding utf8 if ($Content.Count -gt 0) { if (-not $FilePath.EndsWith('Get-ScriptsContentAndTryReplace.ps1')) { $Content = $Content -replace [regex]::Escape('$PSScriptRoot\..\..\'), '$PSScriptRoot\' $Content = $Content -replace [regex]::Escape('$PSScriptRoot\..\'), '$PSScriptRoot\' $Content = $Content -replace [regex]::Escape("`$PSScriptRoot, '..',"), '$PSScriptRoot,' $Content = $Content -replace [regex]::Escape("`$PSScriptRoot,'..',"), '$PSScriptRoot,' } } try { $Content | Out-File -Append -LiteralPath $OutputPath -Encoding $Encoding } catch { $ErrorMessage = $_.Exception.Message Write-Text "[-] Get-ScriptsContentAndTryReplace - Merge on file $FilePath failed. Error: $ErrorMessage" -Color Red return $false } } } -PreAppend Information -Color Magenta -SpacesBefore " " -ColorBefore Magenta -ColorTime Magenta } } function Import-ValidCertificate { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] param( [parameter(Mandatory, ParameterSetName = 'FilePath')][string] $FilePath, [parameter(Mandatory, ParameterSetName = 'Base64')][string] $CertificateAsBase64, [parameter(Mandatory)][string] $PfxPassword ) if ($FilePath -and (Test-Path -LiteralPath $FilePath)) { $TemporaryFile = $FilePath } elseif ($CertificateAsBase64) { $TemporaryFile = [io.path]::GetTempFileName() if ($PSVersionTable.PSEdition -eq 'Core') { Set-Content -AsByteStream -Value $([System.Convert]::FromBase64String($CertificateAsBase64)) -Path $TemporaryFile -ErrorAction Stop } else { Set-Content -Value $([System.Convert]::FromBase64String($CertificateAsBase64)) -Path $TemporaryFile -Encoding Byte -ErrorAction Stop } } else { return $false } } function Initialize-InternalTests { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [string] $Type ) if ($Configuration.Options.$Type.TestsPath -and (Test-Path -LiteralPath $Configuration.Options.$Type.TestsPath)) { Write-TextWithTime -PreAppend Plus -Text "Running tests ($Type)" { $TestsResult = Invoke-Pester -Script $Configuration.Options.$Type.TestsPath -Verbose -PassThru Write-Host if (-not $TestsResult) { if ($Configuration.Options.$Type.Force) { Write-Text "[e] Tests ($Type) failed, but Force was used to skip failed tests. Continuing" -Color Red } else { Write-Text "[e] Tests ($Type) failed. Terminating." -Color Red return $false } } elseif ($TestsResult.FailedCount -gt 0) { if ($Configuration.Options.$Type.Force) { Write-Text "[e] Tests ($Type) failed, but Force was used to skip failed tests. Continuing" -Color Red } else { Write-Text "[e] Tests ($Type) failed (failedCount $($TestsResult.FailedCount)). Terminating." -Color Red return $false } } } -Color Blue } else { Write-Text "[e] Tests ($Type) are enabled, but the path to tests doesn't exits. Terminating." -Color Red return $false } } function Initialize-ReplacePath { [CmdletBinding()] param( [string] $ReplacementPath, [string] $ModuleName, [string] $ModuleVersion, [System.Collections.IDictionary] $Configuration ) $TagName = "v$($ModuleVersion)" if ($Configuration.CurrentSettings.PreRelease) { $ModuleVersionWithPreRelease = "$($ModuleVersion)-$($Configuration.CurrentSettings.PreRelease)" $TagModuleVersionWithPreRelease = "v$($ModuleVersionWithPreRelease)" } else { $ModuleVersionWithPreRelease = $ModuleVersion $TagModuleVersionWithPreRelease = "v$($ModuleVersion)" } $ReplacementPath = $ReplacementPath.Replace('<TagName>', $TagName) $ReplacementPath = $ReplacementPath.Replace('{TagName}', $TagName) $ReplacementPath = $ReplacementPath.Replace('<ModuleVersion>', $ModuleVersion) $ReplacementPath = $ReplacementPath.Replace('{ModuleVersion}', $ModuleVersion) $ReplacementPath = $ReplacementPath.Replace('<ModuleVersionWithPreRelease>', $ModuleVersionWithPreRelease) $ReplacementPath = $ReplacementPath.Replace('{ModuleVersionWithPreRelease}', $ModuleVersionWithPreRelease) $ReplacementPath = $ReplacementPath.Replace('<TagModuleVersionWithPreRelease>', $TagModuleVersionWithPreRelease) $ReplacementPath = $ReplacementPath.Replace('{TagModuleVersionWithPreRelease}', $TagModuleVersionWithPreRelease) $ReplacementPath = $ReplacementPath.Replace('<ModuleName>', $ModuleName) $ReplacementPath = $ReplacementPath.Replace('{ModuleName}', $ModuleName) $ReplacementPath } function Invoke-RestMethodAndThrowDescriptiveErrorOnFailure { [cmdletbinding()] param( [System.Collections.IDictionary] $requestParametersHashTable ) $requestDetailsAsNicelyFormattedString = Convert-HashTableToNicelyFormattedString $requestParametersHashTable Write-Verbose "Making web request with the following parameters:$NewLine$requestDetailsAsNicelyFormattedString" try { $webRequestResult = Invoke-RestMethod @requestParametersHashTable } catch { [Exception] $exception = $_.Exception [string] $errorMessage = Get-RestMethodExceptionDetailsOrNull -restMethodException $exception if ([string]::IsNullOrWhiteSpace($errorMessage)) { $errorMessage = $exception.ToString() } throw "An unexpected error occurred while making web request:$NewLine$errorMessage" } Write-Verbose "Web request returned the following result:$NewLine$webRequestResult" return $webRequestResult } function Merge-Module { [CmdletBinding()] param ( [string] $ModuleName, [string] $ModulePathSource, [string] $ModulePathTarget, [Parameter(Mandatory = $false, ValueFromPipeline = $false)] [ValidateSet("ASC", "DESC", "NONE", '')] [string] $Sort = 'NONE', [string[]] $FunctionsToExport, [string[]] $AliasesToExport, [System.Collections.IDictionary] $AliasesAndFunctions, [System.Collections.IDictionary] $CmdletsAliases, [Array] $LibrariesStandard, [Array] $LibrariesCore, [Array] $LibrariesDefault, [System.Collections.IDictionary] $FormatCodePSM1, [System.Collections.IDictionary] $FormatCodePSD1, [System.Collections.IDictionary] $Configuration, [string[]] $DirectoriesWithPS1, [string[]] $ClassesPS1, [System.Collections.IDictionary] $IncludeAsArray ) if ($PSVersionTable.PSVersion.Major -gt 5) { $Encoding = 'UTF8BOM' } else { $Encoding = 'UTF8' } $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] Merging files into PSM1" -Color Blue $PSM1FilePath = [System.IO.Path]::Combine($ModulePathTarget, "$ModuleName.psm1") $PSD1FilePath = [System.IO.Path]::Combine($ModulePathTarget, "$ModuleName.psd1") [Array] $ArrayIncludes = foreach ($VariableName in $IncludeAsArray.Keys) { $FilePathVariables = [System.IO.Path]::Combine($ModulePathSource, $IncludeAsArray[$VariableName], "*.ps1") [Array] $FilesInternal = if ($PSEdition -eq 'Core') { Get-ChildItem -Path $FilePathVariables -ErrorAction SilentlyContinue -Recurse -FollowSymlink } else { Get-ChildItem -Path $FilePathVariables -ErrorAction SilentlyContinue -Recurse } "$VariableName = @(" foreach ($Internal in $FilesInternal) { Get-Content -Path $Internal.FullName -Raw -Encoding utf8 } ")" } if ($Configuration.Steps.BuildModule.ClassesDotSource) { [Array] $ListDirectoriesPS1 = foreach ($Dir in $DirectoriesWithPS1) { if ($Dir -ne $ClassesPS1) { $Dir } } } else { [Array] $ListDirectoriesPS1 = $DirectoriesWithPS1 } [Array] $ScriptFunctions = foreach ($Directory in $ListDirectoriesPS1) { $PathToFiles = [System.IO.Path]::Combine($ModulePathSource, $Directory, "*.ps1") if ($PSEdition -eq 'Core') { Get-ChildItem -Path $PathToFiles -ErrorAction SilentlyContinue -Recurse -FollowSymlink } else { Get-ChildItem -Path $PathToFiles -ErrorAction SilentlyContinue -Recurse } } [Array] $ClassesFunctions = foreach ($Directory in $ClassesPS1) { $PathToFiles = [System.IO.Path]::Combine($ModulePathSource, $Directory, "*.ps1") if ($PSEdition -eq 'Core') { Get-ChildItem -Path $PathToFiles -ErrorAction SilentlyContinue -Recurse -FollowSymlink } else { Get-ChildItem -Path $PathToFiles -ErrorAction SilentlyContinue -Recurse } } if ($Sort -eq 'ASC') { $ScriptFunctions = $ScriptFunctions | Sort-Object -Property Name $ClassesFunctions = $ClassesFunctions | Sort-Object -Property Name } elseif ($Sort -eq 'DESC') { $ScriptFunctions = $ScriptFunctions | Sort-Object -Descending -Property Name $ClassesFunctions = $ClassesFunctions | Sort-Object -Descending -Property Name } if ($ArrayIncludes.Count -gt 0) { $ArrayIncludes | Out-File -Append -LiteralPath $PSM1FilePath -Encoding $Encoding } $Success = Get-ScriptsContentAndTryReplace -Files $ScriptFunctions -OutputPath $PSM1FilePath -DoNotAttemptToFixRelativePaths:$Configuration.Steps.BuildModule.DoNotAttemptToFixRelativePaths if ($Success -eq $false) { return $false } $FilePathUsing = [System.IO.Path]::Combine($ModulePathTarget, "$ModuleName.Usings.ps1") $UsingInPlace = Format-UsingNamespace -FilePath $PSM1FilePath -FilePathUsing $FilePathUsing if ($UsingInPlace) { $Success = Format-Code -FilePath $FilePathUsing -FormatCode $FormatCodePSM1 if ($Success -eq $false) { return $false } $Configuration.UsingInPlace = "$ModuleName.Usings.ps1" } $TimeToExecute.Stop() Write-Text "[+] Merging files into PSM1 [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] Detecting required modules" -Color Blue $RequiredModules = @( if ($Configuration.Information.Manifest.RequiredModules.Count -gt 0) { if ($Configuration.Information.Manifest.RequiredModules[0] -is [System.Collections.IDictionary]) { $Configuration.Information.Manifest.RequiredModules.ModuleName } else { $Configuration.Information.Manifest.RequiredModules } } if ($Configuration.Information.Manifest.ExternalModuleDependencies.Count -gt 0) { $Configuration.Information.Manifest.ExternalModuleDependencies } ) [Array] $DuplicateModules = $RequiredModules | Group-Object | Where-Object { $_.Count -gt 1 } | Select-Object -ExpandProperty Name if ($DuplicateModules.Count -gt 0) { Write-Text " [!] Duplicate modules detected in required modules configuration. Please fix your configuration." -Color Red foreach ($DuplicateModule in $DuplicateModules) { Write-Text " [>] Duplicate module $DuplicateModule" -Color Red } return $false } [Array] $ApprovedModules = $Configuration.Options.Merge.Integrate.ApprovedModules | Sort-Object -Unique $ModulesThatWillMissBecauseOfIntegrating = [System.Collections.Generic.List[string]]::new() [Array] $DependantRequiredModules = foreach ($Module in $RequiredModules) { [Array] $TemporaryDependant = Find-RequiredModules -Name $Module if ($TemporaryDependant.Count -gt 0) { if ($Module -in $ApprovedModules) { foreach ($ModulesTemp in $TemporaryDependant) { $ModulesThatWillMissBecauseOfIntegrating.Add($ModulesTemp) } } else { $TemporaryDependant } } } $DependantRequiredModules = $DependantRequiredModules | Sort-Object -Unique $TimeToExecute.Stop() Write-Text "[+] Detecting required modules [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] Searching for missing functions" -Color Blue $MissingFunctions = Get-MissingFunctions -FilePath $PSM1FilePath -SummaryWithCommands -ApprovedModules $ApprovedModules $TimeToExecute.Stop() Write-Text "[+] Searching for missing functions [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] Detecting commands used" -Color Blue [Array] $ApplicationsCheck = $MissingFunctions.Summary | Where-Object { $_.CommandType -eq 'Application' } | Sort-Object -Unique -Property 'Source' [Array] $ModulesToCheck = $MissingFunctions.Summary | Where-Object { $_.CommandType -ne 'Application' -and $_.CommandType -ne '' } | Sort-Object -Unique -Property 'Source' [Array] $CommandsWithoutModule = $MissingFunctions.Summary | Where-Object { $_.CommandType -eq '' } if ($ApplicationsCheck.Source) { Write-Text "[i] Applications used by this module. Make sure those are present on destination system. " -Color Yellow foreach ($Application in $ApplicationsCheck.Source) { Write-Text " [>] Application $Application " -Color Yellow } } $TimeToExecute.Stop() Write-Text "[+] Detecting commands used [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue $approveRequiredModulesSplat = @{ ApprovedModules = $ApprovedModules ModulesToCheck = $ModulesToCheck RequiredModules = $RequiredModules DependantRequiredModules = $DependantRequiredModules MissingFunctions = $MissingFunctions Configuration = $Configuration CommandsWithoutModule = $CommandsWithoutModule } $Success = Approve-RequiredModules @approveRequiredModulesSplat if ($Success -eq $false) { return $false } if ($Configuration.Steps.BuildModule.MergeMissing -eq $true) { if (Test-Path -LiteralPath $PSM1FilePath) { $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] Merge mergable commands" -Color Blue $PSM1Content = Get-Content -LiteralPath $PSM1FilePath -Raw -Encoding $Encoding $IntegrateContent = @( $MissingFunctions.Functions $PSM1Content ) $IntegrateContent | Set-Content -LiteralPath $PSM1FilePath -Encoding $Encoding $NewRequiredModules = foreach ($_ in $Configuration.Information.Manifest.RequiredModules) { if ($_ -is [System.Collections.IDictionary]) { if ($_.ModuleName -notin $ApprovedModules) { $_ } } else { if ($_ -notin $ApprovedModules) { $_ } } } $Configuration.Information.Manifest.RequiredModules = $NewRequiredModules $TimeToExecute.Stop() Write-Text "[+] Merge mergable commands [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue } } $TimeToExecuteSign = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] Finalizing PSM1/PSD1" -Color Blue if ($null -eq $Configuration.Steps.BuildModule.DebugDLL) { $Configuration.Steps.BuildModule.DebugDLL = $false } if ($Configuration.Steps.BuildLibraries.NETLineByLineAddType) { $DoNotOptimizeLoading = $Configuration.Steps.BuildLibraries.NETLineByLineAddType } else { $DoNotOptimizeLoading = $false } [Array] $LibraryContent = New-LibraryContent -Configuration $Configuration -LibrariesStandard $LibrariesStandard -LibrariesCore $LibrariesCore -LibrariesDefault $LibrariesDefault -OptimizedLoading:(-not $DoNotOptimizeLoading) if ($LibraryContent.Count -gt 0) { if ($Configuration.Steps.BuildModule.LibrarySeparateFile -eq $true) { $LibariesPath = [System.IO.Path]::Combine($ModulePathTarget, "$ModuleName.Libraries.ps1") $ScriptsToProcessLibrary = "$ModuleName.Libraries.ps1" } if ($Configuration.Steps.BuildModule.LibraryDotSource -eq $true) { $LibariesPath = [System.IO.Path]::Combine($ModulePathTarget, "$ModuleName.Libraries.ps1") $DotSourcePath = ". `$PSScriptRoot\$ModuleName.Libraries.ps1" } if ($LibariesPath) { $LibraryContent | Out-File -Append -LiteralPath $LibariesPath -Encoding $Encoding } } if ($ClassesFunctions.Count -gt 0) { $ClassesPath = [System.IO.Path]::Combine($ModulePathTarget, "$ModuleName.Classes.ps1") $DotSourceClassPath = ". `$PSScriptRoot\$ModuleName.Classes.ps1" $Success = Get-ScriptsContentAndTryReplace -Files $ClassesFunctions -OutputPath $ClassesPath -DoNotAttemptToFixRelativePaths:$Configuration.Steps.BuildModule.DoNotAttemptToFixRelativePaths if ($Success -eq $false) { return $false } } if ($LibariesPath -gt 0 -or $ClassesPath -gt 0 -or $Configuration.Steps.BuildModule.ResolveBinaryConflicts) { if (Test-Path -LiteralPath $PSM1FilePath) { $PSM1Content = Get-Content -LiteralPath $PSM1FilePath -Raw -Encoding UTF8 } else { Write-Text "[+] PSM1 file doesn't exists. Creating empty content" -Color Blue $PSM1Content = '' } $IntegrateContent = @( if ($Configuration.Steps.BuildModule.ResolveBinaryConflicts -is [System.Collections.IDictionary]) { New-DLLResolveConflict -ProjectName $Configuration.Steps.BuildModule.ResolveBinaryConflicts.ProjectName -LibraryConfiguration $Configuration.Steps.BuildLibraries } elseif ($Configuration.Steps.BuildModule.ResolveBinaryConflicts -eq $true) { New-DLLResolveConflict -LibraryConfiguration $Configuration.Steps.BuildLibraries } Add-BinaryImportModule -Configuration $Configuration -LibrariesStandard $LibrariesStandard -LibrariesCore $LibrariesCore -LibrariesDefault $LibrariesDefault if ($LibraryContent.Count -gt 0) { if ($DotSourcePath) { "# Dot source all libraries by loading external file" $DotSourcePath "" } if (-not $LibariesPath) { "# Load all types" $LibraryContent "" } } if ($ClassesPath) { "# Dot source all classes by loading external file" $DotSourceClassPath "" } $PSM1Content ) $IntegrateContent | Set-Content -LiteralPath $PSM1FilePath -Encoding $Encoding } if ($Configuration.Information.Manifest.DotNetFrameworkVersion) { Find-NetFramework -RequireVersion $Configuration.Information.Manifest.DotNetFrameworkVersion | Out-File -Append -LiteralPath $PSM1FilePath -Encoding $Encoding } $newPSMFileSplat = @{ Path = $PSM1FilePath FunctionNames = $FunctionsToExport FunctionAliaes = $AliasesToExport AliasesAndFunctions = $AliasesAndFunctions CmdletsAliases = $CmdletsAliases LibrariesStandard = $LibrariesStandard LibrariesCore = $LibrariesCore LibrariesDefault = $LibrariesDefault ModuleName = $ModuleName LibariesPath = $LibariesPath InternalModuleDependencies = $Configuration.Information.Manifest.InternalModuleDependencies CommandModuleDependencies = $Configuration.Information.Manifest.CommandModuleDependencies BinaryModule = $Configuration.Steps.BuildLibraries.BinaryModule Configuration = $Configuration } $Success = New-PSMFile @newPSMFileSplat if ($Success -eq $false) { return $false } $Success = Repair-CustomPlaceHolders -Path $PSM1FilePath -Configuration $Configuration if ($Success -eq $false) { return $false } $Success = Format-Code -FilePath $PSM1FilePath -FormatCode $FormatCodePSM1 if ($Success -eq $false) { return $false } if ($LibariesPath) { $Success = Format-Code -FilePath $LibariesPath -FormatCode $FormatCodePSM1 if ($Success -eq $false) { return $false } } $newPersonalManifestSplat = @{ Configuration = $Configuration ManifestPath = $PSD1FilePath AddUsingsToProcess = $true ScriptsToProcessLibrary = $ScriptsToProcessLibrary OnMerge = $true } if ($Configuration.Steps.BuildLibraries.BinaryModule) { $newPersonalManifestSplat.BinaryModule = $Configuration.Steps.BuildLibraries.BinaryModule } New-PersonalManifest @newPersonalManifestSplat $Success = Format-Code -FilePath $PSD1FilePath -FormatCode $FormatCodePSD1 if ($Success -eq $false) { return $false } Get-ChildItem $ModulePathTarget -Recurse -Force -Directory | Sort-Object -Property FullName -Descending | ` Where-Object { $($_ | Get-ChildItem -Force | Select-Object -First 1).Count -eq 0 } | ` Remove-Item $TimeToExecuteSign.Stop() Write-Text "[+] Finalizing PSM1/PSD1 [Time: $($($TimeToExecuteSign.Elapsed).Tostring())]" -Color Blue } function New-DLLCodeOutput { [CmdletBinding()] param( [string[]] $File, [bool] $DebugDLL, [alias('NETHandleAssemblyWithSameName')][bool] $HandleAssemblyWithSameName ) if ($File.Count -gt 1) { if ($DebugDLL) { @( "`$LibrariesToLoad = @(" foreach ($F in $File) { " '$F'" } ")" "`$FoundErrors = @(" " foreach (`$L in `$LibrariesToLoad) {" " try {" " Add-Type -Path `$PSScriptRoot\`$L -ErrorAction Stop" " } catch [System.Reflection.ReflectionTypeLoadException] {" " Write-Warning `"Processing `$(`$ImportName) Exception: `$(`$_.Exception.Message)`"" " `$LoaderExceptions = `$(`$_.Exception.LoaderExceptions) | Sort-Object -Unique", " foreach (`$E in `$LoaderExceptions) {", " Write-Warning `"Processing `$(`$ImportName) LoaderExceptions: `$(`$E.Message)`"" " }" " `$true" " } catch {" if ($HandleAssemblyWithSameName) { " if (`$_.Exception.Message -like '*Assembly with same name is already loaded*') {" " Write-Warning -Message `"Assembly with same name is already loaded. Ignoring '`$L'.`"" " } else {" " Write-Warning `"Processing `$(`$ImportName) Exception: `$(`$_.Exception.Message)`"", " `$LoaderExceptions = `$(`$_.Exception.LoaderExceptions) | Sort-Object -Unique", " foreach (`$E in `$LoaderExceptions) {" " Write-Warning `"Processing `$(`$ImportName) LoaderExceptions: `$(`$E.Message)`"" " }" " `$true" " }" } else { " Write-Warning `"Processing `$(`$ImportName) Exception: `$(`$_.Exception.Message)`"", " `$LoaderExceptions = `$(`$_.Exception.LoaderExceptions) | Sort-Object -Unique", " foreach (`$E in `$LoaderExceptions) {" " Write-Warning `"Processing `$(`$ImportName) LoaderExceptions: `$(`$E.Message)`"" " }" " `$true" } " }" " }" ")" "if (`$FoundErrors.Count -gt 0) {" " Write-Warning `"Importing module failed. Fix errors before continuing.`"" " break" "}" ) } else { if ($HandleAssemblyWithSameName) { $Output = @( "`$LibrariesToLoad = @(" foreach ($F in $File) { " '$F'" } ")" "foreach (`$L in `$LibrariesToLoad) {" ' try {' ' Add-Type -Path $PSScriptRoot\$L -ErrorAction Stop' ' } catch {' " if (`$_.Exception.Message -like '*Assembly with same name is already loaded*') {" " Write-Warning -Message `"Assembly with same name is already loaded. Ignoring '`$L'.`"" ' } else {' ' throw $_' ' }' ' }' "}" ) } else { $Output = @( "`$LibrariesToLoad = @(" foreach ($F in $File) { " '$F'" } ")" "foreach (`$L in `$LibrariesToLoad) {" " Add-Type -Path `$PSScriptRoot\`$L" "}" ) } } $Output } else { if ($DebugDLL) { $Output = @" `$FoundErrors = @( try { `$ImportName = "`$PSScriptRoot\$File" Add-Type -Path `$ImportName -ErrorAction Stop } catch [System.Reflection.ReflectionTypeLoadException] { Write-Warning "Processing `$(`$ImportName) Exception: `$(`$_.Exception.Message)" `$LoaderExceptions = `$(`$_.Exception.LoaderExceptions) | Sort-Object -Unique foreach (`$E in `$LoaderExceptions) { Write-Warning "Processing `$(`$ImportName) LoaderExceptions: `$(`$E.Message)" } `$true } catch { Write-Warning "Processing `$(`$ImportName) Exception: `$(`$_.Exception.Message)" `$LoaderExceptions = `$(`$_.Exception.LoaderExceptions) | Sort-Object -Unique foreach (`$E in `$LoaderExceptions) { Write-Warning "Processing `$(`$ImportName) LoaderExceptions: `$(`$E.Message)" } `$true } ) if (`$FoundErrors.Count -gt 0) { Write-Warning "Importing module failed. Fix errors before continuing." break } "@ } else { if ($HandleAssemblyWithSameName) { $Output = @" try { Add-Type -Path `$PSScriptRoot\$File -ErrorAction Stop } catch { if (`$_.Exception.Message -like '*Assembly with same name is already loaded*') { Write-Warning -Message `"Assembly with same name is already loaded. Ignoring '`$(`$_.InvocationInfo.Statement)'." } else { throw `$_ } } "@ } else { $Output = 'Add-Type -Path $PSScriptRoot\' + $File } } $Output } } function New-DLLHandleRuntime { [CmdletBinding()] param( [alias('NETHandleRuntimes')][bool] $HandleRuntimes ) if ($HandleRuntimes) { $DataHandleRuntimes = @" if (`$IsWindows) { `$Arch = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture `$ArchFolder = switch (`$Arch) { 'X64' { 'win-x64' } 'X86' { 'win-x86' } 'Arm64' { 'win-arm64' } 'Arm' { 'win-arm' } Default { 'win-x64' } } `$LibFolder = if (`$PSEdition -eq 'Core') { 'Core' } else { 'Default' } `$NativePath = Join-Path -Path `$PSScriptRoot -ChildPath "Lib\`$LibFolder\runtimes\`$ArchFolder\native" if ((Test-Path `$NativePath) -and (`$env:PATH -notlike "*`$NativePath*") ) { # Write-Warning -Message "Adding `$NativePath to PATH" `$env:PATH = "`$NativePath;`$env:PATH" } } "@ $DataHandleRuntimes } } function New-DLLResolveConflict { [CmdletBinding()] param( [string] $ProjectName, [System.Collections.IDictionary] $LibraryConfiguration ) if ($LibraryConfiguration.SearchClass) { $SearchClass = $LibraryConfiguration.SearchClass } else { $SearchClass = "`$LibraryName.Initialize" } if ($ProjectName) { $StandardName = "'$ProjectName'" } else { $StandardName = '$myInvocation.MyCommand.Name.Replace(".psm1", "")' } $Output = @" # Get library name, from the PSM1 file name `$LibraryName = $StandardName `$Library = "`$LibraryName.dll" `$Class = "$SearchClass" `$AssemblyFolders = Get-ChildItem -Path `$PSScriptRoot\Lib -Directory -ErrorAction SilentlyContinue # Lets find which libraries we need to load `$Default = `$false `$Core = `$false `$Standard = `$false foreach (`$A in `$AssemblyFolders.Name) { if (`$A -eq 'Default') { `$Default = `$true } elseif (`$A -eq 'Core') { `$Core = `$true } elseif (`$A -eq 'Standard') { `$Standard = `$true } } if (`$Standard -and `$Core -and `$Default) { `$FrameworkNet = 'Default' `$Framework = 'Standard' } elseif (`$Standard -and `$Core) { `$Framework = 'Standard' `$FrameworkNet = 'Standard' } elseif (`$Core -and `$Default) { `$Framework = 'Core' `$FrameworkNet = 'Default' } elseif (`$Standard -and `$Default) { `$Framework = 'Standard' `$FrameworkNet = 'Default' } elseif (`$Standard) { `$Framework = 'Standard' `$FrameworkNet = 'Standard' } elseif (`$Core) { `$Framework = 'Core' `$FrameworkNet = '' } elseif (`$Default) { `$Framework = '' `$FrameworkNet = 'Default' } else { Write-Error -Message 'No assemblies found' } if (`$PSEdition -eq 'Core') { `$LibFolder = `$Framework } else { `$LibFolder = `$FrameworkNet } try { `$ImportModule = Get-Command -Name Import-Module -Module Microsoft.PowerShell.Core if (-not (`$Class -as [type])) { & `$ImportModule ([IO.Path]::Combine(`$PSScriptRoot, 'Lib', `$LibFolder, `$Library)) -ErrorAction Stop } else { `$Type = "`$Class" -as [Type] & `$importModule -Force -Assembly (`$Type.Assembly) } } catch { if (`$ErrorActionPreference -eq 'Stop') { throw } else { Write-Warning -Message "Importing module `$Library failed. Fix errors before continuing. Error: `$(`$_.Exception.Message)" # we will continue, but it's not a good idea to do so # return } } "@ $Output } function New-LibraryContent { [CmdletBinding()] param( [string[]] $LibrariesStandard, [string[]] $LibrariesCore, [string[]] $LibrariesDefault, [System.Collections.IDictionary] $Configuration, [switch] $OptimizedLoading ) if ($Configuration.Steps.BuildLibraries.HandleAssemblyWithSameName) { $Handle = $Configuration.Steps.BuildLibraries.HandleAssemblyWithSameName } else { $Handle = $false } if ($Configuration.Steps.BuildLibraries.HandleRuntimes) { $HandleRuntimes = $Configuration.Steps.BuildLibraries.HandleRuntimes } else { $HandleRuntimes = $false } $LibrariesDefault = foreach ($Library in $LibrariesDefault) { $SlashCount = ($Library -split '\\').Count if ($SlashCount -gt 3) { continue } $Library } $LibrariesCore = foreach ($Library in $LibrariesCore) { $SlashCount = ($Library -split '\\').Count if ($SlashCount -gt 3) { continue } $Library } $LibrariesStandard = foreach ($Library in $LibrariesStandard) { $SlashCount = ($Library -split '\\').Count if ($SlashCount -gt 3) { continue } $Library } if ($OptimizedLoading) { $LibraryContent = @( if ($LibrariesStandard.Count -gt 0) { $Files = :nextFile foreach ($File in $LibrariesStandard) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) { if ($File -like "*\$IgnoredFile") { continue nextFile } } $File } } $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $Files -HandleAssemblyWithSameName $Handle $Output } elseif ($LibrariesCore.Count -gt 0 -and $LibrariesDefault.Count -gt 0) { 'if ($PSEdition -eq ''Core'') {' if ($LibrariesCore.Count -gt 0) { $Files = :nextFile foreach ($File in $LibrariesCore) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) { if ($File -like "*\$IgnoredFile") { continue nextFile } } $File } } $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $Files -HandleAssemblyWithSameName $Handle $Output } '} else {' if ($LibrariesDefault.Count -gt 0) { $Files = :nextFile foreach ($File in $LibrariesDefault) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) { if ($File -like "*\$IgnoredFile") { continue nextFile } } $File } } $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $Files -HandleAssemblyWithSameName $Handle $Output } '}' } else { if ($LibrariesCore.Count -gt 0) { $Files = :nextFile foreach ($File in $LibrariesCore) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) { if ($File -like "*\$IgnoredFile") { continue nextFile } } $File } } $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $Files -HandleAssemblyWithSameName $Handle $Output } if ($LibrariesDefault.Count -gt 0) { $Files = :nextFile foreach ($File in $LibrariesDefault) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) { if ($File -like "*\$IgnoredFile") { continue nextFile } } $File } } $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $Files -HandleAssemblyWithSameName $Handle $Output } } ) } else { $LibraryContent = @( if ($LibrariesStandard.Count -gt 0) { :nextFile foreach ($File in $LibrariesStandard) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) { if ($File -like "*\$IgnoredFile") { continue nextFile } } $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $File -HandleAssemblyWithSameName $Handle $Output } } } elseif ($LibrariesCore.Count -gt 0 -and $LibrariesDefault.Count -gt 0) { 'if ($PSEdition -eq ''Core'') {' if ($LibrariesCore.Count -gt 0) { :nextFile foreach ($File in $LibrariesCore) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) { if ($File -like "*\$IgnoredFile") { continue nextFile } } $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $File -HandleAssemblyWithSameName $Handle $Output } } } '} else {' if ($LibrariesDefault.Count -gt 0) { :nextFile foreach ($File in $LibrariesDefault) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) { if ($File -like "*\$IgnoredFile") { continue nextFile } } $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $File -HandleAssemblyWithSameName $Handle $Output } } } '}' } else { if ($LibrariesCore.Count -gt 0) { :nextFile foreach ($File in $LibrariesCore) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) { if ($File -like "*\$IgnoredFile") { continue nextFile } } $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $File -HandleAssemblyWithSameName $Handle $Output } } } if ($LibrariesDefault.Count -gt 0) { :nextFile foreach ($File in $LibrariesDefault) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) { if ($File -like "*\$IgnoredFile") { continue nextFile } } $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $File -HandleAssemblyWithSameName $Handle $Output } } } } ) } if ($HandleRuntimes) { $LibraryContent = @( New-DLLHandleRuntime -HandleRuntimes $HandleRuntimes $LibraryContent ) } $LibraryContent } function New-PersonalManifest { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [string] $ManifestPath, [switch] $AddScriptsToProcess, [switch] $AddUsingsToProcess, [string] $ScriptsToProcessLibrary, [switch] $UseWildcardForFunctions, [switch] $OnMerge, [string[]] $BinaryModule ) $TemporaryManifest = [ordered] @{ } $Manifest = $Configuration.Information.Manifest if ($UseWildcardForFunctions) { $Manifest.FunctionsToExport = @("*") $Manifest.AliasesToExport = @("*") } if ($BinaryModule.Count -gt 0) { if ($null -eq $Manifest.CmdletsToExport) { $Data.CmdletsToExport = @("*") } } $Manifest.Path = $ManifestPath if (-not $AddScriptsToProcess) { $Manifest.ScriptsToProcess = @() } if ($AddUsingsToProcess -and $Configuration.UsingInPlace -and -not $ScriptsToProcessLibrary) { $Manifest.ScriptsToProcess = @($Configuration.UsingInPlace) } elseif ($AddUsingsToProcess -and $Configuration.UsingInPlace -and $ScriptsToProcessLibrary) { $Manifest.ScriptsToProcess = @($Configuration.UsingInPlace, $ScriptsToProcessLibrary) } elseif ($ScriptsToProcessLibrary) { $Manifest.ScriptsToProcess = @($ScriptsToProcessLibrary) } if ($Manifest.Contains('ExternalModuleDependencies')) { $TemporaryManifest.ExternalModuleDependencies = $Manifest.ExternalModuleDependencies $Manifest.Remove('ExternalModuleDependencies') } if ($Manifest.Contains('InternalModuleDependencies')) { $TemporaryManifest.InternalModuleDependencies = $Manifest.InternalModuleDependencies $Manifest.Remove('InternalModuleDependencies') } if ($Manifest.Contains('CommandModuleDependencies')) { $TemporaryManifest.CommandModuleDependencies = $Manifest.CommandModuleDependencies $Manifest.Remove('CommandModuleDependencies') } if ($Manifest.PreRelease) { $Configuration.CurrentSettings.PreRelease = $Manifest.PreRelease } if ($OnMerge) { if ($Configuration.Options.Merge.Style.PSD1) { $PSD1Style = $Configuration.Options.Merge.Style.PSD1 } } else { if ($Configuration.Options.Standard.Style.PSD1) { $PSD1Style = $Configuration.Options.Standard.Style.PSD1 } } if (-not $PSD1Style) { if ($Configuration.Options.Style.PSD1) { $PSD1Style = $Configuration.Options.Style.PSD1 } else { $PSD1Style = 'Minimal' } } if ($PSD1Style -eq 'Native' -and $Configuration.Steps.PublishModule.Prerelease -eq '' -and (-not $TemporaryManifest.ExternalModuleDependencies)) { if ($Manifest.ModuleVersion) { New-ModuleManifest @Manifest } else { Write-Text -Text '[-] Module version is not available. Terminating.' -Color Red return $false } Write-TextWithTime -Text "[i] Converting $($ManifestPath) UTF8 without BOM" { (Get-Content -Path $ManifestPath -Raw -Encoding utf8) | Out-FileUtf8NoBom $ManifestPath } } else { if ($PSD1Style -eq 'Native') { Write-Text -Text '[-] Native PSD1 style is not available when using PreRelease or ExternalModuleDependencies. Switching to Minimal.' -Color Yellow } $Data = $Manifest $Data.PrivateData = @{ PSData = [ordered]@{} } if ($Data.Path) { $Data.Remove('Path') } $ValidateEntriesPrivateData = @('Tags', 'LicenseUri', 'ProjectURI', 'IconUri', 'ReleaseNotes', 'Prerelease', 'RequireLicenseAcceptance', 'ExternalModuleDependencies') foreach ($Entry in [string[]] $Data.Keys) { if ($Entry -in $ValidateEntriesPrivateData) { $Data.PrivateData.PSData.$Entry = $Data.$Entry $Data.Remove($Entry) } } $ValidDataEntries = @('ModuleToProcess', 'NestedModules', 'GUID', 'Author', 'CompanyName', 'Copyright', 'ModuleVersion', 'Description', 'PowerShellVersion', 'PowerShellHostName', 'PowerShellHostVersion', 'CLRVersion', 'DotNetFrameworkVersion', 'ProcessorArchitecture', 'RequiredModules', 'TypesToProcess', 'FormatsToProcess', 'ScriptsToProcess', 'PrivateData', 'RequiredAssemblies', 'ModuleList', 'FileList', 'FunctionsToExport', 'VariablesToExport', 'AliasesToExport', 'CmdletsToExport', 'DscResourcesToExport', 'CompatiblePSEditions', 'HelpInfoURI', 'RootModule', 'DefaultCommandPrefix') foreach ($Entry in [string[]] $Data.Keys) { if ($Entry -notin $ValidDataEntries) { Write-Text -Text "[-] Removing wrong entries from PSD1 - $Entry" -Color Red $Data.Remove($Entry) } } foreach ($Entry in [string[]] $Data.PrivateData.PSData.Keys) { if ($Entry -notin $ValidateEntriesPrivateData) { Write-Text -Text "[-] Removing wrong entries from PSD1 Private Data - $Entry" -Color Red $Data.PrivateData.PSData.Remove($Entry) } } if ($Configuration.Steps.PublishModule.Prerelease) { $Data.PrivateData.PSData.Prerelease = $Configuration.Steps.PublishModule.Prerelease } if ($TemporaryManifest.ExternalModuleDependencies) { $Data.PrivateData.PSData.ExternalModuleDependencies = $TemporaryManifest.ExternalModuleDependencies $Data.RequiredModules = @( foreach ($Module in $Manifest.RequiredModules) { if ($Module -is [System.Collections.IDictionary]) { $Module = [ordered] @{ ModuleName = $Module.ModuleName ModuleVersion = $Module.ModuleVersion Guid = $Module.Guid } Remove-EmptyValue -Hashtable $Module $Module } else { $Module } } foreach ($Module in $TemporaryManifest.ExternalModuleDependencies) { if ($Module -is [System.Collections.IDictionary]) { $Module = [ordered] @{ ModuleName = $Module.ModuleName ModuleVersion = $Module.ModuleVersion Guid = $Module.Guid } Remove-EmptyValue -Hashtable $Module $Module } else { $Module } } ) } if (-not $Data.RequiredModules) { $Data.Remove('RequiredModules') } $Data | Export-PSData -DataFile $ManifestPath -Sort } } function New-PrepareManifest { [CmdletBinding()] param( [string] $ProjectName, [string] $ModulePath, [string] $ProjectPath, $FunctionToExport, [string] $ProjectUrl ) $Location = [System.IO.Path]::Combine($projectPath, $ProjectName) Set-Location -Path $Location $manifest = @{ Path = ".\$ProjectName.psd1" RootModule = "$ProjectName.psm1" Author = 'Przemyslaw Klys' CompanyName = 'Evotec' Copyright = 'Evotec (c) 2011-2022. All rights reserved.' Description = "Simple project" FunctionsToExport = $functionToExport CmdletsToExport = '' VariablesToExport = '' AliasesToExport = '' FileList = "$ProjectName.psm1", "$ProjectName.psd1" HelpInfoURI = $projectUrl ProjectUri = $projectUrl } New-ModuleManifest @manifest } function New-PrepareStructure { [CmdletBinding()] param( [System.Collections.IDictionary]$Configuration, [scriptblock] $Settings, [string] $PathToProject, [string] $ModuleName, [string] $FunctionsToExportFolder, [string] $AliasesToExportFolder, [string[]] $ExcludeFromPackage, [string[]] $IncludeRoot, [string[]] $IncludePS1, [string[]] $IncludeAll, [scriptblock] $IncludeCustomCode, [System.Collections.IDictionary] $IncludeToArray, [string] $LibrariesCore, [string] $LibrariesDefault, [string] $LibrariesStandard ) if (-not $Configuration) { $Configuration = [ordered] @{} } if (-not $Configuration.Information) { $Configuration.Information = [ordered] @{} } if (-not $Configuration.Information.Manifest) { $PathToPSD1 = [io.path]::Combine($PathToProject, $ModuleName + '.psd1') if (Test-Path -LiteralPath $PathToPSD1) { try { $Configuration.Information.Manifest = Import-PowerShellDataFile -Path $PathToPSD1 -ErrorAction Stop $Configuration.Information.Manifest.RequiredModules = $null $Configuration.Information.Manifest.FunctionsToExport = $null $Configuration.Information.Manifest.AliasesToExport = $null $Configuration.Information.Manifest.CmdletsToExport = $null } catch { Write-Text "[-] Reading $PathToPSD1 failed. Error: $($_.Exception.Message)" -Color Red Write-Text "[+] Building $PathToPSD1 from scratch." -Color Yellow $Configuration.Information.Manifest = [ordered] @{} } } else { $Configuration.Information.Manifest = [ordered] @{} } } if (-not $Configuration.Information.DirectoryModulesCore) { if ($IsWindows) { $PathCore = [io.path]::Combine($([Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments)), "PowerShell", 'Modules') } else { $PathCore = [io.path]::Combine($env:HOME, ".local", "share", "powershell", "Modules") } $Configuration.Information.DirectoryModulesCore = $PathCore } if (-not $Configuration.Information.DirectoryModules) { if ($IsWindows) { $PathStandard = [io.path]::Combine($([Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments)), "WindowsPowerShell", 'Modules') } else { $PathStandard = $Configuration.Information.DirectoryModulesCore } $Configuration.Information.DirectoryModules = $PathStandard } if (-not $Configuration.CurrentSettings) { $Configuration.CurrentSettings = [ordered] @{} } if (-not $Configuration.CurrentSettings['Artefact']) { $Configuration.CurrentSettings['Artefact'] = @() } if ($ModuleName) { $Configuration.Information.ModuleName = $ModuleName } if ($PSBoundParameters.ContainsKey('ExcludeFromPackage')) { $Configuration.Information.Exclude = $ExcludeFromPackage } if ($PSBoundParameters.ContainsKey('IncludeRoot')) { $Configuration.Information.IncludeRoot = $IncludeRoot } if ($PSBoundParameters.ContainsKey('IncludePS1')) { $Configuration.Information.IncludePS1 = $IncludePS1 } if ($PSBoundParameters.ContainsKey('IncludeAll')) { $Configuration.Information.IncludeAll = $IncludeAll } if ($PSBoundParameters.ContainsKey('IncludeCustomCode')) { $Configuration.Information.IncludeCustomCode = $IncludeCustomCode } if ($PSBoundParameters.ContainsKey('IncludeToArray')) { $Configuration.Information.IncludeToArray = $IncludeToArray } if ($PSBoundParameters.ContainsKey('LibrariesCore')) { $Configuration.Information.LibrariesCore = $LibrariesCore } if ($PSBoundParameters.ContainsKey('LibrariesDefault')) { $Configuration.Information.LibrariesDefault = $LibrariesDefault } if ($PSBoundParameters.ContainsKey('LibrariesStandard')) { $Configuration.Information.LibrariesStandard = $LibrariesStandard } if ($FunctionsToExportFolder) { $Configuration.Information.FunctionsToExport = $FunctionsToExportFolder } if ($AliasesToExportFolder) { $Configuration.Information.AliasesToExport = $AliasesToExportFolder } if (-not $Configuration.Options) { $Configuration.Options = [ordered] @{} } if (-not $Configuration.Options.Merge) { $Configuration.Options.Merge = [ordered] @{} } if (-not $Configuration.Options.Merge.Integrate) { $Configuration.Options.Merge.Integrate = [ordered] @{} } if (-not $Configuration.Options.Standard) { $Configuration.Options.Standard = [ordered] @{} } if (-not $Configuration.Options.Signing) { $Configuration.Options.Signing = [ordered] @{} } if (-not $Configuration.Steps) { $Configuration.Steps = [ordered] @{} } if (-not $Configuration.Steps.PublishModule) { $Configuration.Steps.PublishModule = [ordered] @{} } if (-not $Configuration.Steps.ImportModules) { $Configuration.Steps.ImportModules = [ordered] @{} } if (-not $Configuration.Steps.BuildModule) { $Configuration.Steps.BuildModule = [ordered] @{} } if (-not $Configuration.Steps.BuildModule.Releases) { $Configuration.Steps.BuildModule.Releases = [ordered] @{} } if (-not $Configuration.Steps.BuildModule.ReleasesUnpacked) { $Configuration.Steps.BuildModule.ReleasesUnpacked = [ordered] @{} } if (-not $Configuration.Steps.BuildLibraries) { $Configuration.Steps.BuildLibraries = [ordered] @{} } if (-not $Configuration.Information.Manifest.CommandModuleDependencies) { $Configuration.Information.Manifest.CommandModuleDependencies = [ordered] @{} } if (-not $Configuration.Steps.BuildModule.Artefacts) { $Configuration.Steps.BuildModule.Artefacts = [System.Collections.Generic.List[System.Collections.IDictionary]]::new() } if (-not $Configuration.Steps.BuildModule.GitHubNugets) { $Configuration.Steps.BuildModule.GitHubNugets = [System.Collections.Generic.List[System.Collections.IDictionary]]::new() } if (-not $Configuration.Steps.BuildModule.GalleryNugets) { $Configuration.Steps.BuildModule.GalleryNugets = [System.Collections.Generic.List[System.Collections.IDictionary]]::new() } $Configuration.Information.Manifest.RootModule = "$($ModuleName).psm1" if (-not $Configuration.PlaceHolder) { $Configuration.PlaceHolder = [System.Collections.Generic.List[System.Collections.IDictionary]]::new() } if (-not $Configuration.PlaceHolderOption) { $Configuration.PlaceHolderOption = [ordered] @{} } Write-TextWithTime -Text "Reading configuration" { if ($Settings) { $ExecutedSettings = & $Settings foreach ($Setting in $ExecutedSettings) { if ($Setting.Type -eq 'RequiredModule') { if ($Configuration.Information.Manifest.RequiredModules -isnot [System.Collections.Generic.List[System.Object]]) { $Configuration.Information.Manifest.RequiredModules = [System.Collections.Generic.List[System.Object]]::new() } $Configuration.Information.Manifest.RequiredModules.Add($Setting.Configuration) } elseif ($Setting.Type -eq 'ExternalModule') { if ($Configuration.Information.Manifest.ExternalModuleDependencies -isnot [System.Collections.Generic.List[System.Object]]) { $Configuration.Information.Manifest.ExternalModuleDependencies = [System.Collections.Generic.List[System.Object]]::new() } $Configuration.Information.Manifest.ExternalModuleDependencies.Add($Setting.Configuration) } elseif ($Setting.Type -eq 'ApprovedModule') { if ($Configuration.Options.Merge.Integrate.ApprovedModules -isnot [System.Collections.Generic.List[System.Object]]) { $Configuration.Options.Merge.Integrate.ApprovedModules = [System.Collections.Generic.List[System.Object]]::new() } $Configuration.Options.Merge.Integrate.ApprovedModules.Add($Setting.Configuration) } elseif ($Setting.Type -eq 'ModuleSkip') { $Configuration.Options.Merge.ModuleSkip = $Setting.Configuration } elseif ($Setting.Type -eq 'Manifest') { foreach ($Key in $Setting.Configuration.Keys) { $Configuration.Information.Manifest[$Key] = $Setting.Configuration[$Key] } } elseif ($Setting.Type -eq 'Information') { foreach ($Key in $Setting.Configuration.Keys) { $Configuration.Information[$Key] = $Setting.Configuration[$Key] } } elseif ($Setting.Type -eq 'Formatting') { foreach ($Key in $Setting.Options.Keys) { if (-not $Configuration.Options[$Key]) { $Configuration.Options[$Key] = [ordered] @{} } foreach ($Entry in $Setting.Options[$Key].Keys) { $Configuration.Options[$Key][$Entry] = $Setting.Options[$Key][$Entry] } } } elseif ($Setting.Type -eq 'Command') { $Configuration.Information.Manifest.CommandModuleDependencies[$Setting.Configuration.ModuleName] = @($Setting.Configuration.CommandName) } elseif ($Setting.Type -eq 'Documentation') { $Configuration.Options.Documentation = $Setting.Configuration } elseif ($Setting.Type -eq 'BuildDocumentation') { $Configuration.Steps.BuildDocumentation = $Setting.Configuration } elseif ($Setting.Type -eq 'TestsBeforeMerge') { $Configuration.Options.TestsBeforeMerge = $Setting.Configuration } elseif ($Setting.Type -eq 'TestsAfterMerge') { $Configuration.Options.TestsAfterMerge = $Setting.Configuration } elseif ($Setting.Type -eq 'GitHubPublishing') { $Configuration.Steps.BuildModule.Nugets.Add($Setting.Configuration) } elseif ($Setting.Type -eq 'ImportModules') { foreach ($Key in $Setting.ImportModules.Keys) { $Configuration.Steps.ImportModules[$Key] = $Setting.ImportModules[$Key] } } elseif ($Setting.Type -in 'GalleryNuget') { $Configuration.Steps.BuildModule.GalleryNugets.Add($Setting.Configuration) } elseif ($Setting.Type -in 'GitHubNuget') { $Configuration.Steps.BuildModule.GitHubNugets.Add($Setting.Configuration) } elseif ($Setting.Type -in 'Unpacked', 'Packed', 'Script', 'ScriptPacked') { $Configuration.Steps.BuildModule.Artefacts.Add($Setting.Configuration) } elseif ($Setting.Type -eq 'Build') { foreach ($Key in $Setting.BuildModule.Keys) { $Configuration.Steps.BuildModule[$Key] = $Setting.BuildModule[$Key] } } elseif ($Setting.Type -eq 'BuildLibraries') { foreach ($Key in $Setting.BuildLibraries.Keys) { $Configuration.Steps.BuildLibraries[$Key] = $Setting.BuildLibraries[$Key] } } elseif ($Setting.Type -eq 'Options') { foreach ($Key in $Setting.Options.Keys) { if (-not $Configuration.Options[$Key]) { $Configuration.Options[$Key] = [ordered] @{} } foreach ($Entry in $Setting.Options[$Key].Keys) { $Configuration.Options[$Key][$Entry] = $Setting.Options[$Key][$Entry] } } } elseif ($Setting.Type -eq 'PlaceHolder') { $Configuration.PlaceHolder.Add($Setting.Configuration) } elseif ($Setting.Type -eq 'PlaceHolderOption') { foreach ($Key in $Setting.PlaceHolderOption.Keys) { $Configuration.PlaceHolderOption[$Key] = $Setting.PlaceHolderOption[$Key] } } } } } -PreAppend Information if (-not $Configuration.Options.Merge.Sort) { $Configuration.Options.Merge.Sort = 'None' } if (-not $Configuration.Options.Standard.Sort) { $Configuration.Options.Standard.Sort = 'None' } $Success = Start-ModuleBuilding -Configuration $Configuration -PathToProject $PathToProject if ($Success -contains $false) { return $false } } function New-PSMFile { [cmdletbinding()] param( [string] $Path, [string[]] $FunctionNames, [string[]] $FunctionAliaes, [System.Collections.IDictionary] $AliasesAndFunctions, [System.Collections.IDictionary] $CmdletsAliases, [Array] $LibrariesStandard, [Array] $LibrariesCore, [Array] $LibrariesDefault, [string] $ModuleName, # [switch] $UsingNamespaces, [string] $LibariesPath, [Array] $InternalModuleDependencies, [System.Collections.IDictionary] $CommandModuleDependencies, [string[]] $BinaryModule, [System.Collections.IDictionary] $Configuration ) if ($PSVersionTable.PSVersion.Major -gt 5) { $Encoding = 'UTF8BOM' } else { $Encoding = 'UTF8' } Write-TextWithTime -Text "Adding alises/functions to load in PSM1 file - $Path" -PreAppend Plus { if ($FunctionNames.Count -gt 0) { $Functions = ($FunctionNames | Sort-Object -Unique) -join "','" $Functions = "'$Functions'" } else { $Functions = @() } if ($FunctionAliaes.Count -gt 0) { $Aliases = ($FunctionAliaes | Sort-Object -Unique) -join "','" $Aliases = "'$Aliases'" } else { $Aliases = @() } "" | Out-File -Append -LiteralPath $Path -Encoding $Encoding -ErrorAction Stop if ($InternalModuleDependencies.Count -gt 0) { @( "# Added internal module loading to cater for special cases " "" ) | Out-File -Append -LiteralPath $Path -Encoding $Encoding $ModulesText = "'$($InternalModuleDependencies -join "','")'" @" `$ModulesOptional = $ModulesText foreach (`$Module in `$ModulesOptional) { Import-Module -Name `$Module -ErrorAction SilentlyContinue } "@ | Out-File -Append -LiteralPath $Path -Encoding $Encoding -ErrorAction Stop } if ($Configuration.Information.Manifest.CmdletsToExport) { $Cmdlet = ($Configuration.Information.Manifest.CmdletsToExport | Sort-Object -Unique) -join "','" $Cmdlet = "'$Cmdlet'" } else { if ($BinaryModule.Count -gt 0) { $Cmdlet = '"*"' } } if ($CommandModuleDependencies -and $CommandModuleDependencies.Keys.Count -gt 0) { @( "`$ModuleFunctions = @{" foreach ($Module in $CommandModuleDependencies.Keys) { "$Module = @{" foreach ($Command in $($CommandModuleDependencies[$Module])) { $Alias = "'$($AliasesAndFunctions[$Command] -join "','")'" " '$Command' = $Alias" } "}" } "}" @" [Array] `$FunctionsAll = $Functions [Array] `$AliasesAll = $Aliases `$AliasesToRemove = [System.Collections.Generic.List[string]]::new() `$FunctionsToRemove = [System.Collections.Generic.List[string]]::new() foreach (`$Module in `$ModuleFunctions.Keys) { try { Import-Module -Name `$Module -ErrorAction Stop } catch { foreach (`$Function in `$ModuleFunctions[`$Module].Keys) { `$FunctionsToRemove.Add(`$Function) `$ModuleFunctions[`$Module][`$Function] | ForEach-Object { if (`$_) { `$AliasesToRemove.Add(`$_) } } } } } `$FunctionsToLoad = foreach (`$Function in `$FunctionsAll) { if (`$Function -notin `$FunctionsToRemove) { `$Function } } `$AliasesToLoad = foreach (`$Alias in `$AliasesAll) { if (`$Alias -notin `$AliasesToRemove) { `$Alias } } # Export functions and aliases as required "@ ) | Out-File -Append -LiteralPath $Path -Encoding $Encoding if ($Cmdlet) { "Export-ModuleMember -Function @(`$FunctionsToLoad) -Alias @(`$AliasesToLoad) -Cmdlet $Cmdlet" | Out-File -Append -LiteralPath $Path -Encoding $Encoding -ErrorAction Stop } else { "Export-ModuleMember -Function @(`$FunctionsToLoad) -Alias @(`$AliasesToLoad)" | Out-File -Append -LiteralPath $Path -Encoding $Encoding -ErrorAction Stop } } else { "# Export functions and aliases as required" | Out-File -Append -LiteralPath $Path -Encoding $Encoding if ($Cmdlet) { "Export-ModuleMember -Function @($Functions) -Alias @($Aliases) -Cmdlet $Cmdlet" | Out-File -Append -LiteralPath $Path -Encoding $Encoding -ErrorAction Stop } else { "Export-ModuleMember -Function @($Functions) -Alias @($Aliases)" | Out-File -Append -LiteralPath $Path -Encoding $Encoding -ErrorAction Stop } } } -SpacesBefore ' ' } function Out-FileUtf8NoBom { <# .SYNOPSIS Outputs to a UTF-8-encoded file *without a BOM* (byte-order mark). .DESCRIPTION Mimics the most important aspects of Out-File: * Input objects are sent to Out-String first. * -Append allows you to append to an existing file, -NoClobber prevents overwriting of an existing file. * -Width allows you to specify the line width for the text representations of input objects that aren't strings. However, it is not a complete implementation of all Out-String parameters: * Only a literal output path is supported, and only as a parameter. * -Force is not supported. Caveat: *All* pipeline input is buffered before writing output starts, but the string representations are generated and written to the target file one by one. .NOTES The raison d'être for this advanced function is that, as of PowerShell v5, Out-File still lacks the ability to write UTF-8 files without a BOM: using -Encoding UTF8 invariably prepends a BOM. #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string] $LiteralPath, [switch] $Append, [switch] $NoClobber, [AllowNull()] [int] $Width, [Parameter(ValueFromPipeline)] $InputObject ) [System.IO.Directory]::SetCurrentDirectory($PWD) $LiteralPath = [IO.Path]::GetFullPath($LiteralPath) if ($NoClobber -and (Test-Path $LiteralPath)) { throw [IO.IOException] "The file '$LiteralPath' already exists." } $sw = New-Object IO.StreamWriter $LiteralPath, $Append $htOutStringArgs = @{} if ($Width) { $htOutStringArgs += @{ Width = $Width } } try { $Input | Out-String -Stream @htOutStringArgs | ForEach-Object { $sw.WriteLine($_) } } finally { $sw.Dispose() } } function Register-DataForInitialModule { [CmdletBinding()] param( [Parameter(Mandatory)][string] $FilePath, [Parameter(Mandatory)][string] $ModuleName, [Parameter(Mandatory)][string] $Guid ) if ($PSVersionTable.PSVersion.Major -gt 5) { $Encoding = 'UTF8BOM' } else { $Encoding = 'UTF8' } try { $BuildModule = Get-Content -Path $FilePath -Raw -ErrorAction Stop } catch { Write-Text -Text "[-] Couldn't read $FilePath, error: $($_.Exception.Message)" -Color Red return $false } $BuildModule = $BuildModule -replace "\`$GUID", $Guid $BuildModule = $BuildModule -replace "\`$ModuleName", $ModuleName try { Set-Content -Path $FilePath -Value $BuildModule -Encoding $Encoding -ErrorAction Stop } catch { Write-Text -Text "[-] Couldn't save $FilePath, error: $($_.Exception.Message)" -Color Red return $false } } function Remove-Directory { [CmdletBinding()] param ( [string] $Directory, [string] $SpacesBefore ) if ($Directory) { $Exists = Test-Path -LiteralPath $Directory if ($Exists) { try { Remove-Item -Path $Directory -ErrorAction Stop -Force -Recurse -Confirm:$false } catch { $ErrorMessage = $_.Exception.Message Write-Text "Can't delete folder $Directory. Fix error before continuing: $ErrorMessage" -PreAppend Error -SpacesBefore $SpacesBefore return $false } } } } function Remove-EmptyLines { [CmdletBinding(DefaultParameterSetName = 'FilePath')] param( [Parameter(Mandatory, ParameterSetName = 'FilePath')] [alias('FilePath', 'Path', 'LiteralPath')][string] $SourceFilePath, [Parameter(Mandatory, ParameterSetName = 'Content')][string] $Content, [Parameter(ParameterSetName = 'Content')] [Parameter(ParameterSetName = 'FilePath')] [alias('Destination')][string] $DestinationFilePath, [Parameter(ParameterSetName = 'Content')] [Parameter(ParameterSetName = 'FilePath')] [switch] $RemoveAllEmptyLines, [Parameter(ParameterSetName = 'Content')] [Parameter(ParameterSetName = 'FilePath')] [switch] $RemoveEmptyLines ) if ($PSVersionTable.PSVersion.Major -gt 5) { $Encoding = 'UTF8BOM' } else { $Encoding = 'UTF8' } if ($SourceFilePath) { $Fullpath = Resolve-Path -LiteralPath $SourceFilePath $Content = [IO.File]::ReadAllText($FullPath) } if ($RemoveEmptyLines) { $Content = $Content -replace '(?m)^\s*$', '' } if ($RemoveAllEmptyLines) { $Content = $Content -replace '(?m)^\s*$(\r?\n)?', '' } if ($Content) { $Content = $Content.Trim() } if ($DestinationFilePath) { $Content | Set-Content -Path $DestinationFilePath -Encoding $Encoding } else { $Content } } function Remove-EmptyValue { [alias('Remove-EmptyValues')] [CmdletBinding()] param( [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable, [string[]] $ExcludeParameter, [switch] $Recursive, [int] $Rerun, [switch] $DoNotRemoveNull, [switch] $DoNotRemoveEmpty, [switch] $DoNotRemoveEmptyArray, [switch] $DoNotRemoveEmptyDictionary ) foreach ($Key in [string[]] $Hashtable.Keys) { if ($Key -notin $ExcludeParameter) { if ($Recursive) { if ($Hashtable[$Key] -is [System.Collections.IDictionary]) { if ($Hashtable[$Key].Count -eq 0) { if (-not $DoNotRemoveEmptyDictionary) { $Hashtable.Remove($Key) } } else { Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } } if ($Rerun) { for ($i = 0; $i -lt $Rerun; $i++) { Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive } } } function Remove-ItemAlternative { <# .SYNOPSIS Removes all files and folders within given path .DESCRIPTION Removes all files and folders within given path. Workaround for Access to the cloud file is denied issue .PARAMETER Path Path to file/folder .PARAMETER SkipFolder Do not delete top level folder .PARAMETER Exclude Skip files/folders matching given pattern .EXAMPLE Remove-ItemAlternative -Path "C:\Support\GitHub\GpoZaurr\Docs" .EXAMPLE Remove-ItemAlternative -Path "C:\Support\GitHub\GpoZaurr\Docs" .NOTES General notes #> [cmdletbinding()] param( [alias('LiteralPath')][string] $Path, [switch] $SkipFolder, [string[]] $Exclude ) if ($Path -and (Test-Path -LiteralPath $Path)) { $getChildItemSplat = @{ Path = $Path Recurse = $true Force = $true File = $true Exclude = $Exclude } Remove-EmptyValue -Hashtable $getChildItemSplat $Items = Get-ChildItem @getChildItemSplat foreach ($Item in $Items) { try { $Item.Delete() } catch { if ($ErrorActionPreference -eq 'Stop') { throw "Couldn't delete $($Item.FullName), error: $($_.Exception.Message)" } else { Write-Warning "Remove-ItemAlternative - Couldn't delete $($Item.FullName), error: $($_.Exception.Message)" } } } $getChildItemSplat = @{ Path = $Path Recurse = $true Force = $true Exclude = $Exclude } Remove-EmptyValue -Hashtable $getChildItemSplat $Items = Get-ChildItem @getChildItemSplat | Sort-Object -Descending -Property 'FullName' foreach ($Item in $Items) { try { $Item.Delete() } catch { if ($ErrorActionPreference -eq 'Stop') { throw "Couldn't delete $($Item.FullName), error: $($_.Exception.Message)" } else { Write-Warning "Remove-ItemAlternative - Couldn't delete $($Item.FullName), error: $($_.Exception.Message)" } } } if (-not $SkipFolder.IsPresent) { $Item = Get-Item -LiteralPath $Path try { $Item.Delete($true) } catch { if ($ErrorActionPreference -eq 'Stop') { throw "Couldn't delete $($Item.FullName), error: $($_.Exception.Message)" } else { Write-Warning "Remove-ItemAlternative - Couldn't delete $($Item.FullName), error: $($_.Exception.Message)" } } } } else { if ($ErrorActionPreference -eq 'Stop') { throw "Remove-ItemAlternative - Path $Path doesn't exists. Skipping." } else { Write-Warning "Remove-ItemAlternative - Path $Path doesn't exists. Skipping." } } } function Repair-CustomPlaceHolders { [CmdletBinding()] param( [Parameter(Mandatory)][string] $Path, [System.Collections.IDictionary] $Configuration ) $ModuleName = $Configuration.Information.ModuleName $ModuleVersion = $Configuration.Information.Manifest.ModuleVersion $TagName = "v$($ModuleVersion)" if ($Configuration.CurrentSettings.PreRelease) { $ModuleVersionWithPreRelease = "$($ModuleVersion)-$($Configuration.CurrentSettings.PreRelease)" $TagModuleVersionWithPreRelease = "v$($ModuleVersionWithPreRelease)" } else { $ModuleVersionWithPreRelease = $ModuleVersion $TagModuleVersionWithPreRelease = "v$($ModuleVersion)" } $BuiltinPlaceHolders = @( @{ Find = '{ModuleName}'; Replace = $ModuleName } @{ Find = '<ModuleName>'; Replace = $ModuleName } @{ Find = '{ModuleVersion}'; Replace = $ModuleVersion } @{ Find = '<ModuleVersion>'; Replace = $ModuleVersion } @{ Find = '{ModuleVersionWithPreRelease}'; Replace = $ModuleVersionWithPreRelease } @{ Find = '<ModuleVersionWithPreRelease>'; Replace = $ModuleVersionWithPreRelease } @{ Find = '{TagModuleVersionWithPreRelease}'; Replace = $TagModuleVersionWithPreRelease } @{ Find = '<TagModuleVersionWithPreRelease>'; Replace = $TagModuleVersionWithPreRelease } @{ Find = '{TagName}'; Replace = $TagName } @{ Find = '<TagName>'; Replace = $TagName } ) Write-TextWithTime -Text "Replacing built-in and custom placeholders" -Color Yellow { $PSM1Content = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 -ErrorAction Stop if ($Configuration.PlaceHolderOption.SkipBuiltinReplacements -ne $true) { foreach ($PlaceHolder in $BuiltinPlaceHolders) { $PSM1Content = $PSM1Content.Replace($PlaceHolder.Find, $PlaceHolder.Replace) } } foreach ($PlaceHolder in $Configuration.PlaceHolders) { $PSM1Content = $PSM1Content.Replace($PlaceHolder.Find, $PlaceHolder.Replace) } Set-Content -LiteralPath $Path -Value $PSM1Content -Encoding UTF8 -ErrorAction Stop -Force } -PreAppend Plus -SpacesBefore ' ' } function Resolve-Encoding { <# .SYNOPSIS Resolves encoding names to .NET System.Text.Encoding objects. .DESCRIPTION Converts encoding name strings to proper .NET encoding objects, with special handling for UTF8BOM (UTF8 with BOM) and OEM encodings that aren't directly available in System.Text.Encoding static properties. .PARAMETER Name The name of the encoding to resolve. Supports common encodings used in text file processing. 'Any' is a special value that returns null - used for converting from any encoding. .EXAMPLE Resolve-Encoding -Name 'UTF8BOM' Returns a UTF8Encoding object configured to emit a BOM. .EXAMPLE Resolve-Encoding -Name 'ASCII' Returns the ASCII encoding object. .EXAMPLE Resolve-Encoding -Name 'Any' Returns null - used to indicate "any source encoding" in conversion functions. #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM', 'Any')] [string] $Name ) switch ($Name.ToUpperInvariant()) { 'UTF8BOM' { return [System.Text.UTF8Encoding]::new($true) } 'UTF8' { return [System.Text.UTF8Encoding]::new($false) } 'OEM' { return [System.Text.Encoding]::GetEncoding([Console]::OutputEncoding.CodePage) } 'ANY' { return $null } default { try { return [System.Text.Encoding]::$Name } catch { throw "Failed to resolve encoding '$Name': $($_.Exception.Message)" } } } } $Script:FormatterSettings = @{ IncludeRules = @( 'PSPlaceOpenBrace', 'PSPlaceCloseBrace', 'PSUseConsistentWhitespace', 'PSUseConsistentIndentation', 'PSAlignAssignmentStatement', 'PSUseCorrectCasing' ) Rules = @{ PSPlaceOpenBrace = @{ Enable = $true OnSameLine = $true NewLineAfter = $true IgnoreOneLineBlock = $true } PSPlaceCloseBrace = @{ Enable = $true NewLineAfter = $false IgnoreOneLineBlock = $true NoEmptyLineBefore = $false } PSUseConsistentIndentation = @{ Enable = $true Kind = 'space' PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' IndentationSize = 4 } PSUseConsistentWhitespace = @{ Enable = $true CheckInnerBrace = $true CheckOpenBrace = $true CheckOpenParen = $true CheckOperator = $true CheckPipe = $true CheckSeparator = $true } PSAlignAssignmentStatement = @{ Enable = $true CheckHashtable = $true } PSUseCorrectCasing = @{ Enable = $true } } } function Send-FilesToGitHubRelease { [cmdletbinding()] param( [string[]] $filePathsToUpload, [string] $urlToUploadFilesTo, $authHeader ) [int] $numberOfFilesToUpload = $filePathsToUpload.Count [int] $numberOfFilesUploaded = 0 $filePathsToUpload | ForEach-Object { $filePath = $_ $fileName = Get-Item $filePath | Select-Object -ExpandProperty Name $uploadAssetWebRequestParameters = @{ Uri = $urlToUploadFilesTo + "?name=$fileName" Method = 'POST' Headers = $authHeader ContentType = 'application/zip' InFile = $filePath } $numberOfFilesUploaded = $numberOfFilesUploaded + 1 Write-Verbose "Uploading asset $numberOfFilesUploaded of $numberOfFilesToUpload, '$filePath'." Invoke-RestMethodAndThrowDescriptiveErrorOnFailure $uploadAssetWebRequestParameters > $null } } function Start-ArtefactsBuilding { [CmdletBinding()] param( [System.Collections.IDictionary] $ChosenArtefact, [System.Collections.IDictionary] $Configuration, [string] $FullProjectPath, [System.Collections.IDictionary] $DestinationPaths, [ValidateSet('ReleasesUnpacked', 'Releases')][string] $Type ) if ($Artefact) { $Artefact = $ChosenArtefact $ChosenType = $Artefact.Type $ID = if ($ChosenArtefact.ID) { $ChosenArtefact.ID } else { $null } } elseif ($Type) { if ($Configuration.Steps.BuildModule.$Type) { $Artefact = $Configuration.Steps.BuildModule.$Type $ChosenType = $Type } else { $Artefact = $null } $ID = $null } else { $ID = $null $Artefact = $null } if ($ID) { $TextToDisplay = "Preparing Artefact of type '$ChosenType' (ID: $ID)" } else { $TextToDisplay = "Preparing Artefact of type '$ChosenType'" } if ($null -eq $Artefact -or $Artefact.Count -eq 0) { return } $ModuleName = $Configuration.Information.ModuleName $ModuleVersion = $Configuration.Information.Manifest.ModuleVersion $FullProjectPath = Initialize-ReplacePath -ReplacementPath $FullProjectPath -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration $Artefact.Path = Initialize-ReplacePath -ReplacementPath $Artefact.Path -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration Write-TextWithTime -Text $TextToDisplay -PreAppend Information -SpacesBefore ' ' { if ($Artefact -or $Artefact.Enabled) { if ($Artefact -is [System.Collections.IDictionary]) { if ($Artefact.Path) { if ($Artefact.Relative -eq $false) { $FolderPathReleases = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Artefact.Path) } else { $FolderPathReleases = [System.IO.Path]::Combine($FullProjectPath, $Artefact.Path) } } else { $FolderPathReleases = [System.IO.Path]::Combine($FullProjectPath, $Type) } } else { $FolderPathReleases = [System.IO.Path]::Combine($FullProjectPath, $Type) } if ($Artefact.RequiredModules.ModulesPath) { $DirectPathForPrimaryModule = $Artefact.RequiredModules.ModulesPath } elseif ($Artefact.RequiredModules.Path) { $DirectPathForPrimaryModule = $Artefact.RequiredModules.Path } elseif ($Artefact.Path) { $DirectPathForPrimaryModule = $Artefact.Path } else { $DirectPathForPrimaryModule = $FolderPathReleases } if ($Artefact.RequiredModules.Path) { $DirectPathForRequiredModules = $Artefact.RequiredModules.Path } elseif ($Artefact.RequiredModules.ModulesPath) { $DirectPathForRequiredModules = $Artefact.RequiredModules.ModulesPath } elseif ($Artefact.Path) { $DirectPathForRequiredModules = $Artefact.Path } else { $DirectPathForRequiredModules = $FolderPathReleases } if ($Artefact -eq $true -or $Artefact.Enabled) { if ($Artefact -is [System.Collections.IDictionary]) { if ($DirectPathForPrimaryModule) { if ($Artefact.Relative -eq $false) { $CurrentModulePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DirectPathForPrimaryModule) } else { $DirectPathForPrimaryModule = Initialize-ReplacePath -ReplacementPath $DirectPathForPrimaryModule -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration $CurrentModulePath = [System.IO.Path]::Combine($FullProjectPath, $DirectPathForPrimaryModule) } } else { $CurrentModulePath = [System.IO.Path]::Combine($FullProjectPath, $ChosenType) } if ($DirectPathForRequiredModules) { if ($Artefact.Relative -eq $false) { $RequiredModulesPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DirectPathForRequiredModules) } else { $DirectPathForRequiredModules = Initialize-ReplacePath -ReplacementPath $DirectPathForRequiredModules -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration $RequiredModulesPath = [System.IO.Path]::Combine($FullProjectPath, $DirectPathForRequiredModules) } } else { $RequiredModulesPath = $ArtefactsPath } $ArtefactsPath = $Artefact.Path } else { $ArtefactsPath = [System.IO.Path]::Combine($FullProjectPath, $ChosenType, $TagName) $RequiredModulesPath = $ArtefactsPath $CurrentModulePath = $ArtefactsPath } if ($null -eq $Artefact.DestinationFilesRelative) { if ($null -ne $Artefact.Relative) { $Artefact.DestinationFilesRelative = $Artefact.Relative } } if ($null -eq $Artefact.DestinationDirectoriesRelative) { if ($null -ne $Artefact.Relative) { $Artefact.DestinationDirectoriesRelative = $Artefact.Relative } } $SplatArtefact = @{ ModuleName = $Configuration.Information.ModuleName ModuleVersion = $Configuration.Information.Manifest.ModuleVersion LegacyName = if ($Artefact -is [bool]) { $true } else { $false } CopyMainModule = $true CopyRequiredModules = $Artefact.RequiredModules -eq $true -or $Artefact.RequiredModules.Enabled ProjectPath = $FullProjectPath Destination = $ArtefactsPath DestinationMainModule = $CurrentModulePath DestinationRequiredModules = $RequiredModulesPath RequiredModules = $Configuration.Information.Manifest.RequiredModules Files = $Artefact.FilesOutput Folders = $Artefact.DirectoryOutput DestinationFilesRelative = $Artefact.DestinationFilesRelative DestinationDirectoriesRelative = $Artefact.DestinationDirectoriesRelative Configuration = $Configuration IncludeTag = $Artefact.IncludeTagName ArtefactName = $Artefact.ArtefactName ScriptName = $Artefact.ScriptName ZipIt = if ($ChosenType -in 'Packed', 'Releases', 'ScriptPacked') { $true } else { $false } ConvertToScript = if ($ChosenType -in 'ScriptPacked', 'Script') { $true } else { $false } DestinationZip = $ArtefactsPath PreScriptMerge = $Artefact.PreScriptMerge PostScriptMerge = $Artefact.PostScriptMerge DoNotClear = $Artefact.DoNotClear ID = if ($ChosenArtefact.ID) { $ChosenArtefact.ID } else { $null } } Remove-EmptyValue -Hashtable $SplatArtefact Add-Artefact @SplatArtefact } } } -ColorBefore Yellow -ColorTime Yellow -Color Yellow } function Start-DocumentationBuilding { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [string] $FullProjectPath, [string] $ProjectName ) if ($Configuration.Steps.BuildDocumentation -is [bool]) { $TemporaryBuildDocumentation = $Configuration.Steps.BuildDocumentation $Configuration.Steps.BuildDocumentation = @{ Enable = $TemporaryBuildDocumentation } } if ($Configuration.Steps.BuildDocumentation -is [System.Collections.IDictionary]) { if ($Configuration.Steps.BuildDocumentation.Enable -eq $true) { $WarningVariablesMarkdown = @() $DocumentationPath = "$FullProjectPath\$($Configuration.Options.Documentation.Path)" $ReadMePath = "$FullProjectPath\$($Configuration.Options.Documentation.PathReadme)" Write-Text "[+] Generating documentation to $DocumentationPath with $ReadMePath" -Color Yellow if (-not (Test-Path -Path $DocumentationPath)) { $null = New-Item -Path "$FullProjectPath\Docs" -ItemType Directory -Force } if ($Configuration.Steps.BuildDocumentation.Tool -eq 'HelpOut') { try { Save-MarkdownHelp -Module $ProjectName -OutputPath $DocumentationPath -ErrorAction Stop -WarningVariable +WarningVariablesMarkdown -WarningAction SilentlyContinue -SkipCommandType Alias -ExcludeFile "*.svg" } catch { Write-Text "[-] Documentation warning: $($_.Exception.Message)" -Color Yellow } } else { [Array] $Files = Get-ChildItem -Path $DocumentationPath if ($Files.Count -gt 0) { if ($Configuration.Steps.BuildDocumentation.StartClean -ne $true) { try { $null = Update-MarkdownHelpModule $DocumentationPath -RefreshModulePage -ModulePagePath $ReadMePath -ErrorAction Stop -WarningVariable +WarningVariablesMarkdown -WarningAction SilentlyContinue -ExcludeDontShow } catch { Write-Text "[-] Documentation warning: $($_.Exception.Message)" -Color Yellow } } else { Remove-ItemAlternative -Path $DocumentationPath -SkipFolder [Array] $Files = Get-ChildItem -Path $DocumentationPath } } if ($Files.Count -eq 0) { try { $null = New-MarkdownHelp -Module $ProjectName -WithModulePage -OutputFolder $DocumentationPath -ErrorAction Stop -WarningVariable +WarningVariablesMarkdown -WarningAction SilentlyContinue -ExcludeDontShow } catch { Write-Text "[-] Documentation warning: $($_.Exception.Message)" -Color Yellow } $null = Move-Item -Path "$DocumentationPath\$ProjectName.md" -Destination $ReadMePath -ErrorAction SilentlyContinue if ($Configuration.Steps.BuildDocumentation.UpdateWhenNew) { try { $null = Update-MarkdownHelpModule $DocumentationPath -RefreshModulePage -ModulePagePath $ReadMePath -ErrorAction Stop -WarningVariable +WarningVariablesMarkdown -WarningAction SilentlyContinue -ExcludeDontShow } catch { Write-Text "[-] Documentation warning: $($_.Exception.Message)" -Color Yellow } } } } foreach ($_ in $WarningVariablesMarkdown) { Write-Text "[-] Documentation warning: $_" -Color Yellow } } } } function Start-ImportingModules { [CmdletBinding()] param( [string] $ProjectName, [System.Collections.IDictionary] $Configuration ) $TemporaryVerbosePreference = $VerbosePreference $TemporaryErrorPreference = $global:ErrorActionPreference $global:ErrorActionPreference = 'Stop' if ($null -ne $ImportModules.Verbose) { $VerbosePreference = $true } else { $VerbosePreference = $false } if ($Configuration.Steps.ImportModules.RequiredModules) { Write-TextWithTime -Text 'Importing modules (as defined in dependencies)' { foreach ($Module in $Configuration.Information.Manifest.RequiredModules) { if ($Module -is [System.Collections.IDictionary]) { Write-Text " [>] Importing required module - $($Module.ModuleName)" -Color Yellow if ($Module.ModuleVersion) { Import-Module -Name $Module.ModuleName -MinimumVersion $Module.ModuleVersion -Force -ErrorAction Stop -Verbose:$VerbosePreference } elseif ($Module.ModuleName) { Import-Module -Name $Module.ModuleName -Force -ErrorAction Stop -Verbose:$VerbosePreference } } elseif ($Module -is [string]) { Write-Text " [>] Importing required module - $($Module)" -Color Yellow Import-Module -Name $Module -Force -ErrorAction Stop -Verbose:$VerbosePreference } } } -PreAppend 'Information' } if ($Configuration.Steps.ImportModules.Self) { Write-TextWithTime -Text 'Importing module - SELF' { Import-Module -Name $ProjectName -Force -ErrorAction Stop -Verbose:$VerbosePreference } -PreAppend 'Information' } $global:ErrorActionPreference = $TemporaryErrorPreference $VerbosePreference = $TemporaryVerbosePreference } function Start-LibraryBuilding { [CmdletBinding()] param( [string] $ModuleName, [string] $RootDirectory, [string] $Version, [System.Collections.IDictionary] $LibraryConfiguration, [System.Collections.IDictionary] $CmdletsAliases ) if ($LibraryConfiguration.Count -eq 0) { return } if ($LibraryConfiguration.Enable -ne $true) { return } $TranslateFrameworks = [ordered] @{ 'net472' = 'Default' 'net48' = 'Default' 'net482' = 'Default' 'net470' = 'Default' 'net471' = 'Default' 'net452' = 'Default' 'net451' = 'Default' 'NetStandard2.0' = 'Standard' 'netStandard2.1' = 'Standard' 'netcoreapp2.1' = 'Core' 'netcoreapp3.1' = 'Core' 'net5.0' = 'Core' 'net6.0' = 'Core' 'net6.0-windows' = 'Core' 'net7.0' = 'Core' 'net7.0-windows' = 'Core' 'net8.0' = 'Core' } if ($LibraryConfiguration.Configuration) { $Configuration = $LibraryConfiguration.Configuration } else { $Configuration = 'Release' } if ($LibraryConfiguration.ProjectName) { $ModuleName = $LibraryConfiguration.ProjectName } if ($LibraryConfiguration.NETProjectPath) { $ModuleProjectFile = [System.IO.Path]::Combine($LibraryConfiguration.NETProjectPath, "$($LibraryConfiguration.ProjectName).csproj") $SourceFolder = [System.IO.Path]::Combine($LibraryConfiguration.NETProjectPath) } else { $ModuleProjectFile = [System.IO.Path]::Combine($RootDirectory, "Sources", $ModuleName, "$ModuleName.csproj") $SourceFolder = [System.IO.Path]::Combine($RootDirectory, "Sources", $ModuleName) } $ModuleBinFolder = [System.IO.Path]::Combine($RootDirectory, "Lib") if (Test-Path -LiteralPath $ModuleBinFolder) { $Items = Get-ChildItem -LiteralPath $ModuleBinFolder -Recurse -Force $Items | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue } $null = New-Item -Path $ModuleBinFolder -ItemType Directory -Force try { Push-Location -Path $SourceFolder -ErrorAction Stop } catch { Write-Text "[-] Couldn't switch to folder $SourceFolder. Error: $($_.Exception.Message)" -Color Red return $false } try { [xml] $ProjectInformation = Get-Content -Raw -LiteralPath $ModuleProjectFile -Encoding UTF8 -ErrorAction Stop } catch { Write-Text "[-] Can't read $ModuleProjectFile file. Error: $($_.Exception.Message)" -Color Red return $false } if ($IsLinux) { $OSVersion = 'Linux' } elseif ($IsMacOS) { $OSVersion = 'OSX' } else { $OSVersion = 'Windows' } $SupportedFrameworks = foreach ($PropertyGroup in $ProjectInformation.Project.PropertyGroup) { if ($PropertyGroup.TargetFrameworks) { if ($PropertyGroup.TargetFrameworks -is [array]) { foreach ($Target in $PropertyGroup.TargetFrameworks) { if ($Target.Condition -and $Target.Condition -like "*$OSVersion*" -and $Target.'#text') { $Target.'#text'.Trim() -split ";" } else { $Target.'#text'.Trim() -split ";" } } } else { $PropertyGroup.TargetFrameworks -split ";" } } elseif ($PropertyGroup.TargetFrameworkVersion) { throw "TargetFrameworkVersion is not supported. Please use TargetFrameworks/TargetFramework instead which may require different project profile." } elseif ($PropertyGroup.TargetFramework) { $PropertyGroup.TargetFramework } } $Count = 0 foreach ($Framework in $TranslateFrameworks.Keys) { if ($SupportedFrameworks.Contains($Framework.ToLower()) -and $LibraryConfiguration.Framework.Contains($Framework.ToLower())) { Write-Text "[+] Building $Framework ($Configuration)" $null = dotnet publish --configuration $Configuration --verbosity q -nologo -p:Version=$Version --framework $Framework if ($LASTEXITCODE) { Write-Host Write-Text "[-] Building $Framework - failed. Error: $LASTEXITCODE" -Color Red exit } } else { continue } $InitialFolder = [System.IO.Path]::Combine($SourceFolder, "bin", $Configuration, $Framework, "publish") $InitialFolder = (Resolve-Path -Path $InitialFolder).Path $PublishDirFolder = [System.IO.Path]::Combine($SourceFolder, "bin", $Configuration, $Framework, "publish", "*") $ModuleBinFrameworkFolder = [System.IO.Path]::Combine($ModuleBinFolder, $TranslateFrameworks[$Framework]) $null = New-Item -Path $ModuleBinFrameworkFolder -ItemType Directory -ErrorAction SilentlyContinue try { $List = Get-ChildItem -Filter "*" -ErrorAction Stop -Path $PublishDirFolder -File -Recurse $List = @( foreach ($File in $List) { if ($File.Extension -in '.dll', '.so', 'dylib') { $File } } ) } catch { Write-Text "[-] Can't list files in $PublishDirFolder folder. Error: $($_.Exception.Message)" -Color Red return $false } $Errors = $false Write-Text -Text "[i] Preparing copying module files for $ModuleName from $InitialFolder" -Color DarkGray :fileLoop foreach ($File in $List) { if ($LibraryConfiguration.ExcludeMainLibrary -and $File.Name -eq "$ModuleName.dll") { continue } if ($LibraryConfiguration.ExcludeLibraryFilter) { foreach ($Library in $LibraryConfiguration.ExcludeLibraryFilter) { if ($File.Name -like $Library) { continue fileLoop } } } if (-not $LibraryConfiguration.BinaryModuleCmdletScanDisabled) { $SkipAssembly = $false if ($PSVersionTable.PSEdition -eq 'Core') { if ($TranslateFrameworks[$Framework] -eq 'Default') { $SkipAssembly = $true } } else { if ($TranslateFrameworks[$Framework] -eq 'Core') { $SkipAssembly = $true } } if (-not $SkipAssembly) { if ($InitialFolder -eq $File.DirectoryName) { $CmdletsFound = Get-PowerShellAssemblyMetadata -Path $File.FullName if ($CmdletsFound -eq $false) { $Errors = $true } else { if ($CmdletsFound.CmdletsToExport.Count -gt 0 -or $CmdletsFound.AliasesToExport.Count -gt 0) { Write-Text -Text "Found $($CmdletsFound.CmdletsToExport.Count) cmdlets and $($CmdletsFound.AliasesToExport.Count) aliases in $File" -Color Yellow -PreAppend Information -SpacesBefore " " if ($CmdletsFound.CmdletsToExport.Count -gt 0) { Write-Text -Text "Cmdlets: $($CmdletsFound.CmdletsToExport -join ', ')" -Color Yellow -PreAppend Plus -SpacesBefore " " } if ($CmdletsFound.AliasesToExport.Count -gt 0) { Write-Text -Text "Aliases: $($CmdletsFound.AliasesToExport -join ', ')" -Color Yellow -PreAppend Plus -SpacesBefore " " } $CmdletsAliases[$File.FullName] = $CmdletsFound } } } else { Write-Text -Text " [-] Skipping $($File.FullName) as it's not in the same folder as the assembly" -Color Yellow } } } try { if ($LibraryConfiguration.NETDoNotCopyLibrariesRecursively) { Write-Text -Text " [+] Copying '$($File.FullName)' to folder '$ModuleBinFrameworkFolder'" -Color DarkGray Copy-Item -Path $File.FullName -Destination $ModuleBinFrameworkFolder -ErrorAction Stop } else { $relativePath = $File.FullName.Substring($InitialFolder.Length + 1) $destinationFilePath = [System.IO.Path]::Combine($ModuleBinFrameworkFolder, $relativePath) $destinationDirectory = [System.IO.Path]::GetDirectoryName($destinationFilePath) if (-not (Test-Path -Path $destinationDirectory)) { New-Item -ItemType Directory -Path $destinationDirectory -Force } Write-Text -Text " [+] Copying '$($File.FullName)' as file '$destinationFilePath'" -Color DarkGray Copy-Item -Path $File.FullName -Destination $destinationFilePath -ErrorAction Stop } } catch { Write-Text " [-] Copying $File to $ModuleBinFrameworkFolder failed. Error: $($_.Exception.Message)" -Color Red $Errors = $true } } if ($Errors) { return $false } Write-Text -Text "[i] Preparing copying module files for $ModuleName from $($InitialFolder). Completed!" -Color DarkGray if ($LibraryConfiguration.NETBinaryModuleDocumentation) { $Errors = $false try { $List = Get-ChildItem -Filter "*.dll-Help.xml" -ErrorAction Stop -Path $PublishDirFolder -File } catch { Write-Text "[-] Can't list files in $PublishDirFolder folder. Error: $($_.Exception.Message)" -Color Red return $false } :fileLoop foreach ($File in $List) { if ($LibraryConfiguration.ExcludeMainLibrary -and $File.Name -eq "$ModuleName.dll") { continue } $Culture = 'en-US' $TargetPathFolder = [System.IO.Path]::Combine($ModuleBinFrameworkFolder, $Culture) $TargetPath = [System.IO.Path]::Combine($TargetPathFolder, $File.Name) if (-not (Test-Path -Path $TargetPathFolder)) { $null = New-Item -Path $TargetPathFolder -ItemType Directory -ErrorAction SilentlyContinue } try { Copy-Item -Path $File.FullName -Destination $TargetPath -ErrorAction Stop } catch { Write-Text "[-] Copying $File to $TargetPath failed. Error: $($_.Exception.Message)" -Color Red $Errors = $true } } if ($Errors) { return $false } } } try { Pop-Location -ErrorAction Stop } catch { Write-Text "[-] Couldn't switch back to the root folder. Error: $($_.Exception.Message)" -Color Red return $false } } function Start-ModuleBuilding { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [string] $PathToProject ) $DestinationPaths = [ordered] @{ } if ($Configuration.Information.Manifest.CompatiblePSEditions) { if ($Configuration.Information.Manifest.CompatiblePSEditions -contains 'Desktop') { $DestinationPaths.Desktop = [IO.path]::Combine($Configuration.Information.DirectoryModules, $Configuration.Information.ModuleName) } if ($Configuration.Information.Manifest.CompatiblePSEditions -contains 'Core') { $DestinationPaths.Core = [IO.path]::Combine($Configuration.Information.DirectoryModulesCore, $Configuration.Information.ModuleName) } } else { $DestinationPaths.Desktop = [IO.path]::Combine($Configuration.Information.DirectoryModules, $Configuration.Information.ModuleName) $DestinationPaths.Core = [IO.path]::Combine($Configuration.Information.DirectoryModulesCore, $Configuration.Information.ModuleName) } [string] $Random = Get-Random 10000000000 [string] $FullModuleTemporaryPath = [IO.path]::GetTempPath() + '' + $Configuration.Information.ModuleName [string] $FullTemporaryPath = [IO.path]::GetTempPath() + '' + $Configuration.Information.ModuleName + "_TEMP_$Random" if ($Configuration.Information.DirectoryProjects) { [string] $FullProjectPath = [IO.Path]::Combine($Configuration.Information.DirectoryProjects, $Configuration.Information.ModuleName) } else { [string] $FullProjectPath = $PathToProject } [string] $ProjectName = $Configuration.Information.ModuleName $PSD1FilePath = [System.IO.Path]::Combine($FullProjectPath, "$ProjectName.psd1") $PSM1FilePath = [System.IO.Path]::Combine($FullProjectPath, "$ProjectName.psm1") if ($Configuration.Information.Manifest.ModuleVersion) { if ($Configuration.Steps.BuildModule.LocalVersion) { $Versioning = Step-Version -Module $Configuration.Information.ModuleName -ExpectedVersion $Configuration.Information.Manifest.ModuleVersion -Advanced -LocalPSD1 $PSD1FilePath } else { $Versioning = Step-Version -Module $Configuration.Information.ModuleName -ExpectedVersion $Configuration.Information.Manifest.ModuleVersion -Advanced } $Configuration.Information.Manifest.ModuleVersion = $Versioning.Version } else { $Configuration.Information.Manifest.ModuleVersion = 1.0.0 } Write-Text '----------------------------------------------------' Write-Text "[i] Project/Module Name: $ProjectName" -Color Yellow if ($Configuration.Steps.BuildModule.LocalVersion) { Write-Text "[i] Current Local Version: $($Versioning.CurrentVersion)" -Color Yellow } else { Write-Text "[i] Current PSGallery Version: $($Versioning.CurrentVersion)" -Color Yellow } Write-Text "[i] Expected Version: $($Configuration.Information.Manifest.ModuleVersion)" -Color Yellow Write-Text "[i] Full module temporary path: $FullModuleTemporaryPath" -Color Yellow Write-Text "[i] Full project path: $FullProjectPath" -Color Yellow Write-Text "[i] Full temporary path: $FullTemporaryPath" -Color Yellow Write-Text "[i] PSScriptRoot: $PSScriptRoot" -Color Yellow Write-Text "[i] Current PSEdition: $PSEdition" -Color Yellow Write-Text "[i] Destination Desktop: $($DestinationPaths.Desktop)" -Color Yellow Write-Text "[i] Destination Core: $($DestinationPaths.Core)" -Color Yellow Write-Text '----------------------------------------------------' if (-not $Configuration.Steps.BuildModule) { Write-Text '[-] Section BuildModule is missing. Terminating.' -Color Red return $false } if (-not $Configuration.Information.ModuleName) { Write-Text '[-] Section Information.ModuleName is missing. Terminating.' -Color Red return $false } if (-not (Test-Path -Path $FullProjectPath)) { Write-Text "[-] Project path doesn't exists $FullProjectPath. Terminating" -Color Red return $false } $CmdletsAliases = [ordered] @{} $startLibraryBuildingSplat = @{ RootDirectory = $FullProjectPath Version = $Configuration.Information.Manifest.ModuleVersion ModuleName = $ProjectName LibraryConfiguration = $Configuration.Steps.BuildLibraries CmdletsAliases = $CmdletsAliases } $Success = Start-LibraryBuilding @startLibraryBuildingSplat if ($Success -contains $false) { return $false } $Success = Convert-RequiredModules -Configuration $Configuration if ($Success -eq $false) { return $false } if ($Configuration.Steps.BuildModule.Enable -eq $true) { $CurrentLocation = (Get-Location).Path $Success = Start-PreparingStructure -Configuration $Configuration -FullProjectPath $FullProjectPath -FullTemporaryPath $FullTemporaryPath -FullModuleTemporaryPath $FullModuleTemporaryPath -DestinationPaths $DestinationPaths if ($Success -eq $false) { return $false } $Variables = Start-PreparingVariables -Configuration $Configuration -FullProjectPath $FullProjectPath if ($Variables -eq $false) { return $false } $LinkDirectories = $Variables.LinkDirectories $LinkFilesRoot = $Variables.LinkFilesRoot $LinkPrivatePublicFiles = $Variables.LinkPrivatePublicFiles $DirectoriesWithClasses = $Variables.DirectoriesWithClasses $DirectoriesWithPS1 = $Variables.DirectoriesWithPS1 $Files = $Variables.Files $startPreparingFunctionsAndAliasesSplat = @{ Configuration = $Configuration FullProjectPath = $FullProjectPath Files = $Files CmdletsAliases = $CmdletsAliases } $AliasesAndFunctions = Start-PreparingFunctionsAndAliases @startPreparingFunctionsAndAliasesSplat if ($AliasesAndFunctions -contains $false) { return $false } $SaveConfiguration = Copy-DictionaryManual -Dictionary $Configuration $newPersonalManifestSplat = @{ Configuration = $Configuration ManifestPath = $PSD1FilePath AddScriptsToProcess = $true } if ($Configuration.Steps.BuildModule.UseWildcardForFunctions) { $newPersonalManifestSplat.UseWildcardForFunctions = $Configuration.Steps.BuildModule.UseWildcardForFunctions } if ($Configuration.Steps.BuildLibraries.BinaryModule) { $newPersonalManifestSplat.BinaryModule = $Configuration.Steps.BuildLibraries.BinaryModule } $Success = New-PersonalManifest @newPersonalManifestSplat if ($Success -eq $false) { return $false } Write-TextWithTime -Text "Verifying created PSD1 is readable" -PreAppend Information { if (Test-Path -LiteralPath $PSD1FilePath) { try { $null = Import-PowerShellDataFile -Path $PSD1FilePath -ErrorAction Stop } catch { Write-Text "[-] PSD1 Reading $PSD1FilePath failed. Error: $($_.Exception.Message)" -Color Red return $false } } else { Write-Text "[-] PSD1 Reading $PSD1FilePath failed. File not created..." -Color Red return $false } } -ColorBefore Yellow -ColorTime Yellow -Color Yellow $Configuration = $SaveConfiguration $Success = Format-Code -FilePath $PSD1FilePath -FormatCode $Configuration.Options.Standard.FormatCodePSD1 if ($Success -eq $false) { return $false } $Success = Format-Code -FilePath $PSM1FilePath -FormatCode $Configuration.Options.Standard.FormatCodePSM1 if ($Success -eq $false) { return $false } if ($Configuration.Steps.BuildModule.RefreshPSD1Only) { return } $startModuleMergingSplat = @{ Configuration = $Configuration ProjectName = $ProjectName FullTemporaryPath = $FullTemporaryPath FullModuleTemporaryPath = $FullModuleTemporaryPath FullProjectPath = $FullProjectPath LinkDirectories = $LinkDirectories LinkFilesRoot = $LinkFilesRoot LinkPrivatePublicFiles = $LinkPrivatePublicFiles DirectoriesWithPS1 = $DirectoriesWithPS1 DirectoriesWithClasses = $DirectoriesWithClasses AliasesAndFunction = $AliasesAndFunctions CmdletsAliases = $CmdletsAliases } $Success = Start-ModuleMerging @startModuleMergingSplat if ($Success -contains $false) { return $false } Set-Location -Path $CurrentLocation $Success = if ($Configuration.Steps.BuildModule.Enable) { if ($DestinationPaths.Desktop) { Write-TextWithTime -Text "Copy module to PowerShell 5 destination: $($DestinationPaths.Desktop)" { $Success = Remove-Directory -Directory $DestinationPaths.Desktop if ($Success -eq $false) { return $false } Add-Directory -Directory $DestinationPaths.Desktop Get-ChildItem -LiteralPath $FullModuleTemporaryPath | Copy-Item -Destination $DestinationPaths.Desktop -Recurse Get-ChildItem $DestinationPaths.Desktop -Recurse -Force -Directory | Sort-Object -Property FullName -Descending | ` Where-Object { $($_ | Get-ChildItem -Force | Select-Object -First 1).Count -eq 0 } | ` Remove-Item } -PreAppend Plus } if ($DestinationPaths.Core) { Write-TextWithTime -Text "Copy module to PowerShell 6/7 destination: $($DestinationPaths.Core)" { $Success = Remove-Directory -Directory $DestinationPaths.Core if ($Success -eq $false) { return $false } Add-Directory -Directory $DestinationPaths.Core Get-ChildItem -LiteralPath $FullModuleTemporaryPath | Copy-Item -Destination $DestinationPaths.Core -Recurse Get-ChildItem $DestinationPaths.Core -Recurse -Force -Directory | Sort-Object -Property FullName -Descending | ` Where-Object { $($_ | Get-ChildItem -Force | Select-Object -First 1).Count -eq 0 } | ` Remove-Item } -PreAppend Plus } } if ($Success -contains $false) { return $false } $Success = Write-TextWithTime -Text "Building artefacts" -PreAppend Information { $Success = Start-ArtefactsBuilding -Configuration $Configuration -FullProjectPath $FullProjectPath -DestinationPaths $DestinationPaths -Type 'Releases' if ($Success -eq $false) { return $false } $Success = Start-ArtefactsBuilding -Configuration $Configuration -FullProjectPath $FullProjectPath -DestinationPaths $DestinationPaths -Type 'ReleasesUnpacked' if ($Success -eq $false) { return $false } foreach ($Artefact in $Configuration.Steps.BuildModule.Artefacts) { $Success = Start-ArtefactsBuilding -Configuration $Configuration -FullProjectPath $FullProjectPath -DestinationPaths $DestinationPaths -ChosenArtefact $Artefact if ($Success -contains $false) { return $false } } } -ColorBefore Yellow -ColorTime Yellow -Color Yellow if ($Success -contains $false) { return $false } } if ($Configuration.Steps.ImportModules) { $ImportSuccess = Start-ImportingModules -Configuration $Configuration -ProjectName $ProjectName if ($ImportSuccess -contains $false) { return $false } } if ($Configuration.Options.TestsAfterMerge) { $TestsSuccess = Initialize-InternalTests -Configuration $Configuration -Type 'TestsAfterMerge' if ($TestsSuccess -eq $false) { return $false } } if ($Configuration.Steps.PublishModule.Enabled) { $Publishing = Start-PublishingGallery -Configuration $Configuration if ($Publishing -eq $false) { return $false } } if ($Configuration.Steps.PublishModule.GitHub) { $Publishing = Start-PublishingGitHub -Configuration $Configuration -ProjectName $ProjectName if ($Publishing -eq $false) { return $false } } foreach ($ChosenNuget in $Configuration.Steps.BuildModule.GalleryNugets) { $Success = Start-PublishingGallery -Configuration $Configuration -ChosenNuget $ChosenNuget if ($Success -eq $false) { return $false } } foreach ($ChosenNuget in $Configuration.Steps.BuildModule.GitHubNugets) { $Success = Start-PublishingGitHub -Configuration $Configuration -ChosenNuget $ChosenNuget -ProjectName $ProjectName if ($Success -eq $false) { return $false } } if ($Configuration.Steps.BuildDocumentation) { Start-DocumentationBuilding -Configuration $Configuration -FullProjectPath $FullProjectPath -ProjectName $ProjectName } Write-Text "[+] Cleaning up directories created in TEMP directory" -Color Yellow $Success = Remove-Directory $FullModuleTemporaryPath if ($Success -eq $false) { return $false } $Success = Remove-Directory $FullTemporaryPath if ($Success -eq $false) { return $false } } function Start-ModuleMerging { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [string] $ProjectName, [string] $FullTemporaryPath, [string] $FullModuleTemporaryPath, [string] $FullProjectPath, [Array] $LinkDirectories, [Array] $LinkFilesRoot, [Array] $LinkPrivatePublicFiles, [string[]] $DirectoriesWithPS1, [string[]] $DirectoriesWithClasses, [System.Collections.IDictionary] $AliasesAndFunctions, [System.Collections.IDictionary] $CmdletsAliases ) if ($Configuration.Steps.BuildModule.Merge) { foreach ($Directory in $LinkDirectories) { $Dir = [System.IO.Path]::Combine($FullTemporaryPath, "$Directory") Add-Directory -Directory $Dir } [Array] $CompareWorkaround = foreach ($Directory in $DirectoriesWithPS1) { if ($null -eq $IsWindows -or $IsWindows -eq $true) { $Dir = -join ($Directory, "\") } else { $Dir = -join ($Directory, "/") } } $LinkDirectoriesWithSupportFiles = $LinkDirectories | Where-Object { $_ -notin $CompareWorkaround } foreach ($Directory in $LinkDirectoriesWithSupportFiles) { $Dir = [System.IO.Path]::Combine($FullModuleTemporaryPath, "$Directory") Add-Directory -Directory $Dir } $LinkingFilesTime = Write-Text "[+] Linking files from root and sub directories" -Start Copy-InternalFiles -LinkFiles $LinkFilesRoot -FullModulePath $FullTemporaryPath -FullProjectPath $FullProjectPath Copy-InternalFiles -LinkFiles $LinkPrivatePublicFiles -FullModulePath $FullTemporaryPath -FullProjectPath $FullProjectPath Write-Text -End -Time $LinkingFilesTime $FilesToLink = $LinkPrivatePublicFiles | Where-Object { $_ -notlike '*.ps1' -and $_ -notlike '*.psd1' } Copy-InternalFiles -LinkFiles $FilesToLink -FullModulePath $FullModuleTemporaryPath -FullProjectPath $FullProjectPath if ($Configuration.Information.LibrariesStandard) { } elseif ($Configuration.Information.LibrariesCore -and $Configuration.Information.LibrariesDefault) { } else { $Configuration.Information.LibrariesStandard = [System.IO.Path]::Combine("Lib", "Standard") $Configuration.Information.LibrariesCore = [System.IO.Path]::Combine("Lib", "Core") $Configuration.Information.LibrariesDefault = [System.IO.Path]::Combine("Lib", "Default") } if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesCore)) { if ($null -eq $IsWindows -or $IsWindows -eq $true) { $StartsWithCore = -join ($Configuration.Information.LibrariesCore, "\") } else { $StartsWithCore = -join ($Configuration.Information.LibrariesCore, "/") } } if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesDefault)) { if ($null -eq $IsWindows -or $IsWindows -eq $true) { $StartsWithDefault = -join ($Configuration.Information.LibrariesDefault, "\") } else { $StartsWithDefault = -join ($Configuration.Information.LibrariesDefault, "/") } } if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesStandard)) { if ($null -eq $IsWindows -or $IsWindows -eq $true) { $StartsWithStandard = -join ($Configuration.Information.LibrariesStandard, "\") } else { $StartsWithStandard = -join ($Configuration.Information.LibrariesStandard, "/") } } if ($null -ne $LinkPrivatePublicFiles) { $CoreFiles = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithCore) } $DefaultFiles = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithDefault) } $StandardFiles = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithStandard) } } else { $CoreFiles = @() $DefaultFiles = @() $StandardFiles = @() } $Default = $false $Core = $false $Standard = $false if ($CoreFiles.Count -gt 0) { $Core = $true } if ($DefaultFiles.Count -gt 0) { $Default = $true } if ($StandardFiles.Count -gt 0) { $Standard = $true } if ($Standard -and $Core -and $Default) { $FrameworkNet = 'Default' $Framework = 'Standard' } elseif ($Standard -and $Core) { $Framework = 'Standard' $FrameworkNet = 'Standard' } elseif ($Core -and $Default) { $Framework = 'Core' $FrameworkNet = 'Default' } elseif ($Standard -and $Default) { $Framework = 'Standard' $FrameworkNet = 'Default' } elseif ($Standard) { $Framework = 'Standard' $FrameworkNet = 'Standard' } elseif ($Core) { $Framework = 'Core' $FrameworkNet = '' } elseif ($Default) { $Framework = '' $FrameworkNet = 'Default' } if ($Framework -eq 'Core') { $FilesLibrariesCore = $CoreFiles } elseif ($Framework -eq 'Standard') { $FilesLibrariesCore = $StandardFiles } if ($FrameworkNet -eq 'Default') { $FilesLibrariesDefault = $DefaultFiles } elseif ($FrameworkNet -eq 'Standard') { $FilesLibrariesDefault = $StandardFiles } if ($FrameworkNet -eq 'Standard' -and $Framework -eq 'Standard') { $FilesLibrariesStandard = $FilesLibrariesCore } $mergeModuleSplat = @{ ModuleName = $ProjectName ModulePathSource = $FullTemporaryPath ModulePathTarget = $FullModuleTemporaryPath Sort = $Configuration.Options.Merge.Sort FunctionsToExport = $Configuration.Information.Manifest.FunctionsToExport AliasesToExport = $Configuration.Information.Manifest.AliasesToExport AliasesAndFunctions = $AliasesAndFunctions CmdletsAliases = $CmdletsAliases LibrariesStandard = $FilesLibrariesStandard LibrariesCore = $FilesLibrariesCore LibrariesDefault = $FilesLibrariesDefault FormatCodePSM1 = $Configuration.Options.Merge.FormatCodePSM1 FormatCodePSD1 = $Configuration.Options.Merge.FormatCodePSD1 Configuration = $Configuration DirectoriesWithPS1 = $DirectoriesWithPS1 ClassesPS1 = $DirectoriesWithClasses IncludeAsArray = $Configuration.Information.IncludeAsArray } $Success = Merge-Module @mergeModuleSplat if ($Success -eq $false) { return $false } if ($Configuration.Steps.BuildModule.CreateFileCatalog) { $TimeToExecuteSign = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] Creating file catalog" -Color Blue $TimeToExecuteSign = [System.Diagnostics.Stopwatch]::StartNew() $CategoryPaths = @( $FullModuleTemporaryPath $NotEmptyPaths = (Get-ChildItem -Directory -Path $FullModuleTemporaryPath -Recurse).FullName if ($NotEmptyPaths) { $NotEmptyPaths } ) foreach ($CatPath in $CategoryPaths) { $CatalogFile = [io.path]::Combine($CatPath, "$ProjectName.cat") $FileCreated = New-FileCatalog -Path $CatPath -CatalogFilePath $CatalogFile -CatalogVersion 2.0 if ($FileCreated) { Write-Text " [>] Catalog file covering $CatPath was created $($FileCreated.Name)" -Color Yellow } } $TimeToExecuteSign.Stop() Write-Text "[+] Creating file catalog [Time: $($($TimeToExecuteSign.Elapsed).Tostring())]" -Color Blue } $SuccessFullSigning = Start-ModuleSigning -Configuration $Configuration -FullModuleTemporaryPath $FullModuleTemporaryPath if ($SuccessFullSigning -eq $false) { return $false } } else { foreach ($Directory in $LinkDirectories) { $Dir = [System.IO.Path]::Combine($FullModuleTemporaryPath, "$Directory") Add-Directory -Directory $Dir } $LinkingFilesTime = Write-Text "[+] Linking files from root and sub directories" -Start Copy-InternalFiles -LinkFiles $LinkFilesRoot -FullModulePath $FullModuleTemporaryPath -FullProjectPath $FullProjectPath Copy-InternalFiles -LinkFiles $LinkPrivatePublicFiles -FullModulePath $FullModuleTemporaryPath -FullProjectPath $FullProjectPath Write-Text -End -Time $LinkingFilesTime } } function Start-ModuleSigning { [CmdletBinding()] param( $Configuration, $FullModuleTemporaryPath ) if ($Configuration.Steps.BuildModule.SignMerged) { Write-TextWithTime -Text 'Applying signature to files' { $registerCertificateSplat = @{ WarningAction = 'SilentlyContinue' WarningVariable = 'Warnings' LocalStore = 'CurrentUser' Path = $FullModuleTemporaryPath Include = @('*.ps1', '*.psd1', '*.psm1', '*.dll', '*.cat') TimeStampServer = 'http://timestamp.digicert.com' } if ($Configuration.Options.Signing) { if ($Configuration.Options.Signing.CertificatePFXBase64) { $Success = Import-ValidCertificate -CertificateAsBase64 $Configuration.Options.Signing.CertificatePFXBase64 -PfxPassword $Configuration.Options.Signing.CertificatePFXPassword if (-not $Success) { return $false } $registerCertificateSplat.Thumbprint = $Success.Thumbprint } elseif ($Configuration.Options.Signing.CertificatePFXPath) { $Success = Import-ValidCertificate -FilePath $Configuration.Options.Signing.CertificatePFXPath -PfxPassword $Configuration.Options.Signing.CertificatePFXPassword if (-not $Success) { return $false } $registerCertificateSplat.Thumbprint = $Success.Thumbprint } else { if ($Configuration.Options.Signing -and $Configuration.Options.Signing.Thumbprint) { $registerCertificateSplat.Thumbprint = $Configuration.Options.Signing.Thumbprint } elseif ($Configuration.Options.Signing -and $Configuration.Options.Signing.CertificateThumbprint) { $registerCertificateSplat.Thumbprint = $Configuration.Options.Signing.CertificateThumbprint } } [Array] $SignedFiles = Register-Certificate @registerCertificateSplat if ($Warnings) { foreach ($W in $Warnings) { Write-Text -Text " [!] $($W.Message)" -Color Red } } if ($SignedFiles.Count -eq 0) { throw "Please configure certificate for use, or disable signing." return $false } else { if ($SignedFiles[0].Thumbprint) { Write-Text -Text " [i] Multiple certificates found for signing:" foreach ($Certificate in $SignedFiles) { Write-Text " [>] Certificate $($Certificate.Thumbprint) with subject: $($Certificate.Subject)" -Color Yellow } throw "Please configure single certificate for use or disable signing." return $false } else { foreach ($File in $SignedFiles) { Write-Text " [>] File $($File.Path) with status: $($File.StatusMessage)" -Color Yellow } } } } } -PreAppend Plus } } function Start-PreparingFunctionsAndAliases { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, $FullProjectPath, $Files, [System.Collections.IDictionary] $CmdletsAliases ) if ($Configuration.Information.Manifest.FunctionsToExport -and $Configuration.Information.Manifest.AliasesToExport -and $Configuration.Information.Manifest.CmdletsToExport) { return $true } $AliasesAndFunctions = Write-TextWithTime -Text 'Preparing function and aliases names' { Get-FunctionAliasesFromFolder -FullProjectPath $FullProjectPath -Files $Files -FunctionsToExport $Configuration.Information.FunctionsToExport -AliasesToExport $Configuration.Information.AliasesToExport } -PreAppend Information Write-TextWithTime -Text "Checking for duplicates in funcions, aliases and cmdlets" { if ($null -eq $Configuration.Information.Manifest.FunctionsToExport) { $Configuration.Information.Manifest.FunctionsToExport = $AliasesAndFunctions.Keys | Where-Object { $_ } if (-not $Configuration.Information.Manifest.FunctionsToExport) { $Configuration.Information.Manifest.FunctionsToExport = @() } } if ($null -eq $Configuration.Information.Manifest.AliasesToExport) { $Configuration.Information.Manifest.AliasesToExport = @( $AliasesAndFunctions.Values | ForEach-Object { $_ } | Where-Object { $_ } $CmdletsAliases.Values.AliasesToExport | ForEach-Object { $_ } | Where-Object { $_ } ) if (-not $Configuration.Information.Manifest.AliasesToExport) { $Configuration.Information.Manifest.AliasesToExport = @() } } if ($null -eq $Configuration.Information.Manifest.CmdletsToExport) { $Configuration.Information.Manifest.CmdletsToExport = @( $CmdletsAliases.Values.CmdletsToExport | ForEach-Object { $_ } | Where-Object { $_ } ) if (-not $Configuration.Information.Manifest.CmdletsToExport) { $Configuration.Information.Manifest.CmdletsToExport = @() } else { $Configuration.Information.Manifest.CmdletsToExport = $Configuration.Information.Manifest.CmdletsToExport } } $FoundDuplicateAliases = $false if ($Configuration.Information.Manifest.AliasesToExport) { $UniqueAliases = $Configuration.Information.Manifest.AliasesToExport | Select-Object -Unique $DiffrenceAliases = Compare-Object -ReferenceObject $Configuration.Information.Manifest.AliasesToExport -DifferenceObject $UniqueAliases foreach ($Alias in $Configuration.Information.Manifest.AliasesToExport) { if ($Alias -in $Configuration.Information.Manifest.FunctionsToExport) { Write-Text " [-] Alias $Alias is also used as function name. Fix it!" -Color Red $FoundDuplicateAliases = $true } if ($Alias -in $Configuration.Information.Manifest.CmdletsToExport) { Write-Text " [-] Alias $Alias is also used as cmdlet name. Fix it!" -Color Red $FoundDuplicateAliases = $true } } foreach ($Alias in $DiffrenceAliases.InputObject) { Write-TextWithTime -Text " [-] Alias $Alias is used multiple times. Fix it!" -Color Red $FoundDuplicateAliases = $true } if ($FoundDuplicateAliases) { return $false } } $FoundDuplicateCmdlets = $false if ($Configuration.Information.Manifest.CmdletsToExport) { $UniqueCmdlets = $Configuration.Information.Manifest.CmdletsToExport | Select-Object -Unique $DiffrenceCmdlets = Compare-Object -ReferenceObject $Configuration.Information.Manifest.CmdletsToExport -DifferenceObject $UniqueCmdlets foreach ($Cmdlet in $Configuration.Information.Manifest.CmdletsToExport) { if ($Cmdlet -in $Configuration.Information.Manifest.FunctionsToExport) { Write-Text " [-] Cmdlet $Cmdlet is also used as function name. Fix it!" -Color Red $FoundDuplicateCmdlets = $true } } foreach ($Cmdlet in $DiffrenceCmdlets.InputObject) { Write-TextWithTime -Text " [-] Cmdlet $Cmdlet is used multiple times. Fix it!" -Color Red $FoundDuplicateCmdlets = $true } if ($FoundDuplicateCmdlets) { return $false } } if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.ScriptsToProcess)) { if ($null -eq $IsWindows -or $IsWindows -eq $true) { $StartsWithEnums = "$($Configuration.Information.ScriptsToProcess)\" } else { $StartsWithEnums = "$($Configuration.Information.ScriptsToProcess)/" } $FilesEnums = @( $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithEnums) } ) if ($FilesEnums.Count -gt 0) { Write-TextWithTime -Text "ScriptsToProcess export $FilesEnums" -PreAppend Plus -SpacesBefore ' ' $Configuration.Information.Manifest.ScriptsToProcess = $FilesEnums } } } -PreAppend Information $AliasesAndFunctions } function Start-PreparingStructure { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [System.Collections.IDictionary] $DestinationPaths, [string] $FullProjectPath, [string] $FullModuleTemporaryPath, [string] $FullTemporaryPath ) Write-TextWithTime -Text "Preparing structure" -PreAppend Information { if ($Configuration.Steps.BuildModule.DeleteBefore -eq $true) { Write-TextWithTime -Text "Deleting old module (Desktop destination) $($DestinationPaths.Desktop)" { $Success = Remove-Directory -Directory $($DestinationPaths.Desktop) -ErrorAction Stop -SpacesBefore " " if ($Success -eq $false) { return $false } } -PreAppend Minus -SpacesBefore " " -Color Blue -ColorError Red -ColorTime Green -ColorBefore Yellow Write-TextWithTime -Text "Deleting old module (Core destination) $($DestinationPaths.Core)" { $Success = Remove-Directory -Directory $($DestinationPaths.Core) -SpacesBefore " " if ($Success -eq $false) { return $false } } -PreAppend Minus -SpacesBefore " " -Color Blue -ColorError Red -ColorTime Green -ColorBefore Yellow } Set-Location -Path $FullProjectPath Write-TextWithTime -Text "Cleaning up temporary path $($FullModuleTemporaryPath)" { $Success = Remove-Directory -Directory $FullModuleTemporaryPath -SpacesBefore " " if ($Success -eq $false) { return $false } Add-Directory -Directory $FullModuleTemporaryPath } -PreAppend Minus -SpacesBefore " " -Color Blue -ColorError Red -ColorTime Green -ColorBefore Yellow Write-TextWithTime -Text "Cleaning up temporary path $($FullTemporaryPath)" { $Success = Remove-Directory -Directory $FullTemporaryPath -SpacesBefore " " if ($Success -eq $false) { return $false } Add-Directory -Directory $FullTemporaryPath } -PreAppend Minus -SpacesBefore " " -Color Blue -ColorError Red -ColorTime Green -ColorBefore Yellow } } function Start-PreparingVariables { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [string] $FullProjectPath ) Write-TextWithTime -Text "Preparing files and folders variables" -PreAppend Plus { $LinkDirectories = @() $LinkPrivatePublicFiles = @() if ($null -ne $Configuration.Information.Exclude) { $Exclude = $Configuration.Information.Exclude } else { $Exclude = '.*', 'Ignore', 'Examples', 'package.json', 'Publish', 'Docs' } if ($null -ne $Configuration.Information.IncludeRoot) { $IncludeFilesRoot = $Configuration.Information.IncludeRoot } else { $IncludeFilesRoot = '*.psm1', '*.psd1', 'License*' } if ($null -ne $Configuration.Information.IncludePS1) { $DirectoriesWithPS1 = $Configuration.Information.IncludePS1 } else { $DirectoriesWithPS1 = 'Classes', 'Private', 'Public', 'Enums' } $DirectoriesWithArrays = $Configuration.Information.IncludeAsArray.Values if ($null -ne $Configuration.Information.IncludeClasses) { $DirectoriesWithClasses = $Configuration.Information.IncludeClasses } else { $DirectoriesWithClasses = 'Classes' } if ($null -ne $Configuration.Information.IncludeAll) { $DirectoriesWithAll = $Configuration.Information.IncludeAll | ForEach-Object { if ($null -eq $IsWindows -or $IsWindows -eq $true) { if ($_.EndsWith('\')) { $_ } else { "$_\" } } else { if ($_.EndsWith('/')) { $_ } else { "$_/" } } } } else { if ($null -eq $IsWindows -or $IsWindows -eq $true) { $DirectoriesWithAll = 'Images\', 'Resources\', 'Templates\', 'Bin\', 'Lib\', 'Data\' } else { $DirectoriesWithAll = 'Images/', 'Resources/', 'Templates/', 'Bin/', 'Lib/', 'Data/' } } $Path = [io.path]::Combine($FullProjectPath, '*') if ($PSEdition -eq 'core') { $Directories = @( $TempDirectories = Get-ChildItem -Path $FullProjectPath -Directory -Exclude $Exclude -FollowSymlink @( $TempDirectories $TempDirectories | Get-ChildItem -Directory -Recurse -FollowSymlink ) ) $Files = Get-ChildItem -Path $FullProjectPath -Exclude $Exclude -FollowSymlink | Get-ChildItem -File -Recurse -FollowSymlink $FilesRoot = Get-ChildItem -Path $Path -Include $IncludeFilesRoot -File -FollowSymlink } else { $Directories = @( $TempDirectories = Get-ChildItem -Path $FullProjectPath -Directory -Exclude $Exclude @( $TempDirectories $TempDirectories | Get-ChildItem -Directory -Recurse ) ) $Files = Get-ChildItem -Path $FullProjectPath -Exclude $Exclude | Get-ChildItem -File -Recurse $FilesRoot = Get-ChildItem -Path $Path -Include $IncludeFilesRoot -File } $LinkDirectories = @( foreach ($Directory in $Directories) { if ($null -eq $IsWindows -or $IsWindows -eq $true) { $RelativeDirectoryPath = (Resolve-Path -LiteralPath $Directory.FullName -Relative).Replace('.\', '') $RelativeDirectoryPath = "$RelativeDirectoryPath\" } else { $RelativeDirectoryPath = (Resolve-Path -LiteralPath $Directory.FullName -Relative).Replace('./', '') $RelativeDirectoryPath = "$RelativeDirectoryPath/" } $RelativeDirectoryPath } ) $AllFiles = foreach ($File in $Files) { if ($null -eq $IsWindows -or $IsWindows -eq $true) { $RelativeFilePath = (Resolve-Path -LiteralPath $File.FullName -Relative).Replace('.\', '') } else { $RelativeFilePath = (Resolve-Path -LiteralPath $File.FullName -Relative).Replace('./', '') } $RelativeFilePath } $RootFiles = foreach ($File in $FilesRoot) { if ($null -eq $IsWindows -or $IsWindows -eq $true) { $RelativeFilePath = (Resolve-Path -LiteralPath $File.FullName -Relative).Replace('.\', '') } else { $RelativeFilePath = (Resolve-Path -LiteralPath $File.FullName -Relative).Replace('./', '') } $RelativeFilePath } $LinkFilesRoot = @( foreach ($File in $RootFiles | Sort-Object -Unique) { switch -Wildcard ($file) { '*.psd1' { $File } '*.psm1' { $File } 'License*' { $File } } } ) $LinkPrivatePublicFiles = @( foreach ($file in $AllFiles | Sort-Object -Unique) { switch -Wildcard ($file) { '*.ps1' { foreach ($dir in $DirectoriesWithPS1) { if ($file -like "$dir*") { $file } } foreach ($dir in $DirectoriesWithArrays) { if ($file -like "$dir*") { $file } } continue } '*.*' { foreach ($dir in $DirectoriesWithAll) { if ($file -like "$dir*") { $file } } continue } } } ) $LinkPrivatePublicFiles = $LinkPrivatePublicFiles | Select-Object -Unique [ordered] @{ LinkDirectories = $LinkDirectories LinkFilesRoot = $LinkFilesRoot LinkPrivatePublicFiles = $LinkPrivatePublicFiles DirectoriesWithClasses = $DirectoriesWithClasses Files = $Files DirectoriesWithPS1 = $DirectoriesWithPS1 } } } function Start-PublishingGallery { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [System.Collections.IDictionary] $ChosenNuget ) if ($ChosenNuget) { $Repository = if ($ChosenNuget.RepositoryName) { $ChosenNuget.RepositoryName } else { 'PSGallery' } Write-TextWithTime -Text "Publishing Module to Gallery ($Repository)" { if ($ChosenNuget.ApiKey) { $publishModuleSplat = @{ Name = $Configuration.Information.ModuleName Repository = $Repository NuGetApiKey = $ChosenNuget.ApiKey Force = $ChosenNuget.Force Verbose = $ChosenNuget.Verbose ErrorAction = 'Stop' } Publish-Module @publishModuleSplat } else { return $false } } -PreAppend Plus } elseif ($Configuration.Steps.PublishModule.Enabled) { Write-TextWithTime -Text "Publishing Module to PowerShellGallery" { if ($Configuration.Options.PowerShellGallery.FromFile) { $ApiKey = Get-Content -Path $Configuration.Options.PowerShellGallery.ApiKey -ErrorAction Stop -Encoding UTF8 } else { $ApiKey = $Configuration.Options.PowerShellGallery.ApiKey } $publishModuleSplat = @{ Name = $Configuration.Information.ModuleName Repository = 'PSGallery' NuGetApiKey = $ApiKey Force = $Configuration.Steps.PublishModule.RequireForce Verbose = if ($Configuration.Steps.PublishModule.PSGalleryVerbose) { $Configuration.Steps.PublishModule.PSGalleryVerbose } else { $false } ErrorAction = 'Stop' } Publish-Module @publishModuleSplat } -PreAppend Plus } } function Start-PublishingGitHub { [cmdletBinding()] param( [System.Collections.IDictionary] $ChosenNuget, [System.Collections.IDictionary] $Configuration, [string] $ProjectName ) if ($ChosenNuget) { [Array] $ListZips = if ($ChosenNuget.Id) { foreach ($Zip in $Configuration.CurrentSettings['Artefact']) { if ($Zip.Id -eq $ChosenNuget.Id) { $ZipPath = $Zip.ZipPath if ($ZipPath -and (Test-Path -LiteralPath $ZipPath)) { $ZipPath } } } } else { if ($Configuration.CurrentSettings['ArtefactDefault']) { $ZipPath = $Configuration.CurrentSettings['ArtefactDefault'].ZipPath if ($ZipPath -and (Test-Path -LiteralPath $ZipPath)) { $ZipPath } } else { } } if ($ChosenNuget.ID) { $TextToUse = "Publishing to GitHub [ID: $($ChosenNuget.Id)] ($ZipPath)" } else { $TextToUse = "Publishing to GitHub ($ZipPath)" } if ($ListZips.Count -gt 0) { Write-TextWithTime -Text $TextToUse -PreAppend Information -ColorBefore Yellow { if ($ZipPath -and (Test-Path -LiteralPath $ZipPath)) { if ($ChosenNuget.OverwriteTagName) { $ModuleName = $Configuration.Information.Manifest.ModuleName $ModuleVersion = $Configuration.Information.Manifest.ModuleVersion if ($Configuration.CurrentSettings.PreRelease) { $ModuleVersionWithPreRelease = "$($ModuleVersion)-$($Configuration.CurrentSettings.PreRelease)" $TagModuleVersionWithPreRelease = "v$($ModuleVersionWithPreRelease)" } else { $ModuleVersionWithPreRelease = $ModuleVersion $TagModuleVersionWithPreRelease = "v$($ModuleVersion)" } $TagNameDefault = "v$($ModuleVersion)" $TagName = $ChosenNuget.OverwriteTagName $TagName = $TagName.Replace('{ModuleName}', $ModuleName) $TagName = $TagName.Replace('<ModuleName>', $ModuleName) $TagName = $TagName.Replace('{ModuleVersion}', $ModuleVersion) $TagName = $TagName.Replace('<ModuleVersion>', $ModuleVersion) $TagName = $TagName.Replace('{ModuleVersionWithPreRelease}', $ModuleVersionWithPreRelease) $TagName = $TagName.Replace('<ModuleVersionWithPreRelease>', $ModuleVersionWithPreRelease) $TagName = $TagName.Replace('{TagModuleVersionWithPreRelease}', $TagModuleVersionWithPreRelease) $TagName = $TagName.Replace('<TagModuleVersionWithPreRelease>', $TagModuleVersionWithPreRelease) $TagName = $TagName.Replace('{TagName}', $TagNameDefault) $TagName = $TagName.Replace('<TagName>', $TagNameDefault) } else { $ModuleVersion = $Configuration.Information.Manifest.ModuleVersion if ($Configuration.CurrentSettings.PreRelease) { $TagName = "v$($ModuleVersion)-$($Configuration.CurrentSettings.PreRelease)" } else { $TagName = "v$($ModuleVersion)" } } if ($Configuration.CurrentSettings.Prerelease) { if ($ChosenNuget.DoNotMarkAsPreRelease) { $IsPreRelease = $false } else { $IsPreRelease = $true } } else { $IsPreRelease = $false } $sendGitHubReleaseSplat = [ordered] @{ GitHubUsername = $ChosenNuget.UserName GitHubRepositoryName = if ($ChosenNuget.RepositoryName) { $ChosenNuget.RepositoryName } else { $ProjectName } GitHubAccessToken = $ChosenNuget.ApiKey TagName = $TagName AssetFilePaths = $ListZips IsPreRelease = $IsPreRelease Verbose = if ($ChosenNuget.Verbose) { $ChosenNuget.Verbose } else { $false } } $StatusGithub = Send-GitHubRelease @sendGitHubReleaseSplat if ($StatusGithub.ReleaseCreationSucceeded -and $statusGithub.Succeeded) { $GithubColor = 'Green' $GitHubText = '+' } else { $GithubColor = 'Red' $GitHubText = '-' } Write-Text "[$GitHubText] GitHub Release Creation Status: $($StatusGithub.ReleaseCreationSucceeded)" -Color $GithubColor Write-Text "[$GitHubText] GitHub Release Succeeded: $($statusGithub.Succeeded)" -Color $GithubColor Write-Text "[$GitHubText] GitHub Release Asset Upload Succeeded: $($statusGithub.AllAssetUploadsSucceeded)" -Color $GithubColor Write-Text "[$GitHubText] GitHub Release URL: $($statusGitHub.ReleaseUrl)" -Color $GithubColor if ($statusGithub.ErrorMessage) { Write-Text "[$GitHubText] GitHub Release ErrorMessage: $($statusGithub.ErrorMessage)" -Color $GithubColor return $false } } else { Write-Text " [e] GitHub Release Creation Status: Failed" -Color Red Write-Text " [e] GitHub Release Creation Reason: $ZipPath doesn't exists. Most likely Releases option is disabled." -Color Red return $false } } } else { Write-Text -Text "[-] Publishing to GitHub failed. No ZIPs to process." -Color Red return $false } } else { if (-not $Configuration.CurrentSettings.ArtefactZipPath -or -not (Test-Path -LiteralPath $Configuration.CurrentSettings.ArtefactZipPath)) { Write-Text -Text "[-] Publishing to GitHub failed. File $($Configuration.CurrentSettings.ArtefactZipPath) doesn't exists" -Color Red return $false } $TagName = "v$($Configuration.Information.Manifest.ModuleVersion)" $ZipPath = $Configuration.CurrentSettings.ArtefactZipPath if ($Configuration.Options.GitHub.FromFile) { $GitHubAccessToken = Get-Content -LiteralPath $Configuration.Options.GitHub.ApiKey -Encoding UTF8 } else { $GitHubAccessToken = $Configuration.Options.GitHub.ApiKey } if ($GitHubAccessToken) { if ($Configuration.Options.GitHub.RepositoryName) { $GitHubRepositoryName = $Configuration.Options.GitHub.RepositoryName } else { $GitHubRepositoryName = $ProjectName } Write-TextWithTime -Text "Publishing to GitHub ($ZipPath)" -PreAppend Information -ColorBefore Yellow { if (Test-Path -LiteralPath $ZipPath) { if ($Configuration.Steps.PublishModule.Prerelease) { $IsPreRelease = $true } else { $IsPreRelease = $false } $sendGitHubReleaseSplat = [ordered] @{ GitHubUsername = $Configuration.Options.GitHub.UserName GitHubRepositoryName = $GitHubRepositoryName GitHubAccessToken = $GitHubAccessToken TagName = $TagName AssetFilePaths = $ZipPath IsPreRelease = $IsPreRelease Verbose = if ($Configuration.Steps.PublishModule.GitHubVerbose) { $Configuration.Steps.PublishModule.GitHubVerbose } else { $false } } $StatusGithub = Send-GitHubRelease @sendGitHubReleaseSplat if ($StatusGithub.ReleaseCreationSucceeded -and $statusGithub.Succeeded) { $GithubColor = 'Green' $GitHubText = '>' } else { $GithubColor = 'Red' $GitHubText = '-' } Write-Text " [$GitHubText] GitHub Release Creation Status: $($StatusGithub.ReleaseCreationSucceeded)" -Color $GithubColor Write-Text " [$GitHubText] GitHub Release Succeeded: $($statusGithub.Succeeded)" -Color $GithubColor Write-Text " [$GitHubText] GitHub Release Asset Upload Succeeded: $($statusGithub.AllAssetUploadsSucceeded)" -Color $GithubColor Write-Text " [$GitHubText] GitHub Release URL: $($statusGitHub.ReleaseUrl)" -Color $GithubColor if ($statusGithub.ErrorMessage) { Write-Text "[$GitHubText] GitHub Release ErrorMessage: $($statusGithub.ErrorMessage)" -Color $GithubColor return $false } } else { Write-Text " [e] GitHub Release Creation Status: Failed" -Color Red Write-Text " [e] GitHub Release Creation Reason: $ZipPath doesn't exists. Most likely Releases option is disabled." -Color Red return $false } } } } } function Step-Version { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER Module Parameter description .PARAMETER ExpectedVersion Parameter description .PARAMETER Advanced Parameter description .EXAMPLE Step-Version -Module Testimo12 -ExpectedVersion '0.1.X' Step-Version -ExpectedVersion '0.1.X' Step-Version -ExpectedVersion '0.1.5.X' Step-Version -ExpectedVersion '1.2.X' Step-Version -Module PSWriteHTML -ExpectedVersion '0.0.X' Step-Version -Module PSWriteHTML1 -ExpectedVersion '0.1.X' Step-Version -Module PSPublishModule -ExpectedVersion '0.9.X' -Advanced -LocalPSD1 "C:\Support\GitHub\PSPublishModule\PSPublishModule.psd1" .NOTES General notes #> [cmdletBinding()] param( [string] $Module, [Parameter(Mandatory)][string] $ExpectedVersion, [switch] $Advanced, [string] $LocalPSD1 ) $Version = $null $VersionCheck = [version]::TryParse($ExpectedVersion, [ref] $Version) if ($VersionCheck) { @{ Version = $ExpectedVersion CurrentVersion = 'Not aquired, no auto versioning.' } } else { if ($Module) { if (-not $LocalPSD1) { try { $ModuleGallery = Find-Module -Name $Module -ErrorAction Stop -Verbose:$false -WarningAction SilentlyContinue $CurrentVersion = [version] $ModuleGallery.Version } catch { $CurrentVersion = [version] $null } } else { if (Test-Path -LiteralPath $LocalPSD1) { $PSD1Data = Import-PowerShellDataFile -Path $LocalPSD1 if ($PSD1Data.ModuleVersion) { try { $CurrentVersion = [version] $PSD1Data.ModuleVersion } catch { Write-Warning -Message "Couldn't parse version $($PSD1Data.ModuleVersion) from PSD1 file $LocalPSD1" $CurrentVersion = $null } } } else { Write-Warning -Message "Couldn't find local PSD1 file $LocalPSD1" $CurrentVersion = $null } } } else { $CurrentVersion = $null } $Splitted = $ExpectedVersion.Split('.') $PreparedVersion = [ordered] @{ Major = $Splitted[0] Minor = $Splitted[1] Build = $Splitted[2] Revision = $Splitted[3] } [string] $StepType = foreach ($Key in $PreparedVersion.Keys) { if ($PreparedVersion[$Key] -eq 'X') { $Key break } } if ($null -eq $CurrentVersion) { $PreparedVersion.$StepType = 1 } else { $PreparedVersion.$StepType = $CurrentVersion.$StepType } if ([version] (($PreparedVersion.Values | Where-Object { $null -ne $_ }) -join '.') -gt $CurrentVersion) { $PreparedVersion.$StepType = 0 } while ([version] (($PreparedVersion.Values | Where-Object { $null -ne $_ }) -join '.') -le $CurrentVersion) { $PreparedVersion.$StepType = $PreparedVersion.$StepType + 1 } $ProposedVersion = ([version] (($PreparedVersion.Values | Where-Object { $null -ne $_ }) -join '.')).ToString() $FinalVersion = $null $VersionCheck = [version]::TryParse($ProposedVersion, [ref] $FinalVersion) if ($VersionCheck) { if ($Advanced) { [ordered] @{ Version = $ProposedVersion CurrentVersion = $CurrentVersion } } else { $ProposedVersion } } else { throw "Couldn't properly verify version is version. Terminating." } } } function Test-AllFilePathsAndThrowErrorIfOneIsNotValid { [CmdletBinding()] param( [string[]] $filePaths ) foreach ($filePath in $filePaths) { [bool] $fileWasNotFoundAtPath = [string]::IsNullOrEmpty($filePath) -or !(Test-Path -Path $filePath -PathType Leaf) if ($fileWasNotFoundAtPath) { throw "There is no file at the specified path, '$filePath'." } } } function Test-ReparsePoint { [CmdletBinding()] param ( [string]$path ) $file = Get-Item $path -Force -ea SilentlyContinue return [bool]($file.Attributes -band [IO.FileAttributes]::ReparsePoint) } function Update-VersionInBuildScript { <# .SYNOPSIS Updates the version in the Build-Module.ps1 script. .DESCRIPTION Modifies the ModuleVersion entry in the Build-Module.ps1 script with the new version. .PARAMETER ScriptFile Path to the Build-Module.ps1 file. .PARAMETER Version The new version string to set. #> [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory = $true)] [string]$ScriptFile, [Parameter(Mandatory = $true)] [string]$Version, [System.Collections.IDictionary] $CurrentVersionHash ) if (!(Test-Path -Path $ScriptFile)) { Write-Warning "Build script file not found: $ScriptFile" return $false } $CurrentFileVersion = $CurrentVersionHash[$ScriptFile] try { $content = Get-Content -Path $ScriptFile -Raw $newContent = $content -replace "ModuleVersion\s*=\s*['""][\d\.]+['""]", "ModuleVersion = '$Version'" if ($content -eq $newContent) { Write-Verbose "No version change needed for $ScriptFile," return $true } Write-Verbose -Message "Updating version in $ScriptFile from '$CurrentFileVersion' to '$Version'" if ($PSCmdlet.ShouldProcess("Build script $ScriptFile", "Update version from '$CurrentFileVersion' to '$Version'")) { $newContent | Set-Content -Path $ScriptFile -NoNewline Write-Host "Updated version in $ScriptFile to $Version" -ForegroundColor Green } return $true } catch { Write-Error "Error updating build script $ScriptFile`: $_" return $false } } function Update-VersionInCsProj { <# .SYNOPSIS Updates the version in a .csproj file. .DESCRIPTION Modifies the VersionPrefix element in a .csproj file with the new version. .PARAMETER ProjectFile Path to the .csproj file. .PARAMETER Version The new version string to set. #> [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory = $true)] [string]$ProjectFile, [Parameter(Mandatory = $true)] [string]$Version, [System.Collections.IDictionary] $CurrentVersionHash ) if (!(Test-Path -Path $ProjectFile)) { Write-Warning "Project file not found: $ProjectFile" return $false } $CurrentFileVersion = $CurrentVersionHash[$ProjectFile] try { $content = Get-Content -Path $ProjectFile -Raw $newContent = $content -replace '<VersionPrefix>[\d\.]+<\/VersionPrefix>', "<VersionPrefix>$Version</VersionPrefix>" if ($content -eq $newContent) { Write-Verbose "No version change needed for $ProjectFile" return $true } Write-Verbose -Message "Updating version in $ProjectFile from '$CurrentFileVersion' to '$Version'" if ($PSCmdlet.ShouldProcess("Project file $ProjectFile", "Update version from '$CurrentFileVersion' to '$Version'")) { $newContent | Set-Content -Path $ProjectFile -NoNewline Write-Host "Updated version in $ProjectFile to $Version" -ForegroundColor Green } return $true } catch { Write-Error "Error updating project file $ProjectFile`: $_" return $false } } function Update-VersionInPsd1 { <# .SYNOPSIS Updates the version in a PowerShell module manifest (.psd1) file. .DESCRIPTION Modifies the ModuleVersion entry in a module manifest with the new version. .PARAMETER ManifestFile Path to the .psd1 file. .PARAMETER Version The new version string to set. #> [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory = $true)] [string]$ManifestFile, [Parameter(Mandatory = $true)] [string]$Version, [System.Collections.IDictionary] $CurrentVersionHash ) if (!(Test-Path -Path $ManifestFile)) { Write-Warning "Module manifest file not found: $ManifestFile" return $false } $CurrentFileVersion = $CurrentVersionHash[$ManifestFile] try { $content = Get-Content -Path $ManifestFile -Raw $newContent = $content -replace "ModuleVersion\s*=\s*['""][\d\.]+['""]", "ModuleVersion = '$Version'" if ($content -eq $newContent) { Write-Verbose "No version change needed for $ManifestFile" return $true } Write-Verbose -Message "Updating version in $ManifestFile from '$CurrentFileVersion' to '$Version'" if ($PSCmdlet.ShouldProcess("Module manifest $ManifestFile", "Update version from '$CurrentFileVersion' to '$Version'")) { $newContent | Set-Content -Path $ManifestFile -NoNewline Write-Host "Updated version in $ManifestFile to $Version" -ForegroundColor Green } return $true } catch { Write-Error "Error updating module manifest $ManifestFile`: $_" return $false } } function Update-VersionNumber { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$Version, [Parameter(Mandatory = $true)] [ValidateSet('Major', 'Minor', 'Build', 'Revision')] [string]$Type ) $versionParts = $Version -split '\.' while ($versionParts.Count -lt 3) { $versionParts += "0" } if ($Type -eq 'Revision' -and $versionParts.Count -lt 4) { $versionParts += "0" } switch ($Type) { 'Major' { $versionParts[0] = [string]([int]$versionParts[0] + 1) $versionParts[1] = "0" $versionParts[2] = "0" if ($versionParts.Count -gt 3) { $versionParts[3] = "0" } } 'Minor' { $versionParts[1] = [string]([int]$versionParts[1] + 1) $versionParts[2] = "0" if ($versionParts.Count -gt 3) { $versionParts[3] = "0" } } 'Build' { $versionParts[2] = [string]([int]$versionParts[2] + 1) if ($versionParts.Count -gt 3) { $versionParts[3] = "0" } } 'Revision' { if ($versionParts.Count -lt 4) { $versionParts += "1" } else { $versionParts[3] = [string]([int]$versionParts[3] + 1) } } } $newVersion = $versionParts -join '.' $versionPartCount = ($Version -split '\.' | Measure-Object).Count if ($versionPartCount -eq 3 -and $Type -ne 'Revision') { $newVersion = ($versionParts | Select-Object -First 3) -join '.' } return $newVersion } function Write-PowerShellHashtable { <# .SYNOPSIS Takes an creates a script to recreate a hashtable .DESCRIPTION Allows you to take a hashtable and create a hashtable you would embed into a script. Handles nested hashtables and indents nested hashtables automatically. .PARAMETER InputObject The hashtable to turn into a script .PARAMETER AsScriptBlock Determines if a string or a scriptblock is returned .PARAMETER Sort Sorts the hashtable alphabetically .EXAMPLE # Corrects the presentation of a PowerShell hashtable @{Foo='Bar';Baz='Bing';Boo=@{Bam='Blang'}} | Write-PowerShellHashtable .NOTES Original idea: https://github.com/StartAutomating/Pipeworks Modifications by: Przemyslaw Klys #> [cmdletbinding()] [OutputType([string], [ScriptBlock])] param( [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)][PSObject] $InputObject, # Returns the content as a script block, rather than a string [Alias('ScriptBlock')][switch]$AsScriptBlock, # If set, items in the hashtable will be sorted alphabetically [Switch]$Sort ) process { $callstack = @(foreach ($_ in (Get-PSCallStack)) { if ($_.Command -eq "Write-PowerShellHashtable") { $_ } }) $depth = $callStack.Count if ($inputObject -isnot [System.Collections.IDictionary]) { $newInputObject = @{ PSTypeName = @($inputobject.pstypenames)[-1] } foreach ($prop in $inputObject.psobject.properties) { $newInputObject[$prop.Name] = $prop.Value } $inputObject = $newInputObject } if ($inputObject -is [System.Collections.IDictionary]) { $scriptString = "" $indent = $depth * 4 $scriptString += "@{ " $items = $inputObject.GetEnumerator() if ($Sort) { $items = $items | Sort-Object Key } foreach ($kv in $items) { $scriptString += " " * $indent $keyString = "$($kv.Key)" if ($keyString.IndexOfAny(" _.#-+:;()'!?^@#$%&".ToCharArray()) -ne -1) { if ($keyString.IndexOf("'") -ne -1) { $scriptString += "'$($keyString.Replace("'","''"))'=" } else { $scriptString += "'$keyString'=" } } elseif ($keyString) { $scriptString += "$keyString=" } $value = $kv.Value if ($value -is [string]) { $value = "'" + $value.Replace("'", "''").Replace("’", "’’").Replace("‘", "‘‘") + "'" } elseif ($value -is [ScriptBlock]) { $value = "{$value}" } elseif ($value -is [switch]) { $value = if ($value) { '$true' } else { '$false' } } elseif ($value -is [DateTime]) { $value = if ($value) { "[DateTime]'$($value.ToString("o"))'" } } elseif ($value -is [bool]) { $value = if ($value) { '$true' } else { '$false' } } elseif ($value -is [System.Collections.IList] -and $value.Count -eq 0) { $value = '@()' } elseif ($value -is [System.Collections.IList] -and $value.Count -gt 0) { $value = foreach ($v in $value) { if ($v -is [System.Collections.IDictionary]) { Write-PowerShellHashtable $v -Sort:$Sort.IsPresent } elseif ($v -is [Object] -and $v -isnot [string]) { Write-PowerShellHashtable $v -Sort:$Sort.IsPresent } else { ("'" + "$v".Replace("'", "''").Replace("’", "’’").Replace("‘", "‘‘") + "'") } } $oldOfs = $ofs $ofs = ",$(' ' * ($indent + 4))" $value = "@($value)" $ofs = $oldOfs } elseif ($value -as [System.Collections.IDictionary[]]) { $value = foreach ($v in $value) { Write-PowerShellHashtable $v -Sort:$Sort.IsPresent } $value = $value -join "," } elseif ($value -is [System.Collections.IDictionary]) { $value = "$(Write-PowerShellHashtable $value -Sort:$Sort.IsPresent)" } elseif ($value -as [Double]) { $value = "$value" } else { $valueString = "'$value'" if ($valueString[0] -eq "'" -and $valueString[1] -eq "@" -and $valueString[2] -eq "{") { $value = Write-PowerShellHashtable -InputObject $value -Sort:$Sort.IsPresent } else { $value = $valueString } } $scriptString += "$value " } $scriptString += " " * ($depth - 1) * 4 $scriptString += "}" if ($AsScriptBlock) { [ScriptBlock]::Create($scriptString) } else { $scriptString } } } } function Write-Text { [CmdletBinding()] param( [Parameter(Position = 0)][string] $Text, [System.ConsoleColor] $Color, [System.ConsoleColor] $ColorBefore, [System.ConsoleColor] $ColorTime, [switch] $Start, [switch] $End, [System.Diagnostics.Stopwatch] $Time, [ValidateSet('Plus', 'Minus', 'Information', 'Addition', 'Error')][string] $PreAppend, [string] $SpacesBefore ) if ($PreAppend) { if ($PreAppend -eq "Information") { $TextBefore = "$SpacesBefore[i] " if (-not $ColorBefore) { $ColorBefore = [System.ConsoleColor]::Yellow } } elseif ($PreAppend -eq 'Minus') { $TextBefore = "$SpacesBefore[-] " if (-not $ColorBefore) { $ColorBefore = [System.ConsoleColor]::Red } } elseif ($PreAppend -eq 'Plus') { $TextBefore = "$SpacesBefore[+] " if (-not $ColorBefore) { $ColorBefore = [System.ConsoleColor]::Cyan } } elseif ($PreAppend -eq 'Addition') { $TextBefore = "$SpacesBefore[>] " if (-not $ColorBefore) { $ColorBefore = [System.ConsoleColor]::Yellow } } elseif ($PreAppend -eq 'Error') { $TextBefore = "$SpacesBefore[e] " if (-not $ColorBefore) { $ColorBefore = [System.ConsoleColor]::Red } if (-not $Color) { $Color = [System.ConsoleColor]::Red } } Write-Host -Object "$TextBefore" -NoNewline -ForegroundColor $ColorBefore } if (-not $Color) { $Color = [System.ConsoleColor]::Cyan } if (-not $ColorTime) { $ColorTime = [System.ConsoleColor]::Green } if (-not $Start -and -not $End) { Write-Host "$Text" -ForegroundColor $Color } if ($Start) { Write-Host "$Text" -NoNewline -ForegroundColor $Color $Time = [System.Diagnostics.Stopwatch]::StartNew() } if ($End) { $TimeToExecute = $Time.Elapsed.ToString() Write-Host " [Time: $TimeToExecute]" -ForegroundColor $ColorTime $Time.Stop() } else { if ($Time) { return $Time } } } function Write-TextWithTime { [CmdletBinding()] param( [ScriptBlock] $Content, [ValidateSet('Plus', 'Minus', 'Information', 'Addition')][string] $PreAppend, [string] $Text, [switch] $Continue, [System.ConsoleColor] $Color = [System.ConsoleColor]::Cyan, [System.ConsoleColor] $ColorTime = [System.ConsoleColor]::Green, [System.ConsoleColor] $ColorError = [System.ConsoleColor]::Red, [System.ConsoleColor] $ColorBefore, [string] $SpacesBefore ) if ($PreAppend) { if ($PreAppend -eq "Information") { $TextBefore = "$SpacesBefore[i] " if (-not $ColorBefore) { $ColorBefore = [System.ConsoleColor]::Yellow } } elseif ($PreAppend -eq 'Minus') { $TextBefore = "$SpacesBefore[-] " if (-not $ColorBefore) { $ColorBefore = [System.ConsoleColor]::Red } } elseif ($PreAppend -eq 'Plus') { $TextBefore = "$SpacesBefore[+] " if (-not $ColorBefore) { $ColorBefore = [System.ConsoleColor]::Cyan } } elseif ($PreAppend -eq 'Addition') { $TextBefore = "$SpacesBefore[>] " if (-not $ColorBefore) { $ColorBefore = [System.ConsoleColor]::Yellow } } Write-Host -Object "$TextBefore" -NoNewline -ForegroundColor $ColorBefore Write-Host -Object "$Text" -ForegroundColor $Color } else { Write-Host -Object "$Text" -ForegroundColor $Color } $Time = [System.Diagnostics.Stopwatch]::StartNew() if ($null -ne $Content) { try { $InputData = & $Content if ($InputData -contains $false) { $ErrorMessage = "Failure in action above. Check output above." } else { $InputData } } catch { $ErrorMessage = $_.Exception.Message + " (File: $($_.InvocationInfo.ScriptName), Line: " + $_.InvocationInfo.ScriptLineNumber + ")" } } $TimeToExecute = $Time.Elapsed.ToString() if ($ErrorMessage) { Write-Host -Object "$SpacesBefore[e] $Text [Error: $ErrorMessage]" -ForegroundColor $ColorError if ($PreAppend) { Write-Host -Object "$($TextBefore)" -NoNewline -ForegroundColor $ColorError } Write-Host -Object "$Text [Time: $TimeToExecute]" -ForegroundColor $ColorError $Time.Stop() return $false break } else { if ($PreAppend) { Write-Host -Object "$($TextBefore)" -NoNewline -ForegroundColor $ColorBefore } Write-Host -Object "$Text [Time: $TimeToExecute]" -ForegroundColor $ColorTime } if (-not $Continue) { $Time.Stop() } } function Convert-CommandsToList { [cmdletbinding()] param( [parameter(Mandatory)][string] $ModuleName, [string[]] $CommandTypes ) $Commands = Get-Command -Module $ModuleName $CommandsOnly = $Commands | Where-Object { $_.CommandType -eq 'Function' } $List = [ordered] @{} foreach ($Command in $CommandsOnly) { if ($Command.Name.StartsWith('Get')) { $CommandType = 'Get' } elseif ($Command.Name.StartsWith('Set')) { $CommandType = 'Set' } else { $CommandType = 'Other' } if ($CommandType -ne 'Other') { $Name = $Command.Name.Replace("Get-", '').Replace("Set-", '') if (-not $List[$Name]) { $List[$Name] = [PSCustomObject] @{ Get = if ($CommandType -eq 'Get') { $Command.Name } else { '' } Set = if ($CommandType -eq 'Set') { $Command.Name } else { '' } } } else { $List[$Name].$CommandType = $Command.Name } } } $List.Values } function Convert-ProjectEncoding { <# .SYNOPSIS Converts encoding for all source files in a project directory with comprehensive safety features. .DESCRIPTION Recursively converts encoding for PowerShell, C#, and other source code files in a project directory. Includes comprehensive safety features: WhatIf support, automatic backups, rollback protection, and detailed reporting. Designed specifically for development projects with intelligent file type detection. .PARAMETER Path Path to the project directory to process. .PARAMETER ProjectType Type of project to process. Determines which file extensions are included. Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom' .PARAMETER CustomExtensions Custom file extensions to process when ProjectType is 'Custom'. Example: @('*.ps1', '*.psm1', '*.cs', '*.vb') .PARAMETER SourceEncoding Expected source encoding of files. When specified, only files with this encoding will be converted. When not specified (or set to 'Any'), files with any encoding except the target encoding will be converted. .PARAMETER TargetEncoding Target encoding for conversion. Default is 'UTF8BOM' for PowerShell projects (PS 5.1 compatibility), 'UTF8' for others. .PARAMETER ExcludeDirectories Directory names to exclude from processing (e.g., '.git', 'bin', 'obj'). .PARAMETER CreateBackups Create backup files before conversion for additional safety. .PARAMETER BackupDirectory Directory to store backup files. If not specified, backups are created alongside original files. .PARAMETER Force Convert files even when their detected encoding doesn't match SourceEncoding. .PARAMETER NoRollbackOnMismatch Skip rolling back changes when content verification fails. .PARAMETER PassThru Return detailed results for each processed file. .EXAMPLE Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType PowerShell -WhatIf Preview encoding conversion for a PowerShell project (will convert from ANY encoding to UTF8BOM by default). .EXAMPLE Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType PowerShell -TargetEncoding UTF8BOM Convert ALL files in a PowerShell project to UTF8BOM regardless of their current encoding. .EXAMPLE Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType Mixed -SourceEncoding ASCII -TargetEncoding UTF8BOM -CreateBackups Convert ONLY ASCII files in a mixed project to UTF8BOM with backups. .EXAMPLE Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType CSharp -TargetEncoding UTF8 -PassThru Convert ALL files in a C# project to UTF8 without BOM and return detailed results. .NOTES File type mappings: - PowerShell: *.ps1, *.psm1, *.psd1, *.ps1xml - CSharp: *.cs, *.csx, *.csproj, *.sln, *.config, *.json, *.xml - Mixed: Combination of PowerShell and CSharp - All: Common source code extensions including JS, Python, etc. PowerShell Encoding Recommendations: - UTF8BOM is recommended for PowerShell files to ensure PS 5.1 compatibility - UTF8 without BOM can cause PS 5.1 to misinterpret files as ASCII - This can lead to broken special characters and module loading issues - UTF8BOM ensures proper encoding detection across all PowerShell versions #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ProjectType')] param( [Parameter(Mandatory)] [string] $Path, [Parameter(ParameterSetName = 'ProjectType')] [ValidateSet('PowerShell', 'CSharp', 'Mixed', 'All')] [string] $ProjectType = 'Mixed', [Parameter(ParameterSetName = 'Custom', Mandatory)] [string[]] $CustomExtensions, [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM', 'Any')] [string] $SourceEncoding = 'Any', [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')] [string] $TargetEncoding, [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode'), [switch] $CreateBackups, [string] $BackupDirectory, [switch] $Force, [switch] $NoRollbackOnMismatch, [switch] $PassThru ) if (-not (Test-Path -LiteralPath $Path -PathType Container)) { throw "Project path '$Path' not found or is not a directory" } $extensionMappings = @{ 'PowerShell' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml') 'CSharp' = @('*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.resx') 'Mixed' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml') 'All' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.js', '*.ts', '*.py', '*.rb', '*.java', '*.cpp', '*.h', '*.hpp', '*.sql', '*.md', '*.txt', '*.yaml', '*.yml') } if ($PSCmdlet.ParameterSetName -eq 'Custom') { $filePatterns = $CustomExtensions } else { $filePatterns = $extensionMappings[$ProjectType] } Write-Verbose "Processing project type: $ProjectType with patterns: $($filePatterns -join ', ')" if (-not $PSBoundParameters.ContainsKey('TargetEncoding')) { switch ($ProjectType) { 'PowerShell' { $TargetEncoding = 'UTF8BOM' } 'Mixed' { $TargetEncoding = 'UTF8BOM' } default { $TargetEncoding = 'UTF8' } } Write-Verbose "Using default TargetEncoding '$TargetEncoding' for project type '$ProjectType'" } if ($CreateBackups -and $BackupDirectory) { if (-not (Test-Path -LiteralPath $BackupDirectory)) { New-Item -Path $BackupDirectory -ItemType Directory -Force | Out-Null Write-Verbose "Created backup directory: $BackupDirectory" } } $target = Resolve-Encoding -Name $TargetEncoding $source = if ($SourceEncoding -eq 'Any') { $null } else { Resolve-Encoding -Name $SourceEncoding } $allFiles = @() foreach ($pattern in $filePatterns) { $params = @{ Path = $Path Filter = $pattern File = $true Recurse = $true } $files = Get-ChildItem @params | Where-Object { $file = $_ $excluded = $false foreach ($excludeDir in $ExcludeDirectories) { if ($file.DirectoryName -like "*\$excludeDir" -or $file.DirectoryName -like "*\$excludeDir\*") { $excluded = $true break } } -not $excluded } $allFiles += $files } $uniqueFiles = $allFiles | Sort-Object FullName | Get-Unique -AsString Write-Host "Found $($uniqueFiles.Count) files to process" -ForegroundColor Green if ($uniqueFiles.Count -eq 0) { Write-Warning "No files found matching the specified criteria" return } $results = @() $converted = 0 $skipped = 0 $errors = 0 foreach ($file in $uniqueFiles) { try { if ($SourceEncoding -eq 'Any') { $currentEncodingObj = Get-FileEncoding -Path $file.FullName -AsObject $currentEncodingName = $currentEncodingObj.EncodingName $targetName = if ($target -is [System.Text.UTF8Encoding] -and $target.GetPreamble().Length -eq 3) { 'UTF8BOM' } elseif ($target -is [System.Text.UTF8Encoding]) { 'UTF8' } elseif ($target -is [System.Text.UnicodeEncoding]) { 'Unicode' } elseif ($target -is [System.Text.UTF7Encoding]) { 'UTF7' } elseif ($target -is [System.Text.UTF32Encoding]) { 'UTF32' } elseif ($target -is [System.Text.ASCIIEncoding]) { 'ASCII' } elseif ($target -is [System.Text.BigEndianUnicodeEncoding]) { 'BigEndianUnicode' } else { $target.WebName } if ($currentEncodingName -eq $targetName) { Write-Verbose "Skipping $($file.FullName) because encoding is already '$targetName'." $results += @{ FilePath = $file.FullName Status = 'Skipped' Reason = "Already target encoding '$targetName'" DetectedEncoding = $currentEncodingName } $skipped++ continue } $convertParams = @{ FilePath = $file.FullName SourceEncoding = $currentEncodingObj.Encoding TargetEncoding = $target Force = $true NoRollbackOnMismatch = $NoRollbackOnMismatch WhatIf = $WhatIfPreference } } else { $convertParams = @{ FilePath = $file.FullName SourceEncoding = $source TargetEncoding = $target Force = $Force NoRollbackOnMismatch = $NoRollbackOnMismatch WhatIf = $WhatIfPreference } } if ($CreateBackups) { $convertParams.CreateBackup = $true } $result = Convert-FileEncodingSingle @convertParams if ($result) { $results += $result switch ($result.Status) { 'Converted' { $converted++ } 'Skipped' { $skipped++ } 'Error' { $errors++ } 'Failed' { $errors++ } } if ($CreateBackups -and $BackupDirectory -and $result.BackupPath -and (Test-Path $result.BackupPath)) { $relativePath = Get-RelativePath -From $Path -To $file.FullName $backupTargetPath = Join-Path $BackupDirectory $relativePath $backupTargetDir = Split-Path $backupTargetPath -Parent if (-not (Test-Path $backupTargetDir)) { New-Item -Path $backupTargetDir -ItemType Directory -Force | Out-Null } Move-Item -Path $result.BackupPath -Destination $backupTargetPath -Force $result.BackupPath = $backupTargetPath } } } catch { Write-Warning "Unexpected error processing $($file.FullName): $_" $errors++ } } $summary = @{ TotalFiles = $uniqueFiles.Count Converted = $converted Skipped = $skipped Errors = $errors SourceEncoding = $SourceEncoding TargetEncoding = $TargetEncoding ProjectPath = $Path ProjectType = if ($PSCmdlet.ParameterSetName -eq 'Custom') { "Custom ($($CustomExtensions -join ', '))" } else { $ProjectType } } Write-Host "`nConversion Summary:" -ForegroundColor Cyan Write-Host " Total files processed: $($summary.TotalFiles)" -ForegroundColor White Write-Host " Successfully converted: $($summary.Converted)" -ForegroundColor Green Write-Host " Skipped: $($summary.Skipped)" -ForegroundColor Yellow Write-Host " Errors: $($summary.Errors)" -ForegroundColor Red Write-Host " Encoding: $($summary.SourceEncoding) → $($summary.TargetEncoding)" -ForegroundColor White if ($PassThru) { [PSCustomObject]@{ Summary = $summary Results = $results } } } function Convert-ProjectLineEnding { <# .SYNOPSIS Converts line endings for all source files in a project directory with comprehensive safety features. .DESCRIPTION Recursively converts line endings for PowerShell, C#, and other source code files in a project directory. Includes comprehensive safety features: WhatIf support, automatic backups, rollback protection, and detailed reporting. Can convert between CRLF (Windows), LF (Unix/Linux), and fix mixed line endings. .PARAMETER Path Path to the project directory to process. .PARAMETER ProjectType Type of project to process. Determines which file extensions are included. Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom' .PARAMETER CustomExtensions Custom file extensions to process when ProjectType is 'Custom'. Example: @('*.ps1', '*.psm1', '*.cs', '*.vb') .PARAMETER TargetLineEnding Target line ending style. Valid values: 'CRLF', 'LF' .PARAMETER ExcludeDirectories Directory names to exclude from processing (e.g., '.git', 'bin', 'obj'). .PARAMETER CreateBackups Create backup files before conversion for additional safety. .PARAMETER BackupDirectory Directory to store backup files. If not specified, backups are created alongside original files. .PARAMETER Force Convert all files regardless of current line ending type. .PARAMETER EnsureFinalNewline Ensure all files end with a newline character (POSIX compliance). .PARAMETER OnlyMissingNewline Only process files that are missing final newlines, leave others unchanged. .PARAMETER PassThru Return detailed results for each processed file. .EXAMPLE Convert-ProjectLineEnding -Path 'C:\MyProject' -ProjectType PowerShell -TargetLineEnding CRLF -WhatIf Preview what files would be converted to Windows-style line endings. .EXAMPLE Convert-ProjectLineEnding -Path 'C:\MyProject' -ProjectType Mixed -TargetLineEnding LF -CreateBackups Convert a mixed project to Unix-style line endings with backups. .EXAMPLE Convert-ProjectLineEnding -Path 'C:\MyProject' -ProjectType All -OnlyMixed -PassThru Fix only files with mixed line endings and return detailed results. .NOTES This function modifies files in place. Always use -WhatIf first or -CreateBackups for safety. Line ending types: - CRLF: Windows style (\\r\\n) - LF: Unix/Linux style (\\n) #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string] $Path, [ValidateSet('PowerShell', 'CSharp', 'Mixed', 'All', 'Custom')] [string] $ProjectType = 'Mixed', [string[]] $CustomExtensions, [Parameter(Mandatory)] [ValidateSet('CRLF', 'LF')] [string] $TargetLineEnding, [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode'), [switch] $CreateBackups, [string] $BackupDirectory, [switch] $Force, [switch] $OnlyMixed, [switch] $EnsureFinalNewline, [switch] $OnlyMissingNewline, [switch] $PassThru ) if (-not (Test-Path -LiteralPath $Path -PathType Container)) { throw "Project path '$Path' not found or is not a directory" } $extensionMappings = @{ 'PowerShell' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml') 'CSharp' = @('*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.resx') 'Mixed' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml') 'All' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.js', '*.ts', '*.py', '*.rb', '*.java', '*.cpp', '*.h', '*.hpp', '*.sql', '*.md', '*.txt', '*.yaml', '*.yml') } if ($ProjectType -eq 'Custom' -and $CustomExtensions) { $filePatterns = $CustomExtensions } else { $filePatterns = $extensionMappings[$ProjectType] } Write-Verbose "Processing project type: $ProjectType with patterns: $($filePatterns -join ', ')" if ($CreateBackups -and $BackupDirectory) { if (-not (Test-Path -LiteralPath $BackupDirectory)) { New-Item -Path $BackupDirectory -ItemType Directory -Force | Out-Null Write-Verbose "Created backup directory: $BackupDirectory" } } $allFiles = @() foreach ($pattern in $filePatterns) { $params = @{ Path = $Path Filter = $pattern File = $true Recurse = $true } $files = Get-ChildItem @params | Where-Object { $file = $_ $excluded = $false foreach ($excludeDir in $ExcludeDirectories) { if ($file.DirectoryName -like "*\$excludeDir" -or $file.DirectoryName -like "*\$excludeDir\*") { $excluded = $true break } } -not $excluded } $allFiles += $files } $uniqueFiles = $allFiles | Sort-Object FullName | Get-Unique -AsString Write-Host "Found $($uniqueFiles.Count) files to process" -ForegroundColor Green if ($uniqueFiles.Count -eq 0) { Write-Warning "No files found matching the specified criteria" return } $results = @() $converted = 0 $skipped = 0 $errors = 0 foreach ($file in $uniqueFiles) { try { $currentInfo = Get-CurrentLineEnding -FilePath $file.FullName $currentLineEnding = $currentInfo.LineEnding $hasFinalNewline = $currentInfo.HasFinalNewline $relativePath = Get-RelativePath -From $Path -To $file.FullName $shouldProcess = $false $skipReason = "" if ($currentLineEnding -eq 'Error') { $skipReason = "Could not detect line endings" } elseif ($currentLineEnding -eq 'None') { $skipReason = "Empty file or no line endings" } elseif ($OnlyMixed -and $currentLineEnding -ne 'Mixed') { $skipReason = "Not mixed line endings (OnlyMixed specified)" } elseif ($OnlyMissingNewline -and $hasFinalNewline) { $skipReason = "Already has final newline (OnlyMissingNewline specified)" } elseif (-not $Force -and $currentLineEnding -eq $TargetLineEnding -and ($hasFinalNewline -or -not $EnsureFinalNewline)) { $skipReason = "Already compliant with target settings" } else { $shouldProcess = $true } if (-not $shouldProcess) { $result = @{ FilePath = $relativePath FullPath = $file.FullName Status = 'Skipped' Reason = $skipReason CurrentLineEnding = $currentLineEnding TargetLineEnding = $TargetLineEnding HasFinalNewline = $hasFinalNewline } $results += [PSCustomObject]$result $skipped++ Write-Verbose "Skipped $relativePath`: $skipReason" continue } if ($PSCmdlet.ShouldProcess($relativePath, "Convert line endings from $currentLineEnding to $TargetLineEnding$(if ($EnsureFinalNewline) { ' and ensure final newline' })")) { $conversionResult = Convert-LineEndingSingle -FilePath $file.FullName -TargetLineEnding $TargetLineEnding -CurrentInfo $currentInfo -CreateBackup $CreateBackups -EnsureFinalNewline $EnsureFinalNewline $result = @{ FilePath = $relativePath FullPath = $file.FullName Status = $conversionResult.Status Reason = $conversionResult.Reason CurrentLineEnding = $currentLineEnding TargetLineEnding = $TargetLineEnding HasFinalNewline = $hasFinalNewline BackupPath = $conversionResult.BackupPath } if ($CreateBackups -and $BackupDirectory -and $conversionResult.BackupPath -and (Test-Path $conversionResult.BackupPath)) { $backupTargetPath = Join-Path $BackupDirectory $relativePath $backupTargetDir = Split-Path $backupTargetPath -Parent if (-not (Test-Path $backupTargetDir)) { New-Item -Path $backupTargetDir -ItemType Directory -Force | Out-Null } Move-Item -Path $conversionResult.BackupPath -Destination $backupTargetPath -Force $result.BackupPath = $backupTargetPath } $results += [PSCustomObject]$result switch ($conversionResult.Status) { 'Converted' { $converted++ Write-Verbose "Converted $relativePath from $currentLineEnding to $TargetLineEnding" } 'Error' { $errors++ Write-Warning "Failed to convert $relativePath`: $($conversionResult.Reason)" } default { $skipped++ } } } } catch { Write-Warning "Unexpected error processing $($file.FullName): $_" $errors++ } } $summary = @{ TotalFiles = $uniqueFiles.Count Converted = $converted Skipped = $skipped Errors = $errors TargetLineEnding = $TargetLineEnding ProjectPath = $Path ProjectType = if ($ProjectType -eq 'Custom') { "Custom ($($CustomExtensions -join ', '))" } else { $ProjectType } } Write-Host "`nLine Ending Conversion Summary:" -ForegroundColor Cyan Write-Host " Total files processed: $($summary.TotalFiles)" -ForegroundColor White Write-Host " Successfully converted: $($summary.Converted)" -ForegroundColor Green Write-Host " Skipped: $($summary.Skipped)" -ForegroundColor Yellow Write-Host " Errors: $($summary.Errors)" -ForegroundColor Red Write-Host " Target line ending: $($summary.TargetLineEnding)" -ForegroundColor White if ($PassThru) { [PSCustomObject]@{ Summary = $summary Results = $results } } } function Get-MissingFunctions { [CmdletBinding()] param( [alias('Path')][string] $FilePath, [alias('ScriptBlock')][scriptblock] $Code, [string[]] $Functions, [switch] $Summary, [switch] $SummaryWithCommands, [Array] $ApprovedModules, [Array] $IgnoreFunctions ) $ListCommands = [System.Collections.Generic.List[Object]]::new() if ($FilePath) { $CommandsUsedInCode = Get-ScriptCommands -FilePath $FilePath } elseif ($Code) { $CommandsUsedInCode = Get-ScriptCommands -Code $Code } else { return } if ($IgnoreFunctions.Count -gt 0) { $Result = foreach ($_ in $CommandsUsedInCode) { if ($IgnoreFunctions -notcontains $_) { $_ } } } else { $Result = $CommandsUsedInCode } [Array] $FilteredCommands = Get-FilteredScriptCommands -Commands $Result -Functions $Functions -FilePath $FilePath -ApprovedModules $ApprovedModules foreach ($_ in $FilteredCommands) { $ListCommands.Add($_) } [Array] $FilteredCommandsName = foreach ($Name in $FilteredCommands.Name) { $Name } [Array] $FunctionsOutput = foreach ($_ in $ListCommands) { if ($_.ScriptBlock) { if ($ApprovedModules.Count -gt 0 -and $_.Source -in $ApprovedModules) { "function $($_.Name) { $($_.ScriptBlock) }" } elseif ($ApprovedModules.Count -eq 0) { } } } if ($FunctionsOutput.Count -gt 0) { $IgnoreAlreadyKnownCommands = ($FilteredCommandsName + $IgnoreFunctions) | Sort-Object -Unique $ScriptBlockMissing = [scriptblock]::Create($FunctionsOutput) $AnotherRun = Get-MissingFunctions -SummaryWithCommands -ApprovedModules $ApprovedModules -Code $ScriptBlockMissing -IgnoreFunctions $IgnoreAlreadyKnownCommands } if ($SummaryWithCommands) { if ($AnotherRun) { $Hash = @{ } $Hash.Summary = foreach ($_ in $FilteredCommands + $AnotherRun.Summary) { $_ } $Hash.SummaryFiltered = foreach ($_ in $ListCommands + $AnotherRun.SummaryFiltered) { $_ } $Hash.Functions = foreach ($_ in $FunctionsOutput + $AnotherRun.Functions) { $_ } } else { $Hash = @{ Summary = $FilteredCommands SummaryFiltered = $ListCommands Functions = $FunctionsOutput } } return $Hash } elseif ($Summary) { if ($AnotherRun) { foreach ($_ in $ListCommands + $AnotherRun.SummaryFiltered) { $_ } } else { return $ListCommands } } else { return $FunctionsOutput } } function Get-PowerShellAssemblyMetadata { <# .SYNOPSIS Gets the cmdlets and aliases in a dotnet assembly. .PARAMETER Path The assembly to inspect. .EXAMPLE Get-PowerShellAssemblyMetadata -Path MyModule.dll .NOTES This requires the System.Reflection.MetadataLoadContext assembly to be loaded through Add-Type. WinPS (5.1) will also need to load its deps System.Memory System.Collections.Immutable System.Reflection.Metadata System.Runtime.CompilerServices.Unsafe https://www.nuget.org/packages/System.Reflection.MetadataLoadContext Copyright: (c) 2024, Jordan Borean (@jborean93) <jborean93@gmail.com> MIT License (see LICENSE or https://opensource.org/licenses/MIT) #> [CmdletBinding()] param ( [Parameter(Mandatory)][string] $Path ) Write-Text -Text " [+] Loading assembly $Path" -Color Cyan try { $smaAssembly = [System.Management.Automation.PSObject].Assembly $smaAssemblyPath = $smaAssembly.Location if (-not $smaAssemblyPath) { $smaAssemblyPath = $smaAssembly.CodeBase if ($smaAssemblyPath -like 'file://*') { $smaAssemblyPath = $smaAssemblyPath -replace 'file:///', '' $smaAssemblyPath = [System.Uri]::UnescapeDataString($smaAssemblyPath) } else { Write-Text -Text "[-] Could not determine the path to System.Management.Automation assembly." -Color Red return $false } } $assemblyDirectory = Split-Path -Path $Path $runtimeAssemblies = Get-ChildItem -Path ([System.Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory()) -Filter "*.dll" $assemblyFiles = Get-ChildItem -Path $assemblyDirectory -Filter "*.dll" -Recurse $resolverPaths = [string[]] @( $runtimeAssemblies.FullName $assemblyFiles.FullName $smaAssemblyPath ) $resolver = [System.Reflection.PathAssemblyResolver]::new($resolverPaths) } catch { Write-Text -Text " [-] Can't create PathAssemblyResolver. Please ensure all dependencies are present. Error: $($_.Exception.Message)" -Color Red return $false } try { $context = [System.Reflection.MetadataLoadContext]::new($resolver) $smaAssemblyInContext = $context.LoadFromAssemblyPath($smaAssemblyPath) $cmdletType = $smaAssemblyInContext.GetType('System.Management.Automation.Cmdlet') $cmdletAttribute = $smaAssemblyInContext.GetType('System.Management.Automation.CmdletAttribute') $aliasAttribute = $smaAssemblyInContext.GetType('System.Management.Automation.AliasAttribute') $assembly = $context.LoadFromAssemblyPath($Path) Write-Verbose -Message "Loaded assembly $($assembly.FullName), $($assembly.Location) searching for cmdlets and aliases" $cmdletsToExport = [System.Collections.Generic.List[string]]::new() $aliasesToExport = [System.Collections.Generic.List[string]]::new() $Types = $assembly.GetTypes() $Types | Where-Object { $_.IsSubclassOf($cmdletType) } | ForEach-Object -Process { $cmdletInfo = $_.CustomAttributes | Where-Object { $_.AttributeType -eq $cmdletAttribute } if (-not $cmdletInfo) { return } $name = "$($cmdletInfo.ConstructorArguments[0].Value)-$($cmdletInfo.ConstructorArguments[1].Value)" $cmdletsToExport.Add($name) $aliases = $_.CustomAttributes | Where-Object { $_.AttributeType -eq $aliasAttribute } if (-not $aliases -or -not $aliases.ConstructorArguments.Value) { return } $aliasesToExport.AddRange([string[]]@($aliases.ConstructorArguments.Value.Value)) } [PSCustomObject]@{ CmdletsToExport = $cmdletsToExport AliasesToExport = $aliasesToExport } } catch { Write-Text -Text " [-] Can't load assembly $Path. Error: $($_.Exception.Message)" -Color Red $context.Dispose() return $false } finally { $context.Dispose() } } function Get-ProjectConsistency { <# .SYNOPSIS Provides comprehensive analysis of encoding and line ending consistency across a project. .DESCRIPTION Combines encoding and line ending analysis to provide a complete picture of file consistency across a project. Identifies issues and provides recommendations for standardization. This is the main analysis function that should be run before any bulk conversions. .PARAMETER Path Path to the project directory to analyze. .PARAMETER ProjectType Type of project to analyze. Determines which file extensions are included. Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom' .PARAMETER CustomExtensions Custom file extensions to analyze when ProjectType is 'Custom'. Example: @('*.ps1', '*.psm1', '*.cs', '*.vb') .PARAMETER ExcludeDirectories Directory names to exclude from analysis (e.g., '.git', 'bin', 'obj'). .PARAMETER RecommendedEncoding The encoding standard you want to achieve. Default is 'UTF8BOM' for PowerShell projects (PS 5.1 compatibility), 'UTF8' for others. .PARAMETER RecommendedLineEnding The line ending standard you want to achieve. Default is 'CRLF' on Windows, 'LF' on Unix. .PARAMETER ShowDetails Include detailed file-by-file analysis in the output. .PARAMETER ExportPath Export the detailed report to a CSV file at the specified path. .EXAMPLE Get-ProjectConsistencyReport -Path 'C:\MyProject' -ProjectType PowerShell Analyze consistency in a PowerShell project with UTF8BOM encoding (PS 5.1 compatible). .EXAMPLE Get-ProjectConsistencyReport -Path 'C:\MyProject' -ProjectType Mixed -RecommendedEncoding UTF8BOM -RecommendedLineEnding LF -ShowDetails Analyze a mixed project with specific recommendations and detailed output. .EXAMPLE Get-ProjectConsistencyReport -Path 'C:\MyProject' -ProjectType CSharp -RecommendedEncoding UTF8 -ExportPath 'C:\Reports\consistency-report.csv' Analyze a C# project (UTF8 without BOM is fine) with CSV export. .NOTES This function combines the analysis from Get-ProjectEncoding and Get-ProjectLineEnding to provide a unified view of project file consistency. Use this before running conversion functions. Encoding Recommendations: - PowerShell: UTF8BOM (required for PS 5.1 compatibility with special characters) - C#: UTF8 (BOM not needed, Visual Studio handles UTF8 correctly) - Mixed: UTF8BOM (safest for cross-platform PowerShell compatibility) PowerShell 5.1 Compatibility: UTF8 without BOM can cause PowerShell 5.1 to misinterpret files as ASCII, leading to: - Broken special characters and accented letters - Module import failures - Incorrect string processing UTF8BOM ensures proper encoding detection across all PowerShell versions. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Path, [ValidateSet('PowerShell', 'CSharp', 'Mixed', 'All', 'Custom')] [string] $ProjectType = 'Mixed', [string[]] $CustomExtensions, [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode'), [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')] [string] $RecommendedEncoding = $( if ($ProjectType -eq 'PowerShell') { 'UTF8BOM' } elseif ($ProjectType -eq 'Mixed') { 'UTF8BOM' } # Default to PowerShell-safe for mixed projects else { 'UTF8' } ), [ValidateSet('CRLF', 'LF')] [string] $RecommendedLineEnding = $(if ($IsWindows) { 'CRLF' } else { 'LF' }), [switch] $ShowDetails, [string] $ExportPath ) Write-Host "🔍 Analyzing project consistency..." -ForegroundColor Cyan Write-Host "Project: $Path" -ForegroundColor White Write-Host "Type: $ProjectType" -ForegroundColor White Write-Host "Target encoding: $RecommendedEncoding" -ForegroundColor White Write-Host "Target line ending: $RecommendedLineEnding" -ForegroundColor White Write-Host "`n📝 Analyzing file encodings..." -ForegroundColor Yellow $encodingParams = @{ Path = $Path ProjectType = $ProjectType ExcludeDirectories = $ExcludeDirectories ShowFiles = $true } if ($ProjectType -eq 'Custom' -and $CustomExtensions) { $encodingParams.CustomExtensions = $CustomExtensions } $encodingReport = Get-ProjectEncoding @encodingParams Write-Host "`n📏 Analyzing line endings..." -ForegroundColor Yellow $lineEndingParams = @{ Path = $Path ProjectType = $ProjectType ExcludeDirectories = $ExcludeDirectories ShowFiles = $true CheckMixed = $true } if ($ProjectType -eq 'Custom' -and $CustomExtensions) { $lineEndingParams.CustomExtensions = $CustomExtensions } $lineEndingReport = Get-ProjectLineEnding @lineEndingParams Write-Host "`n🔄 Combining analysis..." -ForegroundColor Yellow $allFiles = @() foreach ($encFile in $encodingReport.Files) { $leFile = $lineEndingReport.Files | Where-Object { $_.FullPath -eq $encFile.FullPath } if ($leFile) { $needsEncodingConversion = $encFile.Encoding -ne $RecommendedEncoding $needsLineEndingConversion = $leFile.LineEnding -ne $RecommendedLineEnding -and $leFile.LineEnding -ne 'None' $hasMixedLineEndings = $leFile.LineEnding -eq 'Mixed' $missingFinalNewline = -not $leFile.HasFinalNewline -and $encFile.Size -gt 0 -and $encFile.Extension -in @('.ps1', '.psm1', '.psd1', '.cs', '.js', '.py', '.rb', '.java', '.cpp', '.h', '.hpp', '.sql', '.md', '.txt', '.yaml', '.yml') $fileDetail = [PSCustomObject]@{ RelativePath = $encFile.RelativePath FullPath = $encFile.FullPath Extension = $encFile.Extension CurrentEncoding = $encFile.Encoding CurrentLineEnding = $leFile.LineEnding RecommendedEncoding = $RecommendedEncoding RecommendedLineEnding = $RecommendedLineEnding NeedsEncodingConversion = $needsEncodingConversion NeedsLineEndingConversion = $needsLineEndingConversion HasMixedLineEndings = $hasMixedLineEndings MissingFinalNewline = $missingFinalNewline HasIssues = $needsEncodingConversion -or $needsLineEndingConversion -or $hasMixedLineEndings -or $missingFinalNewline Size = $encFile.Size LastModified = $encFile.LastModified Directory = $encFile.Directory } $allFiles += $fileDetail } } $totalFiles = $allFiles.Count $filesNeedingEncodingConversion = ($allFiles | Where-Object { $_.NeedsEncodingConversion }).Count $filesNeedingLineEndingConversion = ($allFiles | Where-Object { $_.NeedsLineEndingConversion }).Count $filesWithMixedLineEndings = ($allFiles | Where-Object { $_.HasMixedLineEndings }).Count $filesMissingFinalNewline = ($allFiles | Where-Object { $_.MissingFinalNewline }).Count $filesWithIssues = ($allFiles | Where-Object { $_.HasIssues }).Count $filesCompliant = $totalFiles - $filesWithIssues $extensionIssues = @{} foreach ($file in ($allFiles | Where-Object { $_.HasIssues })) { if (-not $extensionIssues.ContainsKey($file.Extension)) { $extensionIssues[$file.Extension] = @{ Total = 0 EncodingIssues = 0 LineEndingIssues = 0 MixedLineEndings = 0 } } $extensionIssues[$file.Extension].Total++ if ($file.NeedsEncodingConversion) { $extensionIssues[$file.Extension].EncodingIssues++ } if ($file.NeedsLineEndingConversion) { $extensionIssues[$file.Extension].LineEndingIssues++ } if ($file.HasMixedLineEndings) { $extensionIssues[$file.Extension].MixedLineEndings++ } } $summary = [PSCustomObject]@{ ProjectPath = $Path ProjectType = $ProjectType AnalysisDate = Get-Date TotalFiles = $totalFiles FilesCompliant = $filesCompliant FilesWithIssues = $filesWithIssues CompliancePercentage = [math]::Round(($filesCompliant / $totalFiles) * 100, 1) CurrentEncodingDistribution = $encodingReport.Summary.EncodingDistribution FilesNeedingEncodingConversion = $filesNeedingEncodingConversion RecommendedEncoding = $RecommendedEncoding CurrentLineEndingDistribution = $lineEndingReport.Summary.LineEndingDistribution FilesNeedingLineEndingConversion = $filesNeedingLineEndingConversion FilesWithMixedLineEndings = $filesWithMixedLineEndings FilesMissingFinalNewline = $filesMissingFinalNewline RecommendedLineEnding = $RecommendedLineEnding ExtensionIssues = $extensionIssues } Write-Host "`n📊 Project Consistency Summary:" -ForegroundColor Cyan Write-Host " Total files analyzed: $totalFiles" -ForegroundColor White Write-Host " Files compliant with standards: $filesCompliant ($($summary.CompliancePercentage)%)" -ForegroundColor $(if ($summary.CompliancePercentage -ge 90) { 'Green' } elseif ($summary.CompliancePercentage -ge 70) { 'Yellow' } else { 'Red' }) Write-Host " Files needing attention: $filesWithIssues" -ForegroundColor $(if ($filesWithIssues -eq 0) { 'Green' } else { 'Red' }) Write-Host "`n📝 Encoding Issues:" -ForegroundColor Cyan Write-Host " Files needing encoding conversion: $filesNeedingEncodingConversion" -ForegroundColor $(if ($filesNeedingEncodingConversion -eq 0) { 'Green' } else { 'Yellow' }) Write-Host " Target encoding: $RecommendedEncoding" -ForegroundColor White Write-Host "`n📏 Line Ending Issues:" -ForegroundColor Cyan Write-Host " Files needing line ending conversion: $filesNeedingLineEndingConversion" -ForegroundColor $(if ($filesNeedingLineEndingConversion -eq 0) { 'Green' } else { 'Yellow' }) Write-Host " Files with mixed line endings: $filesWithMixedLineEndings" -ForegroundColor $(if ($filesWithMixedLineEndings -eq 0) { 'Green' } else { 'Red' }) Write-Host " Files missing final newline: $filesMissingFinalNewline" -ForegroundColor $(if ($filesMissingFinalNewline -eq 0) { 'Green' } else { 'Yellow' }) Write-Host " Target line ending: $RecommendedLineEnding" -ForegroundColor White if ($extensionIssues.Count -gt 0) { Write-Host "`n⚠️ Extensions with Issues:" -ForegroundColor Yellow foreach ($ext in ($extensionIssues.GetEnumerator() | Sort-Object { $_.Value.Total } -Descending)) { Write-Host " ${ext.Key}: $($ext.Value.Total) files" -ForegroundColor White if ($ext.Value.EncodingIssues -gt 0) { Write-Host " - Encoding issues: $($ext.Value.EncodingIssues)" -ForegroundColor Yellow } if ($ext.Value.LineEndingIssues -gt 0) { Write-Host " - Line ending issues: $($ext.Value.LineEndingIssues)" -ForegroundColor Yellow } if ($ext.Value.MixedLineEndings -gt 0) { Write-Host " - Mixed line endings: $($ext.Value.MixedLineEndings)" -ForegroundColor Red } } } Write-Host "`n💡 Recommendations:" -ForegroundColor Green if ($filesWithIssues -eq 0) { Write-Host " ✅ Your project is fully compliant! No action needed." -ForegroundColor Green } else { if ($filesWithMixedLineEndings -gt 0) { Write-Host " 🔴 Priority 1: Fix mixed line endings first" -ForegroundColor Red Write-Host " Convert-ProjectLineEnding -Path '$Path' -ProjectType $ProjectType -TargetLineEnding $RecommendedLineEnding -OnlyMixed -CreateBackups" -ForegroundColor Gray } if ($filesNeedingEncodingConversion -gt 0) { Write-Host " 🟡 Priority 2: Standardize encoding" -ForegroundColor Yellow Write-Host " Convert-ProjectEncoding -Path '$Path' -ProjectType $ProjectType -TargetEncoding $RecommendedEncoding -CreateBackups" -ForegroundColor Gray } if ($filesNeedingLineEndingConversion -gt 0) { Write-Host " 🟡 Priority 3: Standardize line endings" -ForegroundColor Yellow Write-Host " Convert-ProjectLineEnding -Path '$Path' -ProjectType $ProjectType -TargetLineEnding $RecommendedLineEnding -CreateBackups" -ForegroundColor Gray } if ($filesMissingFinalNewline -gt 0) { Write-Host " 🟡 Priority 4: Add missing final newlines" -ForegroundColor Yellow Write-Host " Convert-ProjectLineEnding -Path '$Path' -ProjectType $ProjectType -TargetLineEnding $RecommendedLineEnding -EnsureFinalNewline -OnlyMissingNewline -CreateBackups" -ForegroundColor Gray } Write-Host " 💾 Always use -WhatIf first and -CreateBackups for safety!" -ForegroundColor Cyan } $report = [PSCustomObject]@{ Summary = $summary EncodingReport = $encodingReport LineEndingReport = $lineEndingReport Files = if ($ShowDetails) { $allFiles } else { $null } ProblematicFiles = $allFiles | Where-Object { $_.HasIssues } } if ($ExportPath) { try { $allFiles | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8 Write-Host "`nDetailed report exported to: $ExportPath" -ForegroundColor Green } catch { Write-Warning "Failed to export report to $ExportPath`: $_" } } return $report } function Get-ProjectEncoding { <# .SYNOPSIS Analyzes encoding consistency across all files in a project directory. .DESCRIPTION Scans all relevant files in a project directory and provides a comprehensive report on file encodings. Identifies inconsistencies, potential issues, and provides recommendations for standardization. Useful for auditing projects before performing encoding conversions. .PARAMETER Path Path to the project directory to analyze. .PARAMETER ProjectType Type of project to analyze. Determines which file extensions are included. Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom' .PARAMETER CustomExtensions Custom file extensions to analyze when ProjectType is 'Custom'. Example: @('*.ps1', '*.psm1', '*.cs', '*.vb') .PARAMETER ExcludeDirectories Directory names to exclude from analysis (e.g., '.git', 'bin', 'obj'). .PARAMETER GroupByEncoding Group results by encoding type for easier analysis. .PARAMETER ShowFiles Include individual file details in the report. .PARAMETER ExportPath Export the detailed report to a CSV file at the specified path. .EXAMPLE Get-ProjectEncoding -Path 'C:\MyProject' -ProjectType PowerShell Analyze encoding consistency in a PowerShell project. .EXAMPLE Get-ProjectEncoding -Path 'C:\MyProject' -ProjectType Mixed -GroupByEncoding -ShowFiles Get detailed encoding report grouped by encoding type with individual file listings. .EXAMPLE Get-ProjectEncoding -Path 'C:\MyProject' -ProjectType All -ExportPath 'C:\Reports\encoding-report.csv' Analyze all file types and export detailed report to CSV. .NOTES This function is read-only and does not modify any files. Use Convert-ProjectEncoding to standardize encodings. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Path, [ValidateSet('PowerShell', 'CSharp', 'Mixed', 'All', 'Custom')] [string] $ProjectType = 'Mixed', [string[]] $CustomExtensions, [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode'), [switch] $GroupByEncoding, [switch] $ShowFiles, [string] $ExportPath ) if (-not (Test-Path -LiteralPath $Path -PathType Container)) { throw "Project path '$Path' not found or is not a directory" } $extensionMappings = @{ 'PowerShell' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml') 'CSharp' = @('*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.resx') 'Mixed' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml') 'All' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.js', '*.ts', '*.py', '*.rb', '*.java', '*.cpp', '*.h', '*.hpp', '*.sql', '*.md', '*.txt', '*.yaml', '*.yml') } if ($ProjectType -eq 'Custom' -and $CustomExtensions) { $filePatterns = $CustomExtensions } else { $filePatterns = $extensionMappings[$ProjectType] } Write-Host "Analyzing project encoding..." -ForegroundColor Cyan Write-Verbose "Project type: $ProjectType with patterns: $($filePatterns -join ', ')" $allFiles = @() foreach ($pattern in $filePatterns) { $params = @{ Path = $Path Filter = $pattern File = $true Recurse = $true } $files = Get-ChildItem @params | Where-Object { $file = $_ $excluded = $false foreach ($excludeDir in $ExcludeDirectories) { if ($file.DirectoryName -like "*\$excludeDir" -or $file.DirectoryName -like "*\$excludeDir\*") { $excluded = $true break } } -not $excluded } $allFiles += $files } $uniqueFiles = $allFiles | Sort-Object FullName | Get-Unique -AsString if ($uniqueFiles.Count -eq 0) { Write-Warning "No files found matching the specified criteria" return } Write-Host "Analyzing $($uniqueFiles.Count) files..." -ForegroundColor Green $fileDetails = @() $encodingStats = @{} $extensionStats = @{} foreach ($file in $uniqueFiles) { try { $encodingInfo = Get-FileEncoding -Path $file.FullName -AsObject $extension = $file.Extension.ToLower() $relativePath = Get-RelativePath -From $Path -To $file.FullName $fileDetail = [PSCustomObject]@{ RelativePath = $relativePath FullPath = $file.FullName Extension = $extension Encoding = $encodingInfo.EncodingName Size = $file.Length LastModified = $file.LastWriteTime Directory = $file.DirectoryName } $fileDetails += $fileDetail if (-not $encodingStats.ContainsKey($encodingInfo.EncodingName)) { $encodingStats[$encodingInfo.EncodingName] = 0 } $encodingStats[$encodingInfo.EncodingName]++ if (-not $extensionStats.ContainsKey($extension)) { $extensionStats[$extension] = @{} } if (-not $extensionStats[$extension].ContainsKey($encodingInfo.EncodingName)) { $extensionStats[$extension][$encodingInfo.EncodingName] = 0 } $extensionStats[$extension][$encodingInfo.EncodingName]++ } catch { Write-Warning "Failed to analyze $($file.FullName): $_" } } $totalFiles = $fileDetails.Count $uniqueEncodings = $encodingStats.Keys | Sort-Object $mostCommonEncoding = ($encodingStats.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 1).Key $inconsistentExtensions = @() foreach ($ext in $extensionStats.Keys) { if ($extensionStats[$ext].Count -gt 1) { $inconsistentExtensions += $ext } } $summary = [PSCustomObject]@{ ProjectPath = $Path ProjectType = $ProjectType TotalFiles = $totalFiles UniqueEncodings = $uniqueEncodings EncodingCount = $uniqueEncodings.Count MostCommonEncoding = $mostCommonEncoding InconsistentExtensions = $inconsistentExtensions EncodingDistribution = $encodingStats ExtensionEncodingMap = $extensionStats AnalysisDate = Get-Date } Write-Host "`nEncoding Analysis Summary:" -ForegroundColor Cyan Write-Host " Total files analyzed: $totalFiles" -ForegroundColor White Write-Host " Unique encodings found: $($uniqueEncodings.Count)" -ForegroundColor White if ($totalFiles -gt 0 -and $mostCommonEncoding) { Write-Host " Most common encoding: $mostCommonEncoding ($($encodingStats[$mostCommonEncoding]) files)" -ForegroundColor Green } elseif ($totalFiles -eq 0) { Write-Host " No files found for analysis" -ForegroundColor Yellow return $result } else { Write-Host " No encoding information available" -ForegroundColor Yellow } if ($inconsistentExtensions.Count -gt 0) { Write-Host " ⚠️ Extensions with mixed encodings: $($inconsistentExtensions -join ', ')" -ForegroundColor Yellow } else { Write-Host " ✅ All file extensions have consistent encodings" -ForegroundColor Green } Write-Host "`nEncoding Distribution:" -ForegroundColor Cyan foreach ($encoding in ($encodingStats.GetEnumerator() | Sort-Object Value -Descending)) { $percentage = [math]::Round(($encoding.Value / $totalFiles) * 100, 1) Write-Host " $($encoding.Key): $($encoding.Value) files ($percentage%)" -ForegroundColor White } if ($inconsistentExtensions.Count -gt 0) { Write-Host "`nExtensions with Mixed Encodings:" -ForegroundColor Yellow foreach ($ext in $inconsistentExtensions) { Write-Host " ${ext}:" -ForegroundColor Yellow foreach ($encoding in ($extensionStats[$ext].GetEnumerator() | Sort-Object Value -Descending)) { Write-Host " $($encoding.Key): $($encoding.Value) files" -ForegroundColor White } } } $report = [PSCustomObject]@{ Summary = $summary Files = if ($ShowFiles) { $fileDetails } else { $null } GroupedByEncoding = if ($GroupByEncoding) { $grouped = @{} foreach ($encoding in $uniqueEncodings) { $grouped[$encoding] = $fileDetails | Where-Object { $_.Encoding -eq $encoding } } $grouped } else { $null } } if ($ExportPath) { try { $fileDetails | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8 Write-Host "`nDetailed report exported to: $ExportPath" -ForegroundColor Green } catch { Write-Warning "Failed to export report to $ExportPath`: $_" } } return $report } function Get-ProjectLineEnding { <# .SYNOPSIS Analyzes line ending consistency across all files in a project directory. .DESCRIPTION Scans all relevant files in a project directory and provides a comprehensive report on line endings. Identifies inconsistencies between CRLF (Windows), LF (Unix/Linux), and mixed line endings. Helps ensure consistency across development environments and prevent Git issues. .PARAMETER Path Path to the project directory to analyze. .PARAMETER ProjectType Type of project to analyze. Determines which file extensions are included. Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom' .PARAMETER CustomExtensions Custom file extensions to analyze when ProjectType is 'Custom'. Example: @('*.ps1', '*.psm1', '*.cs', '*.vb') .PARAMETER ExcludeDirectories Directory names to exclude from analysis (e.g., '.git', 'bin', 'obj'). .PARAMETER GroupByLineEnding Group results by line ending type for easier analysis. .PARAMETER ShowFiles Include individual file details in the report. .PARAMETER CheckMixed Additionally check for files with mixed line endings (both CRLF and LF in same file). .PARAMETER ExportPath Export the detailed report to a CSV file at the specified path. .EXAMPLE Get-ProjectLineEnding -Path 'C:\MyProject' -ProjectType PowerShell Analyze line ending consistency in a PowerShell project. .EXAMPLE Get-ProjectLineEnding -Path 'C:\MyProject' -ProjectType Mixed -CheckMixed -ShowFiles Get detailed line ending report including mixed line ending detection. .EXAMPLE Get-ProjectLineEnding -Path 'C:\MyProject' -ProjectType All -ExportPath 'C:\Reports\lineending-report.csv' Analyze all file types and export detailed report to CSV. .NOTES Line ending types: - CRLF: Windows style (\\r\\n) - LF: Unix/Linux style (\\n) - CR: Classic Mac style (\\r) - rarely used - Mixed: File contains multiple line ending types - None: Empty file or single line without line ending #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Path, [ValidateSet('PowerShell', 'CSharp', 'Mixed', 'All', 'Custom')] [string] $ProjectType = 'Mixed', [string[]] $CustomExtensions, [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode'), [switch] $GroupByLineEnding, [switch] $ShowFiles, [switch] $CheckMixed, [string] $ExportPath ) if (-not (Test-Path -LiteralPath $Path -PathType Container)) { throw "Project path '$Path' not found or is not a directory" } $extensionMappings = @{ 'PowerShell' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml') 'CSharp' = @('*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.resx') 'Mixed' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml') 'All' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.js', '*.ts', '*.py', '*.rb', '*.java', '*.cpp', '*.h', '*.hpp', '*.sql', '*.md', '*.txt', '*.yaml', '*.yml') } if ($ProjectType -eq 'Custom' -and $CustomExtensions) { $filePatterns = $CustomExtensions } else { $filePatterns = $extensionMappings[$ProjectType] } Write-Host "Analyzing project line endings..." -ForegroundColor Cyan Write-Verbose "Project type: $ProjectType with patterns: $($filePatterns -join ', ')" $allFiles = @() foreach ($pattern in $filePatterns) { $params = @{ Path = $Path Filter = $pattern File = $true Recurse = $true } $files = Get-ChildItem @params | Where-Object { $file = $_ $excluded = $false foreach ($excludeDir in $ExcludeDirectories) { if ($file.DirectoryName -like "*\$excludeDir" -or $file.DirectoryName -like "*\$excludeDir\*") { $excluded = $true break } } -not $excluded } $allFiles += $files } $uniqueFiles = $allFiles | Sort-Object FullName | Get-Unique -AsString if ($uniqueFiles.Count -eq 0) { Write-Warning "No files found matching the specified criteria" return } Write-Host "Analyzing $($uniqueFiles.Count) files..." -ForegroundColor Green $fileDetails = @() $lineEndingStats = @{} $extensionStats = @{} $problemFiles = @() $filesWithoutFinalNewline = @() foreach ($file in $uniqueFiles) { try { $lineEndingInfo = Get-LineEndingType -FilePath $file.FullName $lineEndingType = $lineEndingInfo.LineEnding $hasFinalNewline = $lineEndingInfo.HasFinalNewline $extension = $file.Extension.ToLower() $relativePath = Get-RelativePath -From $Path -To $file.FullName $fileDetail = [PSCustomObject]@{ RelativePath = $relativePath FullPath = $file.FullName Extension = $extension LineEnding = $lineEndingType HasFinalNewline = $hasFinalNewline Size = $file.Length LastModified = $file.LastWriteTime Directory = $file.DirectoryName } $fileDetails += $fileDetail if ($lineEndingType -eq 'Mixed' -or ($CheckMixed -and $lineEndingType -eq 'Mixed')) { $problemFiles += $fileDetail } if (-not $hasFinalNewline -and $file.Length -gt 0 -and $extension -in @('.ps1', '.psm1', '.psd1', '.cs', '.js', '.py', '.rb', '.java', '.cpp', '.h', '.hpp', '.sql', '.md', '.txt', '.yaml', '.yml')) { $filesWithoutFinalNewline += $fileDetail } if (-not $lineEndingStats.ContainsKey($lineEndingType)) { $lineEndingStats[$lineEndingType] = 0 } $lineEndingStats[$lineEndingType]++ if (-not $extensionStats.ContainsKey($extension)) { $extensionStats[$extension] = @{} } if (-not $extensionStats[$extension].ContainsKey($lineEndingType)) { $extensionStats[$extension][$lineEndingType] = 0 } $extensionStats[$extension][$lineEndingType]++ } catch { Write-Warning "Failed to analyze $($file.FullName): $_" } } $totalFiles = $fileDetails.Count $uniqueLineEndings = $lineEndingStats.Keys | Sort-Object $mostCommonLineEnding = ($lineEndingStats.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 1).Key $inconsistentExtensions = @() foreach ($ext in $extensionStats.Keys) { if ($extensionStats[$ext].Count -gt 1) { $inconsistentExtensions += $ext } } $summary = [PSCustomObject]@{ ProjectPath = $Path ProjectType = $ProjectType TotalFiles = $totalFiles UniqueLineEndings = $uniqueLineEndings LineEndingCount = $uniqueLineEndings.Count MostCommonLineEnding = $mostCommonLineEnding InconsistentExtensions = $inconsistentExtensions ProblemFiles = $problemFiles.Count FilesWithoutFinalNewline = $filesWithoutFinalNewline.Count LineEndingDistribution = $lineEndingStats ExtensionLineEndingMap = $extensionStats AnalysisDate = Get-Date } Write-Host "`nLine Ending Analysis Summary:" -ForegroundColor Cyan Write-Host " Total files analyzed: $totalFiles" -ForegroundColor White Write-Host " Unique line endings found: $($uniqueLineEndings.Count)" -ForegroundColor White Write-Host " Most common line ending: $mostCommonLineEnding ($($lineEndingStats[$mostCommonLineEnding]) files)" -ForegroundColor Green if ($problemFiles.Count -gt 0) { Write-Host " ⚠️ Files with mixed line endings: $($problemFiles.Count)" -ForegroundColor Red } if ($filesWithoutFinalNewline.Count -gt 0) { Write-Host " ⚠️ Files without final newline: $($filesWithoutFinalNewline.Count)" -ForegroundColor Yellow } else { Write-Host " ✅ All files end with proper newlines" -ForegroundColor Green } if ($inconsistentExtensions.Count -gt 0) { Write-Host " ⚠️ Extensions with mixed line endings: $($inconsistentExtensions -join ', ')" -ForegroundColor Yellow } else { Write-Host " ✅ All file extensions have consistent line endings" -ForegroundColor Green } Write-Host "`nLine Ending Distribution:" -ForegroundColor Cyan foreach ($lineEnding in ($lineEndingStats.GetEnumerator() | Sort-Object Value -Descending)) { $percentage = [math]::Round(($lineEnding.Value / $totalFiles) * 100, 1) $color = switch ($lineEnding.Key) { 'CRLF' { 'Green' } 'LF' { 'Green' } 'Mixed' { 'Red' } 'CR' { 'Yellow' } 'None' { 'Gray' } 'Error' { 'Red' } default { 'White' } } Write-Host " $($lineEnding.Key): $($lineEnding.Value) files ($percentage%)" -ForegroundColor $color } if ($problemFiles.Count -gt 0) { Write-Host "`nFiles with Mixed Line Endings:" -ForegroundColor Red foreach ($problemFile in $problemFiles | Select-Object -First 10) { Write-Host " $($problemFile.RelativePath)" -ForegroundColor Yellow } if ($problemFiles.Count -gt 10) { Write-Host " ... and $($problemFiles.Count - 10) more files" -ForegroundColor Yellow } } if ($filesWithoutFinalNewline.Count -gt 0) { Write-Host "`nFiles Missing Final Newline:" -ForegroundColor Yellow foreach ($missingFile in $filesWithoutFinalNewline | Select-Object -First 10) { Write-Host " $($missingFile.RelativePath)" -ForegroundColor Yellow } if ($filesWithoutFinalNewline.Count -gt 10) { Write-Host " ... and $($filesWithoutFinalNewline.Count - 10) more files" -ForegroundColor Yellow } } if ($inconsistentExtensions.Count -gt 0) { Write-Host "`nExtensions with Mixed Line Endings:" -ForegroundColor Yellow foreach ($ext in $inconsistentExtensions) { Write-Host " ${ext}:" -ForegroundColor Yellow foreach ($lineEnding in ($extensionStats[$ext].GetEnumerator() | Sort-Object Value -Descending)) { Write-Host " $($lineEnding.Key): $($lineEnding.Value) files" -ForegroundColor White } } } $report = [PSCustomObject]@{ Summary = $summary Files = if ($ShowFiles) { $fileDetails } else { $null } ProblemFiles = $problemFiles GroupedByLineEnding = if ($GroupByLineEnding) { $grouped = @{} foreach ($lineEnding in $uniqueLineEndings) { $grouped[$lineEnding] = $fileDetails | Where-Object { $_.LineEnding -eq $lineEnding } } $grouped } else { $null } } if ($ExportPath) { try { $fileDetails | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8 Write-Host "`nDetailed report exported to: $ExportPath" -ForegroundColor Green } catch { Write-Warning "Failed to export report to $ExportPath`: $_" } } return $report } function Get-ProjectVersion { <# .SYNOPSIS Retrieves project version information from various project files. .DESCRIPTION Scans the specified path for C# projects (.csproj), PowerShell modules (.psd1), and PowerShell build scripts that contain 'Invoke-ModuleBuild' to extract version information. .PARAMETER ModuleName Optional module name to filter results to specific projects/modules. .PARAMETER Path The root path to search for project files. Defaults to current location. .PARAMETER ExcludeFolders Array of folder names to exclude from the search (in addition to default 'obj' and 'bin'). .OUTPUTS PSCustomObject[] Returns objects with Version, Source, and Type properties for each found project file. .EXAMPLE Get-ProjectVersion Gets version information from all project files in the current directory. .EXAMPLE Get-ProjectVersion -ModuleName "MyModule" -Path "C:\Projects" Gets version information for the specific module from the specified path. #> [CmdletBinding()] param( [Parameter()] [string]$ModuleName, [Parameter()] [string]$Path = (Get-Location).Path, [Parameter()] [string[]]$ExcludeFolders = @() ) $RepoRoot = $Path $DefaultExcludes = @('obj', 'bin') $AllExcludes = $DefaultExcludes + $ExcludeFolders | Select-Object -Unique $CsprojFiles = Get-ChildItem -Path $RepoRoot -Filter "*.csproj" -Recurse | Where-Object { $file = $_ ($AllExcludes.Count -eq 0 -or -not ($AllExcludes | Where-Object { $_ -and $_.Trim() -ne '' -and $file.FullName -and $file.FullName.ToLower().Contains($_.ToLower()) })) } $PsdFiles = Get-ChildItem -Path $RepoRoot -Filter "*.psd1" -Recurse | Where-Object { $file = $_ ($AllExcludes.Count -eq 0 -or -not ($AllExcludes | Where-Object { $_ -and $_.Trim() -ne '' -and $file.FullName -and $file.FullName.ToLower().Contains($_.ToLower()) })) } $BuildScriptFiles = Get-ChildItem -Path $RepoRoot -Filter "*.ps1" -Recurse | Where-Object { $file = $_ $isExcluded = ($AllExcludes.Count -gt 0 -and ($AllExcludes | Where-Object { $_ -and $_.Trim() -ne '' -and $file.FullName -and $file.FullName.ToLower().Contains($_.ToLower()) })) if ($isExcluded) { return $false } try { $content = Get-Content -Path $file.FullName -Raw -ErrorAction SilentlyContinue return $content -match 'Invoke-ModuleBuild|Build-Module' } catch { return $false } } $targetCsprojFiles = $CsprojFiles if ($ModuleName) { $targetCsprojFiles = $CsprojFiles | Where-Object { $_.BaseName -eq $ModuleName } } foreach ($csProj in $targetCsprojFiles) { $version = Get-CurrentVersionFromCsProj -ProjectFile $csProj.FullName if ($version) { [PSCustomObject]@{ Version = $version Source = $csProj.FullName Type = "C# Project" } } } $targetPsdFiles = $PsdFiles if ($ModuleName) { $targetPsdFiles = $PsdFiles | Where-Object { $_.BaseName -eq $ModuleName } } foreach ($psd1 in $targetPsdFiles) { $version = Get-CurrentVersionFromPsd1 -ManifestFile $psd1.FullName if ($version) { [PSCustomObject]@{ Version = $version Source = $psd1.FullName Type = "PowerShell Module" } } } foreach ($buildScript in $BuildScriptFiles) { $version = Get-CurrentVersionFromBuildScript -ScriptFile $buildScript.FullName if ($version) { [PSCustomObject]@{ Version = $version Source = $buildScript.FullName Type = "Build Script" } } } } function Initialize-PortableModule { [CmdletBinding()] param( [alias('ModuleName')][string] $Name, [string] $Path = $PSScriptRoot, [switch] $Download, [switch] $Import ) if ($PSVersionTable.PSVersion.Major -gt 5) { $Encoding = 'UTF8BOM' } else { $Encoding = 'UTF8' } if (-not $Name) { Write-Warning "Initialize-ModulePortable - Module name not given. Terminating." return } if (-not $Download -and -not $Import) { Write-Warning "Initialize-ModulePortable - Please choose Download/Import switch. Terminating." return } if ($Download) { try { if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { $null = New-Item -ItemType Directory -Path $Path -Force } Save-Module -Name $Name -LiteralPath $Path -WarningVariable WarningData -WarningAction SilentlyContinue -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message if ($WarningData) { Write-Warning "Initialize-ModulePortable - $WarningData" } Write-Warning "Initialize-ModulePortable - Error $ErrorMessage" return } } if ($Download -or $Import) { [Array] $Modules = Get-RequiredModule -Path $Path -Name $Name | Where-Object { $null -ne $_ } if ($null -ne $Modules) { [array]::Reverse($Modules) } $CleanedModules = [System.Collections.Generic.List[string]]::new() foreach ($_ in $Modules) { if ($CleanedModules -notcontains $_) { $CleanedModules.Add($_) } } $CleanedModules.Add($Name) $Items = foreach ($_ in $CleanedModules) { Get-ChildItem -LiteralPath "$Path\$_" -Filter '*.psd1' -Recurse -ErrorAction SilentlyContinue -Depth 1 } [Array] $PSD1Files = $Items.FullName } if ($Download) { $ListFiles = foreach ($PSD1 in $PSD1Files) { $PSD1.Replace("$Path", '$PSScriptRoot') } $Content = @( '$Modules = @(' foreach ($_ in $ListFiles) { " `"$_`"" } ')' "foreach (`$_ in `$Modules) {" " Import-Module `$_ -Verbose:`$false -Force" "}" ) $Content | Set-Content -Path $Path\$Name.ps1 -Force -Encoding $Encoding } if ($Import) { $ListFiles = foreach ($PSD1 in $PSD1Files) { $PSD1 } foreach ($_ in $ListFiles) { Import-Module $_ -Verbose:$false -Force } } } function Initialize-PortableScript { [cmdletBinding()] param( [string] $FilePath, [string] $OutputPath, [Array] $ApprovedModules ) if ($PSVersionTable.PSVersion.Major -gt 5) { $Encoding = 'UTF8BOM' } else { $Encoding = 'UTF8' } $Output = Get-MissingFunctions -FilePath $FilePath -SummaryWithCommands -ApprovedModules $ApprovedModules $Script = Get-Content -LiteralPath $FilePath -Encoding UTF8 $FinalScript = @( $Output.Functions $Script ) $FinalScript | Set-Content -LiteralPath $OutputPath -Encoding $Encoding $Output } function Initialize-ProjectManager { <# .SYNOPSIS Builds VSCode Project manager config from filesystem .DESCRIPTION Builds VSCode Project manager config from filesystem .PARAMETER Path Path to where the projects are located .PARAMETER DisableSorting Disables sorting of the projects by last modified date .EXAMPLE Initialize-ProjectManager -Path "C:\Support\GitHub" .EXAMPLE Initialize-ProjectManager -Path "C:\Support\GitHub" -DisableSorting .NOTES General notes #> [cmdletBinding()] param( [parameter(Mandatory)][string] $Path, [switch] $DisableSorting ) $ProjectsPath = Get-ChildItem -LiteralPath $Path -Directory $SortedProjects = foreach ($Project in $ProjectsPath) { $AllFiles = Get-ChildItem -LiteralPath $Project.FullName -Exclude ".\.git" -Recurse -Depth 2 -File $NewestFile = $AllFiles | Sort-Object -Descending -Property LastWriteTime | Select-Object -First 1 [PSCustomObject] @{ name = $Project.name FullName = $Project.FullName LastWriteTime = $NewestFile.LastWriteTime } } if (-not $DisableSorting) { $SortedProjects = $SortedProjects | Sort-Object -Descending -Property LastWriteTime } $ProjectManager = foreach ($Project in $SortedProjects) { [PSCustomObject] @{ name = $Project.name rootPath = $Project.FullName paths = @() tags = @() enabled = $true } } $PathProjects = @( [io.path]::Combine($Env:APPDATA, "Code\User\globalStorage\alefragnani.project-manager") [io.path]::Combine($Env:APPDATA, "Cursor\User\globalStorage\alefragnani.project-manager") [io.path]::Combine($Env:APPDATA, "Cursor - Insiders\User\globalStorage\alefragnani.project-manager") [io.path]::Combine($Env:APPDATA, "Code - Insiders\User\globalStorage\alefragnani.project-manager") [io.path]::Combine($Env:APPDATA, "Windsurf\User\globalStorage\alefragnani.project-manager") ) foreach ($Project in $PathProjects) { if (Test-Path -LiteralPath $Project) { $JsonPath = [io.path]::Combine($Project, 'projects.json') if (Test-Path -LiteralPath $JsonPath) { Get-Content -LiteralPath $JsonPath -Encoding UTF8 | Set-Content -LiteralPath "$JsonPath.backup" } $ProjectManager | ConvertTo-Json | Set-Content -LiteralPath $JsonPath } } } function Invoke-DotNetReleaseBuild { <# .SYNOPSIS Builds a .NET project in Release configuration and prepares release artefacts. .DESCRIPTION Wrapper around the build, pack and signing process typically used for publishing .NET projects. The function cleans the Release directory, builds the project, signs DLLs and NuGet packages when a certificate is provided, compresses the build output and returns details about the generated files. .PARAMETER ProjectPath Path to the folder containing the project (*.csproj) file. .PARAMETER CertificateThumbprint Optional certificate thumbprint used to sign the built assemblies and NuGet packages. When omitted no signing is performed. .PARAMETER LocalStore Certificate store used when searching for the signing certificate. Defaults to 'CurrentUser'. .PARAMETER TimeStampServer Timestamp server URL used while signing. .OUTPUTS PSCustomObject with properties Version, ReleasePath and ZipPath. .EXAMPLE Invoke-DotNetReleaseBuild -ProjectPath 'C:\Git\MyProject' -CertificateThumbprint '483292C9E317AA13B07BB7A96AE9D1A5ED9E7703' Builds and signs the project located in C:\Git\MyProject and returns paths to the release output. #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$ProjectPath, [Parameter()] [string]$CertificateThumbprint, [string]$LocalStore = 'CurrentUser', [string]$TimeStampServer = 'http://timestamp.digicert.com' ) $result = [ordered]@{ Success = $false Version = $null ReleasePath = $null ZipPath = $null Packages = @() ErrorMessage = $null } if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) { $result.ErrorMessage = 'dotnet CLI is not available.' return [PSCustomObject]$result } if (-not (Test-Path -LiteralPath $ProjectPath)) { $result.ErrorMessage = "Project path '$ProjectPath' not found." return [PSCustomObject]$result } $csproj = Get-ChildItem -Path $ProjectPath -Filter '*.csproj' -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $csproj) { $result.ErrorMessage = "No csproj found in $ProjectPath" return [PSCustomObject]$result } try { [xml]$xml = Get-Content -LiteralPath $csproj.FullName -Raw -ErrorAction Stop } catch { $result.ErrorMessage = "Failed to read '$($csproj.FullName)' as XML: $_" return [PSCustomObject]$result } $version = ($xml.Project.PropertyGroup | Where-Object { $_.VersionPrefix } | Select-Object -First 1).VersionPrefix if (-not $version) { $result.ErrorMessage = "VersionPrefix not found in '$($csproj.FullName)'" return [PSCustomObject]$result } $releasePath = Join-Path -Path $csproj.Directory.FullName -ChildPath 'bin/Release' if (Test-Path -LiteralPath $releasePath) { try { Get-ChildItem -Path $releasePath -Recurse -File | Remove-Item -Force Get-ChildItem -Path $releasePath -Recurse -Filter '*.nupkg' | Remove-Item -Force Get-ChildItem -Path $releasePath -Directory | Remove-Item -Force -Recurse } catch { $result.ErrorMessage = "Failed to clean $($releasePath): $_" return [PSCustomObject]$result } } else { $null = New-Item -ItemType Directory -Path $releasePath -Force } dotnet build $csproj.FullName --configuration Release if ($LASTEXITCODE -ne 0) { $result.ErrorMessage = 'dotnet build failed.' return [PSCustomObject]$result } if ($CertificateThumbprint) { Register-Certificate -Path $releasePath -LocalStore $LocalStore -Include @('*.dll') -TimeStampServer $TimeStampServer -Thumbprint $CertificateThumbprint } $zipPath = Join-Path -Path $releasePath -ChildPath ("{0}.{1}.zip" -f $csproj.BaseName, $version) Compress-Archive -Path (Join-Path $releasePath '*') -DestinationPath $zipPath -Force dotnet pack $csproj.FullName --configuration Release --no-restore --no-build if ($LASTEXITCODE -ne 0) { $result.ErrorMessage = 'dotnet pack failed.' return [PSCustomObject]$result } $nupkgs = Get-ChildItem -Path $releasePath -Recurse -Filter '*.nupkg' -ErrorAction SilentlyContinue if ($CertificateThumbprint -and $nupkgs) { foreach ($pkg in $nupkgs) { dotnet nuget sign $pkg.FullName --certificate-fingerprint $CertificateThumbprint --timestamper $TimeStampServer --overwrite if ($LASTEXITCODE -ne 0) { Write-Warning "Invoke-DotNetReleaseBuild - Failed to sign $($pkg.FullName)" } } } $result.Success = $true $result.Version = $version $result.ReleasePath = $releasePath $result.ZipPath = $zipPath $result.Packages = $nupkgs.FullName return [PSCustomObject]$result } function Invoke-ModuleBuild { <# .SYNOPSIS Command to create new module or update existing one. It will create new module structure and everything around it, or update existing one. .DESCRIPTION Command to create new module or update existing one. It will create new module structure and everything around it, or update existing one. .PARAMETER Settings Provide settings for the module in form of scriptblock. It's using DSL to define settings for the module. .PARAMETER Path Path to the folder where new project will be created, or existing project will be updated. If not provided it will be created in one up folder from the location of build script. .PARAMETER ModuleName Provide name of the module. It's required parameter. .PARAMETER FunctionsToExportFolder Public functions folder name. Default is 'Public'. It will be used as part of PSD1 and PSM1 to export only functions from this folder. .PARAMETER AliasesToExportFolder Public aliases folder name. Default is 'Public'. It will be used as part of PSD1 and PSM1 to export only aliases from this folder. .PARAMETER Configuration Provides a way to configure module using hashtable. It's the old way of configuring module, that requires knowledge of inner workings of the module to name proper key/value pairs It's required for compatibility with older versions of the module. .PARAMETER ExcludeFromPackage Exclude files from Artefacts. Default is '.*, 'Ignore', 'Examples', 'package.json', 'Publish', 'Docs'. .PARAMETER IncludeRoot Include files in the Artefacts from root of the project. Default is '*.psm1', '*.psd1', 'License*' files. Other files will be ignored. .PARAMETER IncludePS1 Include *.ps1 files in the Artefacts from given folders. Default are 'Private', 'Public', 'Enums', 'Classes' folders. If the folder doesn't exists it will be ignored. .PARAMETER IncludeAll Include all files in the Artefacts from given folders. Default are 'Images', 'Resources', 'Templates', 'Bin', 'Lib', 'Data' folders. .PARAMETER IncludeCustomCode Parameter description .PARAMETER IncludeToArray Parameter description .PARAMETER LibrariesCore Parameter description .PARAMETER LibrariesDefault Parameter description .PARAMETER LibrariesStandard Parameter description .PARAMETER ExitCode Exit code to be returned to the caller. If not provided, it will not exit the script, but finish gracefully. Exit code 0 means success, 1 means failure. .EXAMPLE An example .NOTES General notes #> [alias('New-PrepareModule', 'Build-Module', 'Invoke-ModuleBuilder')] [CmdletBinding(DefaultParameterSetName = 'Modern')] param ( [Parameter(Position = 0, ParameterSetName = 'Modern')][scriptblock] $Settings, [parameter(ParameterSetName = 'Modern')][string] $Path, [parameter(Mandatory, ParameterSetName = 'Modern')][alias('ProjectName')][string] $ModuleName, [parameter(ParameterSetName = 'Modern')][string] $FunctionsToExportFolder = 'Public', [parameter(ParameterSetName = 'Modern')][string] $AliasesToExportFolder = 'Public', [Parameter(Mandatory, ParameterSetName = 'Configuration')][System.Collections.IDictionary] $Configuration = [ordered] @{}, [parameter(ParameterSetName = 'Modern')][string[]] $ExcludeFromPackage = @('.*', 'Ignore', 'Examples', 'package.json', 'Publish', 'Docs'), [parameter(ParameterSetName = 'Modern')][string[]] $IncludeRoot = @('*.psm1', '*.psd1', 'License*'), [parameter(ParameterSetName = 'Modern')][string[]] $IncludePS1 = @('Private', 'Public', 'Enums', 'Classes'), [parameter(ParameterSetName = 'Modern')][string[]] $IncludeAll = @('Images', 'Resources', 'Templates', 'Bin', 'Lib', 'Data'), [parameter(ParameterSetName = 'Modern')][scriptblock] $IncludeCustomCode, [parameter(ParameterSetName = 'Modern')][System.Collections.IDictionary] $IncludeToArray, [parameter(ParameterSetName = 'Modern')][string] $LibrariesCore = [io.path]::Combine("Lib", "Core"), [parameter(ParameterSetName = 'Modern')][string] $LibrariesDefault = [io.path]::Combine("Lib", "Default"), [parameter(ParameterSetName = 'Modern')][string] $LibrariesStandard = [io.path]::Combine("Lib", "Standard"), [parameter(ParameterSetName = 'Configuration')] [parameter(ParameterSetName = 'Modern')] [switch] $ExitCode ) if ($PsCmdlet.ParameterSetName -eq 'Configuration') { $ModuleName = $Configuration.Information.ModuleName } if ($Path) { $FullProjectPath = [io.path]::Combine($Path, $ModuleName) } else { $ProjectPathToUse = [io.path]::Combine($MyInvocation.PSScriptRoot, "..") $FullProjectPath = Get-Item -LiteralPath $ProjectPathToUse } $GlobalTime = [System.Diagnostics.Stopwatch]::StartNew() if ($Path -and $ModuleName) { $FullProjectPath = [io.path]::Combine($Path, $ModuleName) if (-not (Test-Path -Path $Path)) { Write-Text -Text "[-] Path $Path doesn't exists. Please create it, before continuing." -Color Red if ($ExitCode) { exit 1 } else { return } } else { $CopiedBuildModule = $false $CopiedPSD1 = $false if (Test-Path -Path $FullProjectPath) { Write-Text -Text "[i] Module $ModuleName ($FullProjectPath) already exists. Skipping inital steps" -Color DarkGray } else { Write-Text -Text "[i] Preparing module structure for $ModuleName in $Path" -Color DarkGray $Folders = 'Private', 'Public', 'Examples', 'Ignore', 'Build' Add-Directory -Directory $FullProjectPath foreach ($folder in $Folders) { $SubFolder = [io.path]::Combine($FullProjectPath, $Folder) Add-Directory -Directory $SubFolder } $PathToData = [io.path]::Combine($PSScriptRoot, "..", "Data") if (-not (Test-Path -LiteralPath $PathToData)) { $PathToData = [io.path]::Combine($PSScriptRoot, "Data") } $FilesToCopy = [ordered] @{ '.gitignore' = @{ Source = [io.path]::Combine($PathToData, "Example-Gitignore.txt") Destination = [io.path]::Combine($FullProjectPath, ".gitignore") } 'CHANGELOG.MD' = @{ Source = [io.path]::Combine($PathToData, "Example-CHANGELOG.MD") Destination = [io.path]::Combine($FullProjectPath, "CHANGELOG.MD") } 'README.MD' = @{ Source = [io.path]::Combine($PathToData, "Example-README.MD") Destination = [io.path]::Combine($FullProjectPath, "README.MD") } 'License' = @{ Source = [io.path]::Combine($PathToData, "Example-LicenseMIT.txt") Destination = [io.path]::Combine($FullProjectPath, "LICENSE") } 'Build-Module.ps1' = @{ Source = [io.path]::Combine($PathToData, "Example-ModuleBuilder.txt") Destination = [io.path]::Combine($FullProjectPath, "Build", "Build-Module.ps1") } "$ModuleName.psm1" = @{ Source = [io.path]::Combine($PathToData, "Example-ModulePSM1.txt") Destination = [io.path]::Combine($FullProjectPath, "$ModuleName.psm1") } "$ModuleName.psd1" = @{ Source = [io.path]::Combine($PathToData, "Example-ModulePSD1.txt") Destination = [io.path]::Combine($FullProjectPath, "$ModuleName.psd1") } } foreach ($File in $FilesToCopy.Keys) { $ValueToProcess = $FilesToCopy[$File] $SourceFilePath = $ValueToProcess.Source $DestinationFilePath = $ValueToProcess.Destination if (-not (Test-Path -LiteralPath $DestinationFilePath)) { Write-Text -Text " [+] Copying '$($File)' file ($SourceFilePath)" -Color DarkGray Copy-Item -Path $SourceFilePath -Destination $DestinationFilePath -ErrorAction Stop if ($File -eq 'Build-Module.ps1') { $CopiedBuildModule = $True } elseif ($File -eq "$ModuleName.psd1") { $CopiedPSD1 = $True } } } $Guid = (New-Guid).Guid if ($CopiedBuildModule) { $FilePath = [io.path]::Combine($FullProjectPath, "Build", "Build-Module.ps1") $Success = Register-DataForInitialModule -FilePath $FilePath -ModuleName $ModuleName -Guid $Guid if ($Success -eq $false) { if ($ExitCode) { exit 1 } else { return } } } if ($CopiedPSD1) { $FilePath = [io.path]::Combine($FullProjectPath, "$ModuleName.psd1") $Success = Register-DataForInitialModule -FilePath $FilePath -ModuleName $ModuleName -Guid $Guid if ($Success -eq $false) { if ($ExitCode) { exit 1 } else { return } } } Write-Text -Text "[i] Preparing module structure for $ModuleName in $Path. Completed." -Color DarkGray } } } $newPrepareStructureSplat = [ordered] @{ Configuration = $Configuration Settings = $Settings PathToProject = $FullProjectPath ModuleName = $ModuleName FunctionsToExportFolder = $FunctionsToExportFolder AliasesToExportFolder = $AliasesToExportFolder ExcludeFromPackage = $ExcludeFromPackage IncludeRoot = $IncludeRoot IncludePS1 = $IncludePS1 IncludeAll = $IncludeAll IncludeCustomCode = $IncludeCustomCode IncludeToArray = $IncludeToArray LibrariesCore = $LibrariesCore LibrariesDefault = $LibrariesDefault LibrariesStandard = $LibrariesStandard } $ModuleOutput = New-PrepareStructure @newPrepareStructureSplat $Execute = "$($GlobalTime.Elapsed.Days) days, $($GlobalTime.Elapsed.Hours) hours, $($GlobalTime.Elapsed.Minutes) minutes, $($GlobalTime.Elapsed.Seconds) seconds, $($GlobalTime.Elapsed.Milliseconds) milliseconds" if ($ModuleOutput -notcontains $false) { Write-Host "[i] Module Build Completed " -NoNewline -ForegroundColor Green Write-Host "[Time Total: $Execute]" -ForegroundColor Green if ($ExitCode) { exit 0 } } else { Write-Host "[i] Module Build Failed " -NoNewline -ForegroundColor Red Write-Host "[Time Total: $Execute]" -ForegroundColor Red if ($ExitCode) { exit 1 } } } function New-ConfigurationArtefact { <# .SYNOPSIS Tells the module to create artefact of specified type .DESCRIPTION Tells the module to create artefact of specified type There can be multiple artefacts created (even of same type) At least one packed artefact is required for publishing to GitHub .PARAMETER PreScriptMerge ScriptBlock that will be added in the beggining of the script. It's only applicable to type of Script, PackedScript. If useed with PreScriptMergePath, this will be ignored. .PARAMETER PostScriptMerge ScriptBlock that will be added in the end of the script. It's only applicable to type of Script, PackedScript. If useed with PostScriptMergePath, this will be ignored. .PARAMETER PreScriptMergePath Path to file that will be added in the beggining of the script. It's only applicable to type of Script, PackedScript. .PARAMETER PostScriptMergePath Path to file that will be added in the end of the script. It's only applicable to type of Script, PackedScript. .PARAMETER Type There are 4 types of artefacts: - Unpacked - unpacked module (useful for testing) - Packed - packed module (as zip) - usually used for publishing to GitHub or copying somewhere - Script - script that is module in form of PS1 without PSD1 - only applicable to very simple modules - PackedScript - packed module (as zip) that is script that is module in form of PS1 without PSD1 - only applicable to very simple modules .PARAMETER ID Optional ID of the artefact. To be used by New-ConfigurationPublish cmdlet If not specified, the first packed artefact will be used for publishing to GitHub .PARAMETER Enable Enable artefact creation. By default artefact creation is disabled. .PARAMETER IncludeTagName Include tag name in artefact name. By default tag name is not included. Alternatively you can provide ArtefactName parameter to specify your own artefact name (with or without TagName) .PARAMETER Path Path where artefact will be created. Please choose a separate directory for each artefact type, as logic may be interfering one another. You can use following variables that will be replaced with actual values: - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0 - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1 - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1 - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0 .PARAMETER AddRequiredModules Add required modules to artefact by copying them over. By default required modules are not added. .PARAMETER ModulesPath Path where main module or required module (if not specified otherwise in RequiredModulesPath) will be copied to. By default it will be put in the Path folder if not specified You can use following variables that will be replaced with actual values: - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0 - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1 - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1 - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0 .PARAMETER RequiredModulesPath Path where required modules will be copied to. By default it will be put in the Path folder if not specified. If ModulesPath is specified, but RequiredModulesPath is not specified it will be put into ModulesPath folder. You can use following variables that will be replaced with actual values: - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0 - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1 - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1 - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0 .PARAMETER CopyDirectories Provide Hashtable of directories to copy to artefact. Key is source directory, value is destination directory. .PARAMETER CopyFiles Provide Hashtable of files to copy to artefact. Key is source file, value is destination file. .PARAMETER CopyDirectoriesRelative Define if destination directories should be relative to artefact root. By default they are not. .PARAMETER CopyFilesRelative Define if destination files should be relative to artefact root. By default they are not. .PARAMETER ArtefactName The name of the artefact. If not specified, the default name will be used. You can use following variables that will be replaced with actual values: - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0 - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1 - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1 - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0 .PARAMETER ScriptName The name of the script. If not specified, the default name will be used. Only applicable to Script and ScriptPacked artefacts. You can use following variables that will be replaced with actual values: - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0 - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1 - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1 - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0 .PARAMETER DoNotClear Do not clear artefact directory before creating artefact. By default artefact directory is cleared. .EXAMPLE New-ConfigurationArtefact -Type Unpacked -Enable -Path "$PSScriptRoot\Artefacts\Unpacked" -RequiredModulesPath "$PSScriptRoot\Artefacts\Unpacked\Modules" .EXAMPLE # standard artefact, packed with tag name without any additional modules or required modules New-ConfigurationArtefact -Type Packed -Enable -Path "$PSScriptRoot\Artefacts\Packed" -IncludeTagName .EXAMPLE # Create artefact in form of a script. This is useful for very simple modules that should be just single PS1 file New-ConfigurationArtefact -Type Script -Enable -Path "$PSScriptRoot\Artefacts\Script" -IncludeTagName .EXAMPLE # Create artefact in form of a script. This is useful for very simple modules that should be just single PS1 file # But additionally pack it into zip fileĄŚż$%# New-ConfigurationArtefact -Type ScriptPacked -Enable -Path "$PSScriptRoot\Artefacts\ScriptPacked" -ArtefactName "Script-<ModuleName>-$((Get-Date).ToString('yyyy-MM-dd')).zip" .NOTES General notes #> [CmdletBinding()] param( [Parameter(Position = 0)][ScriptBlock] $PostScriptMerge, [Parameter(Position = 1)][ScriptBlock] $PreScriptMerge, [Parameter(Mandatory)][ValidateSet('Unpacked', 'Packed', 'Script', 'ScriptPacked')][string] $Type, [switch] $Enable, [switch] $IncludeTagName, [string] $Path, [alias('RequiredModules')][switch] $AddRequiredModules, [string] $ModulesPath, [string] $RequiredModulesPath, [System.Collections.IDictionary] $CopyDirectories, [System.Collections.IDictionary] $CopyFiles, [switch] $CopyDirectoriesRelative, [switch] $CopyFilesRelative, [switch] $DoNotClear, [string] $ArtefactName, [alias('FileName')][string] $ScriptName, [string] $ID, [string] $PostScriptMergePath, [string] $PreScriptMergePath ) $Artefact = [ordered ] @{ Type = $Type Configuration = [ordered] @{ Type = $Type RequiredModules = [ordered] @{} } } if ($PSVersionTable.PSVersion.Major -gt 5) { $Encoding = 'UTF8BOM' } else { $Encoding = 'UTF8' } if ($PSBoundParameters.ContainsKey('Enable')) { $Artefact['Configuration']['Enabled'] = $Enable } if ($PSBoundParameters.ContainsKey('IncludeTagName')) { $Artefact['Configuration']['IncludeTagName'] = $IncludeTagName } if ($PSBoundParameters.ContainsKey('Path')) { if ($null -eq $IsWindows -or $IsWindows -eq $true) { $Artefact['Configuration']['Path'] = $Path.Replace('/', '\') } else { $Artefact['Configuration']['Path'] = $Path.Replace('\', '/') } } if ($PSBoundParameters.ContainsKey('RequiredModulesPath')) { if ($null -eq $IsWindows -or $IsWindows -eq $true) { $Artefact['Configuration']['RequiredModules']['Path'] = $RequiredModulesPath.Replace('/', '\') } else { $Artefact['Configuration']['RequiredModules']['Path'] = $RequiredModulesPath.Replace('\', '/') } } if ($PSBoundParameters.ContainsKey('AddRequiredModules')) { $Artefact['Configuration']['RequiredModules']['Enabled'] = $true } if ($PSBoundParameters.ContainsKey('ModulesPath')) { if ($null -eq $IsWindows -or $IsWindows -eq $true) { $Artefact['Configuration']['RequiredModules']['ModulesPath'] = $ModulesPath.Replace('/', '\') } else { $Artefact['Configuration']['RequiredModules']['ModulesPath'] = $ModulesPath.Replace('\', '/') } } if ($PSBoundParameters.ContainsKey('CopyDirectories')) { foreach ($Directory in [string[]] $CopyDirectories.Keys) { if ($null -eq $IsWindows -or $IsWindows -eq $true) { $CopyDirectories[$Directory] = $CopyDirectories[$Directory].Replace('/', '\') } else { $CopyDirectories[$Directory] = $CopyDirectories[$Directory].Replace('\', '/') } } $Artefact['Configuration']['DirectoryOutput'] = $CopyDirectories } if ($PSBoundParameters.ContainsKey('CopyDirectoriesRelative')) { $Artefact['Configuration']['DestinationDirectoriesRelative'] = $CopyDirectoriesRelative.IsPresent } if ($PSBoundParameters.ContainsKey('CopyFiles')) { foreach ($File in [string[]] $CopyFiles.Keys) { if ($null -eq $IsWindows -or $IsWindows -eq $true) { $CopyFiles[$File] = $CopyFiles[$File].Replace('/', '\') } else { $CopyFiles[$File] = $CopyFiles[$File].Replace('\', '/') } } $Artefact['Configuration']['FilesOutput'] = $CopyFiles } if ($PSBoundParameters.ContainsKey('CopyFilesRelative')) { $Artefact['Configuration']['DestinationFilesRelative'] = $CopyFilesRelative.IsPresent } if ($PSBoundParameters.ContainsKey('DoNotClear')) { $Artefact['Configuration']['DoNotClear'] = $DoNotClear.IsPresent } if ($PSBoundParameters.ContainsKey('ArtefactName')) { $Artefact['Configuration']['ArtefactName'] = $ArtefactName } if ($PSBoundParameters.ContainsKey('ScriptName')) { $Artefact['Configuration']['ScriptName'] = $ScriptName } if ($PSBoundParameters.ContainsKey('PreScriptMerge')) { try { $Artefact['Configuration']['PreScriptMerge'] = Invoke-Formatter -ScriptDefinition $PreScriptMerge.ToString() } catch { Write-Text -Text "[i] Unable to format merge script provided by user. Error: $($_.Exception.Message). Using original script." -Color Red $Artefact['Configuration']['PreScriptMerge'] = $PreScriptMerge.ToString() } } if ($PSBoundParameters.ContainsKey('PostScriptMerge')) { try { $Artefact['Configuration']['PostScriptMerge'] = Invoke-Formatter -ScriptDefinition $PostScriptMerge.ToString() } catch { Write-Text -Text "[i] Unable to format merge script provided by user. Error: $($_.Exception.Message). Using original script." -Color Red $Artefact['Configuration']['PostScriptMerge'] = $PostScriptMerge.ToString() } } if ($PSBoundParameters.ContainsKey('PreScriptMergePath')) { $ScriptContent = Get-Content -Path $PreScriptMergePath -Raw -Encoding $Encoding if ($ScriptContent) { try { $Artefact['Configuration']['PreScriptMerge'] = Invoke-Formatter -ScriptDefinition $ScriptContent } catch { Write-Text -Text "[i] Unable to format merge script provided by user. Error: $($_.Exception.Message). Using original script." -Color Red $Artefact['Configuration']['PreScriptMerge'] = $ScriptContent.ToString() } } } if ($PSBoundParameters.ContainsKey('PostScriptMergePath')) { $ScriptContent = Get-Content -Path $PostScriptMergePath -Raw -Encoding $Encoding if ($ScriptContent) { try { $Artefact['Configuration']['PostScriptMerge'] = Invoke-Formatter -ScriptDefinition $ScriptContent.ToString() } catch { Write-Text -Text "[i] Unable to format merge script provided by user. Error: $($_.Exception.Message). Using original script." -Color Red $Artefact['Configuration']['PostScriptMerge'] = $ScriptContent.ToString() } } } if ($PSBoundParameters.ContainsKey('ID')) { $Artefact['Configuration']['ID'] = $ID } $Artefact } function New-ConfigurationBuild { <# .SYNOPSIS Allows to configure build process for the module .DESCRIPTION Allows to configure build process for the module .PARAMETER Enable Enable build process .PARAMETER DeleteTargetModuleBeforeBuild Delete target module before build .PARAMETER MergeModuleOnBuild Parameter description .PARAMETER MergeFunctionsFromApprovedModules Parameter description .PARAMETER SignModule Parameter description .PARAMETER DotSourceClasses Parameter description .PARAMETER DotSourceLibraries Parameter description .PARAMETER SeparateFileLibraries Parameter description .PARAMETER RefreshPSD1Only Parameter description .PARAMETER UseWildcardForFunctions Parameter description .PARAMETER LocalVersioning Parameter description .PARAMETER DoNotAttemptToFixRelativePaths Configures module builder to not replace $PSScriptRoot\ with $PSScriptRoot\ This is useful if you have a module that has a lot of relative paths that are required when using Private/Public folders, but for merge process those are not supposed to be there as the paths change. By default module builder will attempt to fix it. This option disables this functionality. Best practice is to use $MyInvocation.MyCommand.Module.ModuleBase or similar instead of relative paths. .PARAMETER CertificateThumbprint Parameter description .PARAMETER CertificatePFXPath Parameter description .PARAMETER CertificatePFXBase64 Parameter description .PARAMETER CertificatePFXPassword Parameter description .PARAMETER NETConfiguration Parameter description .PARAMETER NETFramework Parameter description .PARAMETER NETProjectPath Path to the project that you want to build. This is useful if it's not in Sources folder directly within module directory .PARAMETER NETProjectName By default it will assume same name as project name, but you can provide different name if needed. It's required if NETProjectPath is provided .PARAMETER NETExcludeMainLibrary Exclude main library from build, this is useful if you have C# project that you want to build that is used mostly for generating libraries that are used in PowerShell module It won't include main library in the build, but it will include all other libraries .PARAMETER NETExcludeLibraryFilter Provide list of filters for libraries that you want to exclude from build, this is useful if you have C# project that you want to build, but don't want to include all libraries for some reason .PARAMETER NETIgnoreLibraryOnLoad This is to exclude libraries from being loaded in PowerShell by PSM1/Librarties.ps1 files. This is useful if you have a library that is not supposed to be loaded in PowerShell, but you still need it For example library that's not NET based and is as dependency for other libraries .PARAMETER NETBinaryModule Provide list of binary modules that you want to import-module in the module. This is useful if you're building a module that has binary modules and you want to import them in the module. In here you provide one or more binrary module names that you want to import in the module. Just the DLL name with extension without path. Path is assumed to be $PSScriptRoot\Lib\Standard or $PSScriptRoot\Lib\Default or $PSScriptRoot\Lib\Core .PARAMETER NETBinaryModuleDocumentation Include documentation for binary modules, this is useful if you have a lot of binary modules and you want to include documentation for them (if available in XML format) .PARAMETER NETBinaryModuleCmdletScanDisabled This is to disable scanning for cmdlets in binary modules, this is useful if you have a lot of binary modules and you don't want to scan them for cmdlets. By default it will scan for cmdlets/aliases in binary modules and add them to the module PSD1/PSM1 files. .PARAMETER NETHandleAssemblyWithSameName Adds try/catch block to handle assembly with same name is already loaded exception and ignore it. It's useful in PowerShell 7, as it's more strict about this than Windows PowerShell, and usually everything should work as expected. .PARAMETER NETLineByLineAddType Adds Add-Type line by line, this is useful if you have a lot of libraries and you want to see which one is causing the issue. .PARAMETER NETMergeLibraryDebugging Add special logic to simplify debugging of merged libraries, this is useful if you have a lot of libraries and you want to see which one is causing the issue. .PARAMETER NETResolveBinaryConflicts Add special logic to resolve binary conflicts. It uses by defalt the project name. If you want to use different name use NETResolveBinaryConflictsName .PARAMETER NETResolveBinaryConflictsName Add special logic to resolve binary conflicts for specific project name. .PARAMETER NETSearchClass Provide a name for class when using NETResolveBinaryConflicts or NETResolveBinaryConflictsName. By default it uses `$LibraryName.Initialize` however that may not be always the case .PARAMETER NETDoNotCopyLibrariesRecursively Do not copy libraries recursively. Normally all libraries are copied recursively, but this option disables that functionality so it won't copy subfolders of libraries. .PARAMETER NETHandleRuntimes Add special logic to handle runtimes. It's useful if you have a library that is not supposed to be loaded in PowerShell, but you still need it For example library that's not NET based and is as dependency for other libraries .PARAMETER SkipBuiltinReplacements Skip builtin replacements option disables builtin replacements that are done by module builder. This is useful if you use any of known replacements and you don't want them to be replaced by module builder. This has to be used on the PSPublishModule by default, as it would break the module on publish. Current known replacements are: - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0 - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1 - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1 - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0 .EXAMPLE $newConfigurationBuildSplat = @{ Enable = $true SignModule = $true MergeModuleOnBuild = $true MergeFunctionsFromApprovedModules = $true CertificateThumbprint = '483292C9E317AA1' NETResolveBinaryConflicts = $true NETResolveBinaryConflictsName = 'Transferetto' NETProjectName = 'Transferetto' NETConfiguration = 'Release' NETFramework = 'netstandard2.0' DotSourceLibraries = $true DotSourceClasses = $true DeleteTargetModuleBeforeBuild = $true } New-ConfigurationBuild @newConfigurationBuildSplat .NOTES General notes #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] param( [switch] $Enable, [switch] $DeleteTargetModuleBeforeBuild, [switch] $MergeModuleOnBuild, [switch] $MergeFunctionsFromApprovedModules, [switch] $SignModule, [switch] $DotSourceClasses, [switch] $DotSourceLibraries, [switch] $SeparateFileLibraries, [switch] $RefreshPSD1Only, [switch] $UseWildcardForFunctions, [switch] $LocalVersioning, [switch] $SkipBuiltinReplacements, [switch] $DoNotAttemptToFixRelativePaths, [string] $CertificateThumbprint, [string] $CertificatePFXPath, [string] $CertificatePFXBase64, [string] $CertificatePFXPassword, [string] $NETProjectPath, [ValidateSet('Release', 'Debug')][string] $NETConfiguration, # may need to allow user choice [string[]] $NETFramework, [string] $NETProjectName, [switch] $NETExcludeMainLibrary, [string[]] $NETExcludeLibraryFilter, [string[]] $NETIgnoreLibraryOnLoad, [string[]] $NETBinaryModule, [alias('HandleAssemblyWithSameName')][switch] $NETHandleAssemblyWithSameName, [switch] $NETLineByLineAddType, [switch] $NETBinaryModuleCmdletScanDisabled, [alias("MergeLibraryDebugging")][switch] $NETMergeLibraryDebugging, [alias("ResolveBinaryConflicts")][switch] $NETResolveBinaryConflicts, [alias("ResolveBinaryConflictsName")][string] $NETResolveBinaryConflictsName, [alias("NETDocumentation", "NETBinaryModuleDocumenation")][switch] $NETBinaryModuleDocumentation, [switch] $NETDoNotCopyLibrariesRecursively, [string] $NETSearchClass, [switch] $NETHandleRuntimes ) if ($PSBoundParameters.ContainsKey('Enable')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ Enable = $Enable.IsPresent } } } if ($PSBoundParameters.ContainsKey('DeleteTargetModuleBeforeBuild')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ DeleteBefore = $DeleteTargetModuleBeforeBuild.IsPresent } } } if ($PSBoundParameters.ContainsKey('MergeModuleOnBuild')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ Merge = $MergeModuleOnBuild.IsPresent } } } if ($PSBoundParameters.ContainsKey('MergeFunctionsFromApprovedModules')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ MergeMissing = $MergeFunctionsFromApprovedModules.IsPresent } } } if ($PSBoundParameters.ContainsKey('SignModule')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ SignMerged = $SignModule.IsPresent } } } if ($PSBoundParameters.ContainsKey('DotSourceClasses')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ ClassesDotSource = $DotSourceClasses.IsPresent } } } if ($PSBoundParameters.ContainsKey('DotSourceLibraries')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ LibraryDotSource = $DotSourceLibraries.IsPresent } } } if ($PSBoundParameters.ContainsKey('SeparateFileLibraries')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ LibrarySeparateFile = $SeparateFileLibraries.IsPresent } } } if ($PSBoundParameters.ContainsKey('RefreshPSD1Only')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ RefreshPSD1Only = $RefreshPSD1Only.IsPresent } } } if ($PSBoundParameters.ContainsKey('UseWildcardForFunctions')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ UseWildcardForFunctions = $UseWildcardForFunctions.IsPresent } } } if ($PSBoundParameters.ContainsKey('LocalVersioning')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ LocalVersion = $LocalVersioning.IsPresent } } } if ($PSBoundParameters.ContainsKey('DoNotAttemptToFixRelativePaths')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ DoNotAttemptToFixRelativePaths = $DoNotAttemptToFixRelativePaths.IsPresent } } } if ($PSBoundParameters.ContainsKey('NETMergeLibraryDebugging')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ DebugDLL = $NETMergeLibraryDebugging.IsPresent } } } if ($PSBoundParameters.ContainsKey('NETResolveBinaryConflictsName')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ ResolveBinaryConflicts = @{ ProjectName = $NETResolveBinaryConflictsName } } } } elseif ($PSBoundParameters.ContainsKey('NETResolveBinaryConflicts')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ ResolveBinaryConflicts = $NETResolveBinaryConflicts.IsPresent } } } if ($PSBoundParameters.ContainsKey('CertificateThumbprint')) { [ordered] @{ Type = 'Options' Options = [ordered] @{ Signing = [ordered] @{ CertificateThumbprint = $CertificateThumbprint } } } } elseif ($PSBoundParameters.ContainsKey('CertificatePFXPath')) { if ($PSBoundParameters.ContainsKey('CertificatePFXPassword')) { [ordered] @{ Type = 'Options' Options = [ordered] @{ Signing = [ordered] @{ CertificatePFXPath = $CertificatePFXPath CertificatePFXPassword = $CertificatePFXPassword } } } } else { throw "CertificatePFXPassword is required when using CertificatePFXPath" } } elseif ($PSBoundParameters.ContainsKey('CertificatePFXBase64')) { if ($PSBoundParameters.ContainsKey('CertificatePFXPassword')) { [ordered] @{ Type = 'Options' Options = [ordered] @{ Signing = [ordered] @{ CertificatePFXBase64 = $CertificatePFXBase64 CertificatePFXPassword = $CertificatePFXPassword } } } } else { throw "CertificatePFXPassword is required when using CertificatePFXBase64" } } if ($PSBoundParameters.ContainsKey('NETConfiguration')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ Enable = $true Configuration = $NETConfiguration } } } if ($PSBoundParameters.ContainsKey('NETFramework')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ Enable = $true Framework = $NETFramework } } } if ($PSBoundParameters.ContainsKey('NETProjectName')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ ProjectName = $NETProjectName } } } if ($PSBoundParameters.ContainsKey('NETExcludeMainLibrary')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ ExcludeMainLibrary = $NETExcludeMainLibrary.IsPresent } } } if ($PSBoundParameters.ContainsKey('NETExcludeLibraryFilter')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ ExcludeLibraryFilter = $NETExcludeLibraryFilter } } } if ($PSBoundParameters.ContainsKey('NETIgnoreLibraryOnLoad')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ IgnoreLibraryOnLoad = $NETIgnoreLibraryOnLoad } } } if ($PSBoundParameters.ContainsKey('NETBinaryModule')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ BinaryModule = $NETBinaryModule } } } if ($PSBoundParameters.ContainsKey('NETHandleAssemblyWithSameName')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ HandleAssemblyWithSameName = $NETHandleAssemblyWithSameName.IsPresent } } } if ($PSBoundParameters.ContainsKey('NETLineByLineAddType')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ NETLineByLineAddType = $NETLineByLineAddType.IsPresent } } } if ($PSBoundParameters.ContainsKey('NETProjectPath')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ NETProjectPath = $NETProjectPath } } } if ($PSBoundParameters.ContainsKey('NETBinaryModuleCmdletScanDisabled')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ BinaryModuleCmdletScanDisabled = $NETBinaryModuleCmdletScanDisabled.IsPresent } } } if ($PSBoundParameters.ContainsKey('NETSearchClass')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ SearchClass = $NETSearchClass } } } if ($PSBoundParameters.ContainsKey('NETBinaryModuleDocumentation')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ NETBinaryModuleDocumentation = $NETBinaryModuleDocumentation.IsPresent } } } if ($PSBoundParameters.ContainsKey('NETHandleRuntimes')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ HandleRuntimes = $NETHandleRuntimes.IsPresent } } } if ($PSBoundParameters.ContainsKey('NETDoNotCopyLibrariesRecursively')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ NETDoNotCopyLibrariesRecursively = $NETDoNotCopyLibrariesRecursively.IsPresent } } } if ($PSBoundParameters.ContainsKey('SkipBuiltinReplacements')) { [ordered] @{ Type = 'PlaceHolderOption' PlaceHolderOption = [ordered]@{ SkipBuiltinReplacements = $true } } } } function New-ConfigurationCommand { [CmdletBinding()] param( [string] $ModuleName, [string[]] $CommandName ) $Configuration = [ordered] @{ Type = 'Command' Configuration = [ordered] @{ ModuleName = $ModuleName CommandName = $CommandName } } $Configuration } function New-ConfigurationDocumentation { <# .SYNOPSIS Enables or disables creation of documentation from the module using PlatyPS .DESCRIPTION Enables or disables creation of documentation from the module using PlatyPS .PARAMETER Enable Enables creation of documentation from the module. If not specified, the documentation will not be created. .PARAMETER StartClean Removes all files from the documentation folder before creating new documentation. Otherwise the `Update-MarkdownHelpModule` will be used to update the documentation. .PARAMETER UpdateWhenNew Updates the documentation right after running `New-MarkdownHelp` due to platyPS bugs. .PARAMETER Path Path to the folder where documentation will be created. .PARAMETER PathReadme Path to the readme file that will be used for the documentation. .PARAMETER Tool Tool to use for documentation generation. By default `HelpOut` is used. Available options are `PlatyPS` and `HelpOut`. .EXAMPLE New-ConfigurationDocumentation -Enable:$false -StartClean -UpdateWhenNew -PathReadme 'Docs\Readme.md' -Path 'Docs' .EXAMPLE New-ConfigurationDocumentation -Enable -PathReadme 'Docs\Readme.md' -Path 'Docs' .NOTES General notes #> [CmdletBinding()] param( [switch] $Enable, [switch] $StartClean, [switch] $UpdateWhenNew, [Parameter(Mandatory)][string] $Path, [Parameter(Mandatory)][string] $PathReadme, [ValidateSet('PlatyPS', 'HelpOut')][string] $Tool = 'PlatyPS' ) if ($Tool -eq 'PlatyPS') { $ModuleExists = Get-Module -Name 'PlatyPS' -ListAvailable if (-not $ModuleExists) { Write-Warning "Module PlatyPS is not installed. Please install it using Install-Module PlatyPS -Force -Verbose" return } } elseif ($Tool -eq 'HelpOut') { $ModuleExists = Get-Module -Name 'HelpOut' -ListAvailable if (-not $ModuleExists) { Write-Warning "Module HelpOut is not installed. Please install it using Install-Module HelpOut -Force -Verbose" return } } else { return } if ($Path -or $PathReadme) { $Option = [ordered] @{ Type = 'Documentation' Configuration = [ordered] @{ Path = $Path PathReadme = $PathReadme } } $Option } if ($Enable -or $StartClean -or $UpdateWhenNew) { $Option = [ordered]@{ Type = 'BuildDocumentation' Configuration = [ordered]@{ Enable = $Enable StartClean = $StartClean UpdateWhenNew = $UpdateWhenNew Tool = $Tool } } $Option } } function New-ConfigurationExecute { [CmdletBinding()] param( ) } function New-ConfigurationFormat { [CmdletBinding()] param( [Parameter(Mandatory)] [validateSet( 'OnMergePSM1', 'OnMergePSD1', 'DefaultPSM1', 'DefaultPSD1' #"DefaultPublic", 'DefaultPrivate', 'DefaultOther' )][string[]]$ApplyTo, [switch] $EnableFormatting, [validateSet('None', 'Asc', 'Desc')][string] $Sort, [switch] $RemoveComments, [switch] $RemoveEmptyLines, [switch] $RemoveAllEmptyLines, [switch] $RemoveCommentsInParamBlock, [switch] $RemoveCommentsBeforeParamBlock, [switch] $PlaceOpenBraceEnable, [switch] $PlaceOpenBraceOnSameLine, [switch] $PlaceOpenBraceNewLineAfter, [switch] $PlaceOpenBraceIgnoreOneLineBlock, [switch] $PlaceCloseBraceEnable, [switch] $PlaceCloseBraceNewLineAfter, [switch] $PlaceCloseBraceIgnoreOneLineBlock, [switch] $PlaceCloseBraceNoEmptyLineBefore, [switch] $UseConsistentIndentationEnable, [ValidateSet('space', 'tab')][string] $UseConsistentIndentationKind, [ValidateSet('IncreaseIndentationAfterEveryPipeline', 'NoIndentation')][string] $UseConsistentIndentationPipelineIndentation, [int] $UseConsistentIndentationIndentationSize, [switch] $UseConsistentWhitespaceEnable, [switch] $UseConsistentWhitespaceCheckInnerBrace, [switch] $UseConsistentWhitespaceCheckOpenBrace, [switch] $UseConsistentWhitespaceCheckOpenParen, [switch] $UseConsistentWhitespaceCheckOperator, [switch] $UseConsistentWhitespaceCheckPipe, [switch] $UseConsistentWhitespaceCheckSeparator, [switch] $AlignAssignmentStatementEnable, [switch] $AlignAssignmentStatementCheckHashtable, [switch] $UseCorrectCasingEnable, [ValidateSet('Minimal', 'Native')][string] $PSD1Style ) $SettingsCount = 0 $Options = [ordered] @{ Merge = [ordered] @{ } Standard = [ordered] @{ } } foreach ($Apply in $ApplyTo) { $Formatting = [ordered] @{} if ($PSBoundParameters.ContainsKey('RemoveComments')) { $Formatting.RemoveComments = $RemoveComments.IsPresent } if ($PSBoundParameters.ContainsKey('RemoveEmptyLines')) { $Formatting.RemoveEmptyLines = $RemoveEmptyLines.IsPresent } if ($PSBoundParameters.ContainsKey('RemoveAllEmptyLines')) { $Formatting.RemoveAllEmptyLines = $RemoveAllEmptyLines.IsPresent } if ($PSBoundParameters.ContainsKey('RemoveCommentsInParamBlock')) { $Formatting.RemoveCommentsInParamBlock = $RemoveCommentsInParamBlock.IsPresent } if ($PSBoundParameters.ContainsKey('RemoveCommentsBeforeParamBlock')) { $Formatting.RemoveCommentsBeforeParamBlock = $RemoveCommentsBeforeParamBlock.IsPresent } $Formatting.FormatterSettings = [ordered] @{ IncludeRules = @( if ($PlaceOpenBraceEnable) { 'PSPlaceOpenBrace' } if ($PlaceCloseBraceEnable) { 'PSPlaceCloseBrace' } if ($UseConsistentIndentationEnable) { 'PSUseConsistentIndentation' } if ($UseConsistentWhitespaceEnable) { 'PSUseConsistentWhitespace' } if ($AlignAssignmentStatementEnable) { 'PSAlignAssignmentStatement' } if ($UseCorrectCasingEnable) { 'PSUseCorrectCasing' } ) Rules = [ordered] @{} } if ($PlaceOpenBraceEnable) { $Formatting.FormatterSettings.Rules.PSPlaceOpenBrace = [ordered] @{ Enable = $true OnSameLine = $PlaceOpenBraceOnSameLine.IsPresent NewLineAfter = $PlaceOpenBraceNewLineAfter.IsPresent IgnoreOneLineBlock = $PlaceOpenBraceIgnoreOneLineBlock.IsPresent } } if ($PlaceCloseBraceEnable) { $Formatting.FormatterSettings.Rules.PSPlaceCloseBrace = [ordered] @{ Enable = $true NewLineAfter = $PlaceCloseBraceNewLineAfter.IsPresent IgnoreOneLineBlock = $PlaceCloseBraceIgnoreOneLineBlock.IsPresent NoEmptyLineBefore = $PlaceCloseBraceNoEmptyLineBefore.IsPresent } } if ($UseConsistentIndentationEnable) { $Formatting.FormatterSettings.Rules.PSUseConsistentIndentation = [ordered] @{ Enable = $true Kind = $UseConsistentIndentationKind PipelineIndentation = $UseConsistentIndentationPipelineIndentation IndentationSize = $UseConsistentIndentationIndentationSize } } if ($UseConsistentWhitespaceEnable) { $Formatting.FormatterSettings.Rules.PSUseConsistentWhitespace = [ordered] @{ Enable = $true CheckInnerBrace = $UseConsistentWhitespaceCheckInnerBrace.IsPresent CheckOpenBrace = $UseConsistentWhitespaceCheckOpenBrace.IsPresent CheckOpenParen = $UseConsistentWhitespaceCheckOpenParen.IsPresent CheckOperator = $UseConsistentWhitespaceCheckOperator.IsPresent CheckPipe = $UseConsistentWhitespaceCheckPipe.IsPresent CheckSeparator = $UseConsistentWhitespaceCheckSeparator.IsPresent } } if ($AlignAssignmentStatementEnable) { $Formatting.FormatterSettings.Rules.PSAlignAssignmentStatement = [ordered] @{ Enable = $true CheckHashtable = $AlignAssignmentStatementCheckHashtable.IsPresent } } if ($UseCorrectCasingEnable) { $Formatting.FormatterSettings.Rules.PSUseCorrectCasing = [ordered] @{ Enable = $true } } Remove-EmptyValue -Hashtable $Formatting.FormatterSettings -Recursive if ($Formatting.FormatterSettings.Keys.Count -eq 0) { $null = $Formatting.Remove('FormatterSettings') } if ($Formatting.Count -gt 0 -or $EnableFormatting) { $SettingsCount++ $Formatting.Enabled = $true if ($Apply -eq 'OnMergePSM1') { $Options.Merge.FormatCodePSM1 = $Formatting } elseif ($Apply -eq 'OnMergePSD1') { $Options.Merge.FormatCodePSD1 = $Formatting } elseif ($Apply -eq 'DefaultPSM1') { $Options.Standard.FormatCodePSM1 = $Formatting } elseif ($Apply -eq 'DefaultPSD1') { $Options.Standard.FormatCodePSD1 = $Formatting } elseif ($Apply -eq 'DefaultPublic') { $Options.Standard.FormatCodePublic = $Formatting } elseif ($Apply -eq 'DefaultPrivate') { $Options.Standard.FormatCodePrivate = $Formatting } elseif ($Apply -eq 'DefaultOther') { $Options.Standard.FormatCodeOther = $Formatting } else { throw "Unknown ApplyTo: $Apply" } } if ($PSD1Style) { if ($Apply -eq 'OnMergePSD1') { $SettingsCount++ $Options['Merge']['Style'] = [ordered] @{} $Options['Merge']['Style']['PSD1'] = $PSD1Style } elseif ($Apply -eq 'DefaultPSD1') { $SettingsCount++ $Options['Standard']['Style'] = [ordered] @{} $Options['Standard']['Style']['PSD1'] = $PSD1Style } } } if ($SettingsCount -gt 0) { $Output = [ordered] @{ Type = 'Formatting' Options = $Options } $Output } } function New-ConfigurationImportModule { <# .SYNOPSIS Creates a configuration for importing PowerShell modules. .DESCRIPTION This function generates a configuration object for importing PowerShell modules. It allows specifying whether to import the current module itself and/or any required modules. .PARAMETER ImportSelf Indicates whether to import the current module itself. .PARAMETER ImportRequiredModules Indicates whether to import any required modules specified in the module manifest. .EXAMPLE New-ConfigurationImportModule -ImportSelf -ImportRequiredModules .NOTES This function helps in creating a standardized import configuration for PowerShell modules. #> [CmdletBinding()] param( [switch] $ImportSelf, [switch] $ImportRequiredModules ) $Output = [ordered] @{ Type = 'ImportModules' ImportModules = [ordered] @{} } if ($PSBoundParameters.Keys.Contains('ImportSelf')) { $Output['ImportModules']['Self'] = $ImportSelf } if ($PSBoundParameters.Keys.Contains('ImportRequiredModules')) { $Output['ImportModules']['RequiredModules'] = $ImportRequiredModules } if ($VerbosePreference) { $Output['ImportModules']['Verbose'] = $true } $Output } function New-ConfigurationInformation { [cmdletbinding()] param( [string] $FunctionsToExportFolder, [string] $AliasesToExportFolder, [string[]] $ExcludeFromPackage, [string[]] $IncludeRoot, [string[]] $IncludePS1, [string[]] $IncludeAll, [scriptblock] $IncludeCustomCode, [System.Collections.IDictionary] $IncludeToArray, [string] $LibrariesCore, [string] $LibrariesDefault, [string] $LibrariesStandard ) $Configuration = [ordered] @{ FunctionsToExportFolder = $FunctionsToExportFolder AliasesToExportFolder = $AliasesToExportFolder ExcludeFromPackage = $ExcludeFromPackage IncludeRoot = $IncludeRoot IncludePS1 = $IncludePS1 IncludeAll = $IncludeAll IncludeCustomCode = $IncludeCustomCode IncludeToArray = $IncludeToArray LibrariesCore = $LibrariesCore LibrariesDefault = $LibrariesDefault LibrariesStandard = $LibrariesStandard } Remove-EmptyValue -Hashtable $Configuration $Option = @{ Type = 'Information' Configuration = $Configuration } $Option } function New-ConfigurationManifest { <# .SYNOPSIS Creates a new configuration manifest for a PowerShell module. .DESCRIPTION This function generates a new configuration manifest for a PowerShell module. The manifest includes metadata about the module such as version, author, company, and other relevant information. It also allows specifying the functions, cmdlets, and aliases to export. .PARAMETER ModuleVersion Specifies the version of the module. When multiple versions of a module exist on a system, the latest version is loaded by default when you run Import-Module. .PARAMETER CompatiblePSEditions Specifies the module's compatible PowerShell editions. Valid values are 'Desktop' and 'Core'. .PARAMETER GUID Specifies a unique identifier for the module. The GUID is used to distinguish between modules with the same name. .PARAMETER Author Identifies the module author. .PARAMETER CompanyName Identifies the company or vendor who created the module. .PARAMETER Copyright Specifies a copyright statement for the module. .PARAMETER Description Describes the module at a high level. .PARAMETER PowerShellVersion Specifies the minimum version of PowerShell this module requires. Default is '5.1'. .PARAMETER Tags Specifies tags for the module. .PARAMETER IconUri Specifies the URI for the module's icon. .PARAMETER ProjectUri Specifies the URI for the module's project page. .PARAMETER DotNetFrameworkVersion Specifies the minimum version of the Microsoft .NET Framework that the module requires. .PARAMETER LicenseUri Specifies the URI for the module's license. .PARAMETER Prerelease Specifies the prerelease tag for the module. .PARAMETER FunctionsToExport Defines functions to export in the module manifest. By default, functions are auto-detected, but this allows you to override that. .PARAMETER AliasesToExport Defines aliases to export in the module manifest. By default, aliases are auto-detected, but this allows you to override that. .PARAMETER CmdletsToExport Defines cmdlets to export in the module manifest. By default, cmdlets are auto-detected, but this allows you to override that. .EXAMPLE New-ConfigurationManifest -ModuleVersion '1.0.0' -GUID '12345678-1234-1234-1234-1234567890ab' -Author 'John Doe' -CompanyName 'Example Corp' -Description 'This is an example module.' .NOTES This function helps in creating a standardized module manifest for PowerShell modules. #> [CmdletBinding()] param( [Parameter(Mandatory)][string] $ModuleVersion, [ValidateSet('Desktop', 'Core')][string[]] $CompatiblePSEditions = @('Desktop', 'Core'), [Parameter(Mandatory)][string] $GUID, [Parameter(Mandatory)][string] $Author, [string] $CompanyName, [string] $Copyright, [string] $Description, [string] $PowerShellVersion = '5.1', [string[]] $Tags, [string] $IconUri, [string] $ProjectUri, [string] $DotNetFrameworkVersion, [string] $LicenseUri, [alias('PrereleaseTag')][string] $Prerelease, [string[]] $FunctionsToExport, [string[]] $CmdletsToExport, [string[]] $AliasesToExport ) $Manifest = [ordered] @{ ModuleVersion = $ModuleVersion CompatiblePSEditions = @($CompatiblePSEditions) GUID = $GUID Author = $Author CompanyName = $CompanyName Copyright = $Copyright Description = $Description PowerShellVersion = $PowerShellVersion Tags = $Tags IconUri = $IconUri ProjectUri = $ProjectUri DotNetFrameworkVersion = $DotNetFrameworkVersion LicenseUri = $LicenseUri Prerelease = $Prerelease FunctionsToExport = $FunctionsToExport CmdletsToExport = $CmdletsToExport AliasesToExport = $AliasesToExport } Remove-EmptyValue -Hashtable $Manifest $Option = @{ Type = 'Manifest' Configuration = $Manifest } $Option } function New-ConfigurationModule { <# .SYNOPSIS Provides a way to configure Required Modules or External Modules that will be used in the project. .DESCRIPTION Provides a way to configure Required Modules or External Modules that will be used in the project. .PARAMETER Type Choose between RequiredModule, ExternalModule and ApprovedModule, where RequiredModule is the default. .PARAMETER Name Name of PowerShell module that you want your module to depend on. .PARAMETER Version Version of PowerShell module that you want your module to depend on. If you don't specify a version, any version of the module is acceptable. You can also use word 'Latest' to specify that you want to use the latest version of the module, and the module will be pickup up latest version available on the system. .PARAMETER RequiredVersion RequiredVersion of PowerShell module that you want your module to depend on. This forces the module to require this specific version. When using Version, the module will be picked up if it's equal or higher than the version specified. When using RequiredVersion, the module will be picked up only if it's equal to the version specified. .PARAMETER Guid Guid of PowerShell module that you want your module to depend on. If you don't specify a Guid, any Guid of the module is acceptable, but it is recommended to specify it. Alternatively you can use word 'Auto' to specify that you want to use the Guid of the module, and the module GUID .EXAMPLE # Add standard module dependencies (directly, but can be used with loop as well) New-ConfigurationModule -Type RequiredModule -Name 'platyPS' -Guid 'Auto' -Version 'Latest' New-ConfigurationModule -Type RequiredModule -Name 'powershellget' -Guid 'Auto' -Version 'Latest' New-ConfigurationModule -Type RequiredModule -Name 'PSScriptAnalyzer' -Guid 'Auto' -Version 'Latest' .EXAMPLE # Add external module dependencies, using loop for simplicity foreach ($Module in @('Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Archive', 'Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Security')) { New-ConfigurationModule -Type ExternalModule -Name $Module } .EXAMPLE # Add approved modules, that can be used as a dependency, but only when specific function from those modules is used # And on that time only that function and dependant functions will be copied over # Keep in mind it has it's limits when "copying" functions such as it should not depend on DLLs or other external files New-ConfigurationModule -Type ApprovedModule -Name 'PSSharedGoods', 'PSWriteColor', 'Connectimo', 'PSUnifi', 'PSWebToolbox', 'PSMyPassword' .NOTES General notes #> [CmdletBinding()] param( [validateset('RequiredModule', 'ExternalModule', 'ApprovedModule')] $Type = 'RequiredModule', [Parameter(Mandatory)][string[]] $Name, [string] $Version, [string] $RequiredVersion, [string] $Guid ) foreach ($N in $Name) { if ($Type -eq 'ApprovedModule') { $Configuration = $N } else { $ModuleInformation = [ordered] @{ ModuleName = $N ModuleVersion = $Version RequiredVersion = $RequiredVersion Guid = $Guid } if ($Version -and $RequiredVersion) { throw 'You cannot use both Version and RequiredVersion at the same time for the same module. Please choose one or the other (New-ConfigurationModule) ' } Remove-EmptyValue -Hashtable $ModuleInformation if ($ModuleInformation.Count -eq 0) { return } elseif ($ModuleInformation.Count -eq 1 -and $ModuleInformation.Contains('ModuleName')) { $Configuration = $N } else { $Configuration = $ModuleInformation } } $Option = @{ Type = $Type Configuration = $Configuration } $Option } } Register-ArgumentCompleter -CommandName New-ConfigurationModule -ParameterName Version -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) 'Auto', 'Latest' | Where-Object { $_ -like "*$wordToComplete*" } } Register-ArgumentCompleter -CommandName New-ConfigurationModule -ParameterName Guid -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) 'Auto', 'Latest' | Where-Object { $_ -like "*$wordToComplete*" } } function New-ConfigurationModuleSkip { <# .SYNOPSIS Provides a way to ignore certain commands or modules during build process and continue module building on errors. .DESCRIPTION Provides a way to ignore certain commands or modules during build process and continue module building on errors. During build if a build module can't find require module or command it will fail the build process to prevent incomplete module from being created. This option allows to skip certain modules or commands and continue building the module. This is useful for commands we know are not available on all systems, or we get them different way. .PARAMETER IgnoreModuleName Ignore module name or names. If the module is not available on the system it will be ignored and build process will continue. .PARAMETER IgnoreFunctionName Ignore function name or names. If the function is not available in the module it will be ignored and build process will continue. .PARAMETER Force This switch will force build process to continue even if the module or command is not available (aka you know what you are doing) .EXAMPLE New-ConfigurationModuleSkip -IgnoreFunctionName 'Invoke-Formatter', 'Find-Module' -IgnoreModuleName 'platyPS' .NOTES General notes #> [CmdletBinding()] param( [string[]] $IgnoreModuleName, [string[]] $IgnoreFunctionName, [switch] $Force ) $Configuration = [ordered] @{ Type = 'ModuleSkip' Configuration = [ordered] @{ IgnoreModuleName = $IgnoreModuleName IgnoreFunctionName = $IgnoreFunctionName Force = $Force } } Remove-EmptyValue -Hashtable $Configuration.Configuration $Configuration } function New-ConfigurationPlaceHolder { <# .SYNOPSIS Command helping define custom placeholders replacing content within a script or module during the build process. .DESCRIPTION Command helping define custom placeholders replacing content within a script or module during the build process. It modifies only the content of the script or module (PSM1) and does not modify the sources. .PARAMETER CustomReplacement Hashtable array with custom placeholders to replace. Each hashtable must contain two keys: Find and Replace. .PARAMETER Find The string to find in the script or module content. .PARAMETER Replace The string to replace the Find string in the script or module content. .EXAMPLE New-ConfigurationPlaceHolder -Find '{CustomName}' -Replace 'SpecialCase' .EXAMPLE New-ConfigurationPlaceHolder -CustomReplacement @( @{ Find = '{CustomName}'; Replace = 'SpecialCase' } @{ Find = '{CustomVersion}'; Replace = '1.0.0' } ) .NOTES General notes #> [CmdletBinding(DefaultParameterSetName = 'FindAndReplace')] param( [Parameter(Mandatory, ParameterSetName = 'CustomReplacement')][System.Collections.IDictionary[]] $CustomReplacement, [Parameter(Mandatory, ParameterSetName = 'FindAndReplace')][string] $Find, [Parameter(Mandatory, ParameterSetName = 'FindAndReplace')][string] $Replace ) foreach ($Replacement in $CustomReplacement) { [ordered] @{ Type = 'PlaceHolder' Configuration = $Replacement } } if ($PSBoundParameters.ContainsKey("Find") -and $PSBoundParameters.ContainsKey("Replace")) { [ordered] @{ Type = 'PlaceHolder' Configuration = @{ Find = $Find Replace = $Replace } } } } function New-ConfigurationPublish { <# .SYNOPSIS Provide a way to configure publishing to PowerShell Gallery or GitHub .DESCRIPTION Provide a way to configure publishing to PowerShell Gallery or GitHub You can configure publishing to both at the same time You can publish to multiple PowerShellGalleries at the same time as well You can have multiple GitHub configurations at the same time as well .PARAMETER Type Choose between PowerShellGallery and GitHub .PARAMETER FilePath API Key to be used for publishing to GitHub or PowerShell Gallery in clear text in file .PARAMETER UserName When used for GitHub this parameter is required to know to which repository to publish. This parameter is not used for PSGallery publishing .PARAMETER RepositoryName When used for PowerShellGallery publishing this parameter provides a way to overwrite default PowerShellGallery and publish to a different repository When not used, the default PSGallery will be used. When used for GitHub publishing this parameter provides a way to overwrite default repository name and publish to a different repository When not used, the default repository name will be used, that matches the module name .PARAMETER ApiKey API Key to be used for publishing to GitHub or PowerShell Gallery in clear text .PARAMETER Enabled Enable publishing to GitHub or PowerShell Gallery .PARAMETER PreReleaseTag Allow to publish to GitHub as pre-release. By default it will be published as release .PARAMETER OverwriteTagName Allow to overwrite tag name when publishing to GitHub. By default "v<ModuleVersion>" will be used i.e v1.0.0 You can use following variables that will be replaced with actual values: - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0 - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1 - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1 - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0 .PARAMETER DoNotMarkAsPreRelease Allow to publish to GitHub as release even if pre-release tag is set on the module version. By default it will be published as pre-release if pre-release tag is set. This setting prevents it. .PARAMETER Force Allow to publish lower version of module on PowerShell Gallery. By default it will fail if module with higher version already exists. .PARAMETER ID Optional ID of the artefact. If not specified, the default packed artefact will be used. If no packed artefact is specified, the first packed artefact will be used (if enabled) If no packed artefact is enabled, the publishing will fail .EXAMPLE New-ConfigurationPublish -Type PowerShellGallery -FilePath 'C:\Support\Important\PowerShellGalleryAPI.txt' -Enabled:$true .EXAMPLE New-ConfigurationPublish -Type GitHub -FilePath 'C:\Support\Important\GitHubAPI.txt' -UserName 'EvotecIT' -Enabled:$true -ID 'ToGitHub' .NOTES General notes #> [CmdletBinding()] param( [Parameter(Mandatory, ParameterSetName = 'ApiKey')] [Parameter(Mandatory, ParameterSetName = 'ApiFromFile')] [ValidateSet('PowerShellGallery', 'GitHub')][string] $Type, [Parameter(Mandatory, ParameterSetName = 'ApiFromFile')][string] $FilePath, [Parameter(Mandatory, ParameterSetName = 'ApiKey')][string] $ApiKey, [Parameter(ParameterSetName = 'ApiKey')] [Parameter(ParameterSetName = 'ApiFromFile')] [string] $UserName, [Parameter(ParameterSetName = 'ApiKey')] [Parameter(ParameterSetName = 'ApiFromFile')] [string] $RepositoryName, [Parameter(ParameterSetName = 'ApiKey')] [Parameter(ParameterSetName = 'ApiFromFile')] [switch] $Enabled, # [Parameter(ParameterSetName = 'ApiKey')] # [Parameter(ParameterSetName = 'ApiFromFile')] # [string] $PreReleaseTag, [Parameter(ParameterSetName = 'ApiKey')] [Parameter(ParameterSetName = 'ApiFromFile')] [string] $OverwriteTagName, [Parameter(ParameterSetName = 'ApiKey')] [Parameter(ParameterSetName = 'ApiFromFile')] [switch] $Force, [Parameter(ParameterSetName = 'ApiKey')] [Parameter(ParameterSetName = 'ApiFromFile')] [string] $ID, [Parameter(ParameterSetName = 'ApiKey')] [Parameter(ParameterSetName = 'ApiFromFile')] [switch] $DoNotMarkAsPreRelease ) if ($FilePath) { $ApiKeyToUse = Get-Content -Path $FilePath -ErrorAction Stop -Encoding UTF8 } else { $ApiKeyToUse = $ApiKey } if ($Type -eq 'PowerShellGallery') { $TypeToUse = 'GalleryNuget' } elseif ($Type -eq 'GitHub') { $TypeToUse = 'GitHubNuget' if (-not $UserName) { throw 'UserName is required for GitHub. Please fix New-ConfigurationPublish and provide UserName' } } else { return } $Settings = [ordered] @{ Type = $TypeToUse Configuration = [ordered] @{ Type = $Type ApiKey = $ApiKeyToUse ID = $ID Enabled = $Enabled UserName = $UserName RepositoryName = $RepositoryName Force = $Force.IsPresent OverwriteTagName = $OverwriteTagName DoNotMarkAsPreRelease = $DoNotMarkAsPreRelease.IsPresent Verbose = $VerbosePreference } } Remove-EmptyValue -Hashtable $Settings -Recursive 2 $Settings } function New-ConfigurationTest { [CmdletBinding()] param( #[Parameter(Mandatory)][ValidateSet('BeforeMerge', 'AfterMerge')][string[]] $When, [Parameter(Mandatory)][string] $TestsPath, [switch] $Enable, [switch] $Force ) if ($Enable) { if ($null -eq $IsWindows -or $IsWindows -eq $true) { $TestsPath = $TestsPath.Replace('/', '\') } else { $TestsPath = $TestsPath.Replace('\', '/') } $When = 'AfterMerge' foreach ($W in $When) { $Configuration = [ordered] @{ Type = "Tests$W" Configuration = [ordered] @{ When = $W TestsPath = $TestsPath Force = $Force.ispresent } } Remove-EmptyValue -Hashtable $Configuration.Configuration $Configuration } } } function Publish-GitHubReleaseAsset { <# .SYNOPSIS Publishes a release asset to GitHub. .DESCRIPTION Uses `Send-GitHubRelease` to create or update a GitHub release based on the project version and upload the generated zip archive. .PARAMETER ProjectPath Path to the project folder containing the *.csproj file. .PARAMETER GitHubUsername GitHub account name owning the repository. .PARAMETER GitHubRepositoryName Name of the GitHub repository. .PARAMETER GitHubAccessToken Personal access token used for authentication. .PARAMETER IsPreRelease Publish the release as a pre-release. .EXAMPLE Publish-GitHubReleaseAsset -ProjectPath 'C:\Git\MyProject' -GitHubUsername 'EvotecIT' -GitHubRepositoryName 'MyRepo' -GitHubAccessToken $Token Uploads the current project zip to the specified GitHub repository. #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$ProjectPath, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$GitHubUsername, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$GitHubRepositoryName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$GitHubAccessToken, [switch]$IsPreRelease ) $result = [ordered]@{ Success = $false TagName = $null ZipPath = $null ReleaseUrl = $null ErrorMessage = $null } if (-not (Test-Path -LiteralPath $ProjectPath)) { $result.ErrorMessage = "Project path '$ProjectPath' not found." return [PSCustomObject]$result } $csproj = Get-ChildItem -Path $ProjectPath -Filter '*.csproj' -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $csproj) { $result.ErrorMessage = "No csproj found in $ProjectPath" return [PSCustomObject]$result } try { [xml]$xml = Get-Content -LiteralPath $csproj.FullName -Raw -ErrorAction Stop } catch { $result.ErrorMessage = "Failed to read '$($csproj.FullName)' as XML: $_" return [PSCustomObject]$result } $version = ($xml.Project.PropertyGroup | Where-Object { $_.VersionPrefix } | Select-Object -First 1).VersionPrefix if (-not $version) { $result.ErrorMessage = "VersionPrefix not found in '$($csproj.FullName)'" return [PSCustomObject]$result } $zipPath = Join-Path -Path $csproj.Directory.FullName -ChildPath ("bin/Release/{0}.{1}.zip" -f $csproj.BaseName, $version) if (-not (Test-Path -LiteralPath $zipPath)) { $result.ErrorMessage = "Zip file '$zipPath' not found." return [PSCustomObject]$result } $tagName = "v$version" $result.TagName = $tagName $result.ZipPath = $zipPath try { $statusGithub = Send-GitHubRelease -GitHubUsername $GitHubUsername -GitHubRepositoryName $GitHubRepositoryName -GitHubAccessToken $GitHubAccessToken -TagName $tagName -AssetFilePaths $zipPath -IsPreRelease:$IsPreRelease.IsPresent $result.Success = $statusGithub.Succeeded $result.ReleaseUrl = $statusGithub.ReleaseUrl if (-not $statusGithub.Succeeded) { $result.ErrorMessage = $statusGithub.ErrorMessage } } catch { $result.ErrorMessage = $_.Exception.Message } return [PSCustomObject]$result } function Publish-NugetPackage { <# .SYNOPSIS Pushes NuGet packages to a feed. .DESCRIPTION Finds all *.nupkg files in the specified path and uploads them using `dotnet nuget push` with the provided API key and feed URL. .PARAMETER Path Directory to search for NuGet packages. .PARAMETER ApiKey API key used to authenticate against the NuGet feed. .PARAMETER Source NuGet feed URL. Defaults to https://api.nuget.org/v3/index.json. .EXAMPLE Publish-NugetPackage -Path 'C:\Git\Project\bin\Release' -ApiKey $MyKey Uploads all packages in the Release folder to NuGet.org. #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Path, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$ApiKey, [string]$Source = 'https://api.nuget.org/v3/index.json' ) $result = [ordered]@{ Success = $true Pushed = @() Failed = @() ErrorMessage = $null } if (-not (Test-Path -LiteralPath $Path)) { $result.Success = $false $result.ErrorMessage = "Path '$Path' not found." return [PSCustomObject]$result } $packages = Get-ChildItem -Path $Path -Recurse -Filter '*.nupkg' -ErrorAction SilentlyContinue if (-not $packages) { $result.Success = $false $result.ErrorMessage = "No packages found in $Path" return [PSCustomObject]$result } foreach ($pkg in $packages) { dotnet nuget push $pkg.FullName --api-key $ApiKey --source $Source if ($LASTEXITCODE -eq 0) { $result.Pushed += $pkg.FullName } else { $result.Failed += $pkg.FullName $result.Success = $false } } return [PSCustomObject]$result } function Register-Certificate { [cmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, ParameterSetName = 'PFX')][string] $CertificatePFX, [Parameter(Mandatory, ParameterSetName = 'Store')][ValidateSet('LocalMachine', 'CurrentUser')][string] $LocalStore, [alias('CertificateThumbprint')][Parameter(ParameterSetName = 'Store')][string] $Thumbprint, [Parameter(Mandatory)][string] $Path, [string] $TimeStampServer = 'http://timestamp.digicert.com', [ValidateSet('All', 'NotRoot', 'Signer')] [string] $IncludeChain = 'All', [string[]] $Include = @('*.ps1', '*.psd1', '*.psm1', '*.dll', '*.cat'), [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512')][string] $HashAlgorithm = 'SHA256' ) if ($PSBoundParameters.Keys -contains 'LocalStore') { $Cert = Get-ChildItem -Path "Cert:\$LocalStore\My" -CodeSigningCert if ($Thumbprint) { $Certificate = $Cert | Where-Object { $_.Thumbprint -eq $Thumbprint } if (-not $Certificate) { Write-Warning -Message "Register-Certificate - No certificates found by that thumbprint" return } } elseif ($Cert.Count -eq 0) { Write-Warning -Message "Register-Certificate - No certificates found in store." return } elseif ($Cert.Count -eq 1) { $Certificate = $Cert } else { if ($Thumbprint) { $Certificate = $Cert | Where-Object { $_.Thumbprint -eq $Thumbprint } if (-not $Certificate) { Write-Warning -Message "Register-Certificate - No certificates found by that thumbprint" return } } else { $CodeError = "Get-ChildItem -Path Cert:\$LocalStore\My -CodeSigningCert" Write-Warning -Message "Register-Certificate - More than one certificate found in store. Provide Thumbprint for expected certificate" Write-Warning -Message "Register-Certificate - Use: $CodeError" $Cert return } } } elseif ($PSBoundParameters.Keys -contains 'CertificatePFX') { if (Test-Path -LiteralPath $CertificatePFX) { $Certificate = Get-PfxCertificate -FilePath $CertificatePFX if (-not $Certificate) { Write-Warning -Message "Register-Certificate - No certificates found for PFX" return } } } if ($Certificate -and $Path) { if (Test-Path -LiteralPath $Path) { if ($null -ne $IsWindows -and $IsWindows -eq $false) { $ModuleOpenAuthenticode = Get-Module -ListAvailable -Name 'OpenAuthenticode' if ($null -eq $ModuleOpenAuthenticode) { Write-Warning -Message "Register-Certificate - OpenAuthenticode module not found. Please install it from PSGallery" return } if ($IncludeChain -eq 'All') { $IncludeOption = 'WholeChain' } elseif ($IncludeChain -eq 'NotRoot') { $IncludeOption = 'ExcludeRoot' } elseif ($IncludeChain -eq 'Signer') { $IncludeOption = 'EndCertOnly' } else { $IncludeOption = 'None' } Get-ChildItem -Path $Path -Filter * -Include $Include -Recurse -ErrorAction SilentlyContinue | Where-Object { ($_ | Get-OpenAuthenticodeSignature).Status -eq 'NotSigned' } | Set-OpenAuthenticodeSignature -Certificate $Certificate -TimeStampServer $TimeStampServer -IncludeChain $IncludeOption -HashAlgorithm $HashAlgorithm } else { $ModuleSigning = Get-Command -Name Set-AuthenticodeSignature if (-not $ModuleSigning) { Write-Warning -Message "Register-Certificate - Code signing commands not found. Skipping signing." return } Get-ChildItem -Path $Path -Filter * -Include $Include -Recurse -ErrorAction SilentlyContinue | Where-Object { ($_ | Get-AuthenticodeSignature).Status -eq 'NotSigned' } | Set-AuthenticodeSignature -Certificate $Certificate -TimestampServer $TimeStampServer -IncludeChain $IncludeChain -HashAlgorithm $HashAlgorithm } } } } function Remove-Comments { <# .SYNOPSIS Remove comments from PowerShell file .DESCRIPTION Remove comments from PowerShell file and optionally remove empty lines By default comments in param block are not removed By default comments before param block are not removed .PARAMETER SourceFilePath File path to the source file .PARAMETER Content Content of the file .PARAMETER DestinationFilePath File path to the destination file. If not provided, the content will be returned .PARAMETER RemoveEmptyLines Remove empty lines if more than one empty line is found .PARAMETER RemoveAllEmptyLines Remove all empty lines from the content .PARAMETER RemoveCommentsInParamBlock Remove comments in param block. By default comments in param block are not removed .PARAMETER RemoveCommentsBeforeParamBlock Remove comments before param block. By default comments before param block are not removed .EXAMPLE Remove-Comments -SourceFilePath 'C:\Support\GitHub\PSPublishModule\Examples\TestScript.ps1' -DestinationFilePath 'C:\Support\GitHub\PSPublishModule\Examples\TestScript1.ps1' -RemoveAllEmptyLines -RemoveCommentsInParamBlock -RemoveCommentsBeforeParamBlock .NOTES Most of the work done by Chris Dent, with improvements by Przemyslaw Klys #> [CmdletBinding(DefaultParameterSetName = 'FilePath')] param( [Parameter(Mandatory, ParameterSetName = 'FilePath')] [alias('FilePath', 'Path', 'LiteralPath')][string] $SourceFilePath, [Parameter(Mandatory, ParameterSetName = 'Content')][string] $Content, [Parameter(ParameterSetName = 'Content')] [Parameter(ParameterSetName = 'FilePath')] [alias('Destination', 'OutputFile', 'OutputFilePath')][string] $DestinationFilePath, [Parameter(ParameterSetName = 'Content')] [Parameter(ParameterSetName = 'FilePath')] [switch] $RemoveAllEmptyLines, [Parameter(ParameterSetName = 'Content')] [Parameter(ParameterSetName = 'FilePath')] [switch] $RemoveEmptyLines, [Parameter(ParameterSetName = 'Content')] [Parameter(ParameterSetName = 'FilePath')] [switch] $RemoveCommentsInParamBlock, [Parameter(ParameterSetName = 'Content')] [Parameter(ParameterSetName = 'FilePath')] [switch] $RemoveCommentsBeforeParamBlock, [Parameter(ParameterSetName = 'Content')] [Parameter(ParameterSetName = 'FilePath')] [switch] $DoNotRemoveSignatureBlock ) if ($PSVersionTable.PSVersion.Major -gt 5) { $Encoding = 'UTF8BOM' } else { $Encoding = 'UTF8' } if ($SourceFilePath) { $Fullpath = Resolve-Path -LiteralPath $SourceFilePath $Content = [IO.File]::ReadAllText($FullPath, [System.Text.Encoding]::UTF8) } $Tokens = $Errors = @() $Ast = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$Tokens, [ref]$Errors) $groupedTokens = $Tokens | Group-Object { $_.Extent.StartLineNumber } $DoNotRemove = $false $DoNotRemoveCommentParam = $false $CountParams = 0 $ParamFound = $false $SignatureBlock = $false $toRemove = foreach ($line in $groupedTokens) { if ($Ast.Body.ParamBlock.Extent.StartLineNumber -gt $line.Name) { continue } $tokens = $line.Group for ($i = 0; $i -lt $line.Count; $i++) { $token = $tokens[$i] if ($token.Extent.StartOffset -lt $Ast.Body.ParamBlock.Extent.StartOffset) { continue } if ($token.Extent.Text -eq 'function') { if (-not $RemoveCommentsBeforeParamBlock) { $DoNotRemove = $true } continue } if ($token.Extent.Text -eq 'param') { $ParamFound = $true $DoNotRemove = $false } if ($DoNotRemove) { continue } if ($token.Extent.Text -eq 'param') { if (-not $RemoveCommentsInParamBlock) { $DoNotRemoveCommentParam = $true } continue } if ($ParamFound -and ($token.Extent.Text -eq '(' -or $token.Extent.Text -eq '@(')) { $CountParams += 1 } elseif ($ParamFound -and $token.Extent.Text -eq ')') { $CountParams -= 1 } if ($ParamFound -and $token.Extent.Text -eq ')') { if ($CountParams -eq 0) { $DoNotRemoveCommentParam = $false $ParamFound = $false } } if ($DoNotRemoveCommentParam) { continue } if ($token.Kind -ne 'Comment') { continue } if ($DoNotRemoveSignatureBlock) { if ($token.Kind -eq 'Comment' -and $token.Text -eq '# SIG # Begin signature block') { $SignatureBlock = $true continue } if ($SignatureBlock) { if ($token.Kind -eq 'Comment' -and $token.Text -eq '# SIG # End signature block') { $SignatureBlock = $false } continue } } $token } } $toRemove = $toRemove | Sort-Object { $_.Extent.StartOffset } -Descending foreach ($token in $toRemove) { $StartIndex = $token.Extent.StartOffset $HowManyChars = $token.Extent.EndOffset - $token.Extent.StartOffset $content = $content.Remove($StartIndex, $HowManyChars) } if ($RemoveEmptyLines) { $Content = $Content -replace '(?m)^\s*$', '' $Content = $Content -replace "(?:`r?`n|\n|\r)", "`r`n" } if ($RemoveAllEmptyLines) { $Content = $Content -replace '(?m)^\s*$(\r?\n)?', '' } if ($Content) { $Content = $Content.Trim() } if ($DestinationFilePath) { $Content | Set-Content -Path $DestinationFilePath -Encoding $Encoding } else { $Content } } function Send-GitHubRelease { <# .SYNOPSIS Creates a new Release for the given GitHub repository. .DESCRIPTION Uses the GitHub API to create a new Release for a given repository. Allows you to specify all of the Release properties, such as the Tag, Name, Assets, and if it's a Draft or Prerelease or not. .PARAMETER GitHubUsername The username that the GitHub repository exists under. e.g. For the repository https://github.com/deadlydog/New-GitHubRelease, the username is 'deadlydog'. .PARAMETER GitHubRepositoryName The name of the repository to create the Release for. e.g. For the repository https://github.com/deadlydog/New-GitHubRelease, the repository name is 'New-GitHubRelease'. .PARAMETER GitHubAccessToken The Access Token to use as credentials for GitHub. Access tokens can be generated at https://github.com/settings/tokens. The access token will need to have the repo/public_repo permission on it for it to be allowed to create a new Release. .PARAMETER TagName The name of the tag to create at the Commitish. .PARAMETER ReleaseName The name to use for the new release. If blank, the TagName will be used. .PARAMETER ReleaseNotes The text describing the contents of the release. .PARAMETER AssetFilePaths The full paths of the files to include in the release. .PARAMETER Commitish Specifies the commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Unused if the Git tag already exists. Default: the repository's default branch (usually master). .PARAMETER IsDraft True to create a draft (unpublished) release, false to create a published one. Default: false .PARAMETER IsPreRelease True to identify the release as a prerelease. false to identify the release as a full release. Default: false .OUTPUTS A hash table with the following properties is returned: Succeeded = $true if the Release was created successfully and all assets were uploaded to it, $false if some part of the process failed. ReleaseCreationSucceeded = $true if the Release was created successfully (does not include asset uploads), $false if the Release was not created. AllAssetUploadsSucceeded = $true if all assets were uploaded to the Release successfully, $false if one of them failed, $null if there were no assets to upload. ReleaseUrl = The URL of the new Release that was created. ErrorMessage = A message describing what went wrong in the case that Succeeded is $false. .EXAMPLE # Import the module dynamically from the PowerShell Gallery. Use CurrentUser scope to avoid having to run as admin. Import-Module -Name New-GitHubRelease -Scope CurrentUser # Specify the parameters required to create the release. Do it as a hash table for easier readability. $newGitHubReleaseParameters = @{ GitHubUsername = 'deadlydog' GitHubRepositoryName = 'New-GitHubRelease' GitHubAccessToken = 'SomeLongHexidecimalString' ReleaseName = "New-GitHubRelease v1.0.0" TagName = "v1.0.0" ReleaseNotes = "This release contains the following changes: ..." AssetFilePaths = @('C:\MyProject\Installer.exe','C:\MyProject\Documentation.md') IsPreRelease = $false IsDraft = $true # Set to true when testing so we don't publish a real release (visible to everyone) by accident. } # Try to create the Release on GitHub and save the results. $result = New-GitHubRelease @newGitHubReleaseParameters # Provide some feedback to the user based on the results. if ($result.Succeeded -eq $true) { Write-Output "Release published successfully! View it at $($result.ReleaseUrl)" } elseif ($result.ReleaseCreationSucceeded -eq $false) { Write-Error "The release was not created. Error message is: $($result.ErrorMessage)" } elseif ($result.AllAssetUploadsSucceeded -eq $false) { Write-Error "The release was created, but not all of the assets were uploaded to it. View it at $($result.ReleaseUrl). Error message is: $($result.ErrorMessage)" } Attempt to create a new Release on GitHub, and provide feedback to the user indicating if it succeeded or not. .LINK Project home: https://github.com/deadlydog/New-GitHubRelease .NOTES Name: New-GitHubRelease Author: Daniel Schroeder (originally based on the script at https://github.com/majkinetor/au/blob/master/scripts/Github-CreateRelease.ps1) GitHub Release API Documentation: https://developer.github.com/v3/repos/releases/#create-a-release Version: 1.0.2 #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = "The username the repository is under (e.g. deadlydog).")] [string] $GitHubUsername, [Parameter(Mandatory = $true, HelpMessage = "The repository name to create the release in (e.g. Invoke-MsBuild).")] [string] $GitHubRepositoryName, [Parameter(Mandatory = $true, HelpMessage = "The Acess Token to use as credentials for GitHub.")] [string] $GitHubAccessToken, [Parameter(Mandatory = $true, HelpMessage = "The name of the tag to create at the the Commitish.")] [string] $TagName, [Parameter(Mandatory = $false, HelpMessage = "The name of the release. If blank, the TagName will be used.")] [string] $ReleaseName, [Parameter(Mandatory = $false, HelpMessage = "Text describing the contents of the tag.")] [string] $ReleaseNotes, [Parameter(Mandatory = $false, HelpMessage = "The full paths of the files to include in the release.")] [string[]] $AssetFilePaths, [Parameter(Mandatory = $false, HelpMessage = "Specifies the commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Unused if the Git tag already exists. Default: the repository's default branch (usually master).")] [string] $Commitish, [Parameter(Mandatory = $false, HelpMessage = "True to create a draft (unpublished) release, false to create a published one. Default: false")] [bool] $IsDraft = $false, [Parameter(Mandatory = $false, HelpMessage = "True to identify the release as a prerelease. false to identify the release as a full release. Default: false")] [bool] $IsPreRelease = $false #[switch] $GenerateReleaseNotes, #[switch] $MakeLatest ) begin { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls [string] $NewLine = [Environment]::NewLine if ([string]::IsNullOrEmpty($ReleaseName)) { $ReleaseName = $TagName } Test-AllFilePathsAndThrowErrorIfOneIsNotValid $AssetFilePaths } end { } process { $result = @{ } $result.Succeeded = $false $result.ReleaseCreationSucceeded = $false $result.AllAssetUploadsSucceeded = $false $result.ReleaseUrl = $null $result.ErrorMessage = $null [bool] $thereAreNoAssetsToIncludeInTheRelease = ($null -eq $AssetFilePaths) -or ($AssetFilePaths.Count -le 0) if ($thereAreNoAssetsToIncludeInTheRelease) { $result.AllAssetUploadsSucceeded = $null } $authHeader = [ordered] @{ Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($GitHubAccessToken + ":x-oauth-basic")) } $releaseData = [ordered] @{ tag_name = $TagName target_commitish = $Commitish name = $ReleaseName body = $ReleaseNotes draft = $IsDraft prerelease = $IsPreRelease } $createReleaseWebRequestParameters = [ordered] @{ Uri = "https://api.github.com/repos/$GitHubUsername/$GitHubRepositoryName/releases" Method = 'POST' Headers = $authHeader ContentType = 'application/vnd.github+json' Body = (ConvertTo-Json $releaseData -Compress) } try { Write-Verbose "Sending web request to create the new Release..." $createReleaseWebRequestResults = Invoke-RestMethodAndThrowDescriptiveErrorOnFailure -requestParametersHashTable $createReleaseWebRequestParameters } catch { $result.ReleaseCreationSucceeded = $false $result.ErrorMessage = $_.Exception.Message return $result } $result.ReleaseCreationSucceeded = $true $result.ReleaseUrl = $createReleaseWebRequestResults.html_url if ($thereAreNoAssetsToIncludeInTheRelease) { $result.Succeeded = $true return $result } [string] $urlToUploadFilesTo = $createReleaseWebRequestResults.upload_url -replace '{.+}' try { Write-Verbose "Uploading asset files to the new release..." Send-FilesToGitHubRelease -filePathsToUpload $AssetFilePaths -urlToUploadFilesTo $urlToUploadFilesTo -authHeader $authHeader } catch { $result.AllAssetUploadsSucceeded = $false $result.ErrorMessage = $_.Exception.Message return $result } $result.AllAssetUploadsSucceeded = $true $result.Succeeded = $true return $result } } function Set-ProjectVersion { <# .SYNOPSIS Updates version numbers across multiple project files. .DESCRIPTION Updates version numbers in C# projects (.csproj), PowerShell modules (.psd1), and PowerShell build scripts that contain 'Invoke-ModuleBuild'. Can increment version components or set a specific version. .PARAMETER VersionType The type of version increment: Major, Minor, Build, or Revision. .PARAMETER NewVersion Specific version number to set (format: x.x.x or x.x.x.x). .PARAMETER ModuleName Optional module name to filter updates to specific projects/modules. .PARAMETER Path The root path to search for project files. Defaults to current location. .PARAMETER ExcludeFolders Array of folder names to exclude from the search (in addition to default 'obj' and 'bin'). .PARAMETER PassThru Returns the update results when specified. .OUTPUTS PSCustomObject[] When PassThru is specified, returns update results for each modified file. .EXAMPLE Set-ProjectVersion -VersionType Minor Increments the minor version in all project files. .EXAMPLE Set-ProjectVersion -NewVersion "2.1.0" -ModuleName "MyModule" Sets the version to 2.1.0 for the specific module. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter()] [ValidateSet('Major', 'Minor', 'Build', 'Revision')] [string]$VersionType = '', [Parameter()] [ValidatePattern('^\d+\.\d+\.\d+(\.\d+)?$')] [string]$NewVersion = '', [Parameter()] [string]$ModuleName = '', [Parameter()] [string]$Path = (Get-Location).Path, [Parameter()] [string[]]$ExcludeFolders = @(), [switch] $PassThru ) $RepoRoot = $Path $DefaultExcludes = @('obj', 'bin') $AllExcludes = $DefaultExcludes + $ExcludeFolders | Select-Object -Unique $CsprojFiles = Get-ChildItem -Path $RepoRoot -Filter "*.csproj" -Recurse | Where-Object { $file = $_ ($AllExcludes.Count -eq 0 -or -not ($AllExcludes | Where-Object { $_ -and $_.Trim() -ne '' -and $file.FullName -and $file.FullName.ToLower().Contains($_.ToLower()) })) } $PsdFiles = Get-ChildItem -Path $RepoRoot -Filter "*.psd1" -Recurse | Where-Object { $file = $_ ($AllExcludes.Count -eq 0 -or -not ($AllExcludes | Where-Object { $_ -and $_.Trim() -ne '' -and $file.FullName -and $file.FullName.ToLower().Contains($_.ToLower()) })) } $BuildScriptFiles = Get-ChildItem -Path $RepoRoot -Filter "*.ps1" -Recurse | Where-Object { $file = $_ $isExcluded = ($AllExcludes.Count -gt 0 -and ($AllExcludes | Where-Object { $_ -and $_.Trim() -ne '' -and $file.FullName -and $file.FullName.ToLower().Contains($_.ToLower()) })) if ($isExcluded) { return $false } try { $content = Get-Content -Path $file.FullName -Raw -ErrorAction SilentlyContinue return $content -match 'Invoke-ModuleBuild|Build-Module' } catch { return $false } } $targetCsprojFiles = $CsprojFiles if ($ModuleName) { $targetCsprojFiles = $CsprojFiles | Where-Object { $_.BaseName -eq $ModuleName } } $targetPsdFiles = $PsdFiles if ($ModuleName) { $targetPsdFiles = $PsdFiles | Where-Object { $_.BaseName -eq $ModuleName } } $currentVersion = $null foreach ($csProj in $targetCsprojFiles) { $version = Get-CurrentVersionFromCsProj -ProjectFile $csProj.FullName if ($version) { $currentVersion = $version break } } if (-not $currentVersion) { foreach ($psd1 in $targetPsdFiles) { $version = Get-CurrentVersionFromPsd1 -ManifestFile $psd1.FullName if ($version) { $currentVersion = $version break } } } if (-not $currentVersion) { foreach ($buildScript in $BuildScriptFiles) { $version = Get-CurrentVersionFromBuildScript -ScriptFile $buildScript.FullName if ($version) { $currentVersion = $version break } } } if (-not $currentVersion) { Write-Error "Could not determine current version from any project files." return } if (-not [string]::IsNullOrWhiteSpace($NewVersion)) { $newVersion = $NewVersion } else { $newVersion = Update-VersionNumber -Version $currentVersion -Type $VersionType } $CurrentVersions = Get-ProjectVersion -Path $RepoRoot -ExcludeFolders $AllExcludes $CurrentVersionHash = @{} foreach ($C in $CurrentVersions) { $CurrentVersionHash[$C.Source] = $C.Version } $Output = @( foreach ($csProj in $targetCsprojFiles) { Update-VersionInCsProj -ProjectFile $csProj.FullName -Version $newVersion -WhatIf:$WhatIfPreference -CurrentVersionHash $CurrentVersionHash } foreach ($psd1 in $targetPsdFiles) { Update-VersionInPsd1 -ManifestFile $psd1.FullName -Version $newVersion -WhatIf:$WhatIfPreference -CurrentVersionHash $CurrentVersionHash } foreach ($buildScript in $BuildScriptFiles) { Update-VersionInBuildScript -ScriptFile $buildScript.FullName -Version $newVersion -WhatIf:$WhatIfPreference -CurrentVersionHash $CurrentVersionHash } ) if ($PassThru) { $Output } } function Test-BasicModule { [cmdletBinding()] param( [string] $Path, [string] $Type ) if ($Type -contains 'Encoding') { Get-ChildItem -LiteralPath $Path -Recurse -Filter '*.ps1' | Get-FileEncoding } } function Test-ScriptFile { <# .Synopsis Test a PowerShell script for cmdlets .Description This command will analyze a PowerShell script file and display a list of detected commands such as PowerShell cmdlets and functions. Commands will be compared to what is installed locally. It is recommended you run this on a Windows 8.1 client with the latest version of RSAT installed. Unknown commands could also be internally defined functions. If in doubt view the contents of the script file in the PowerShell ISE or a script editor. You can test any .ps1, .psm1 or .txt file. .Parameter Path The path to the PowerShell script file. You can test any .ps1, .psm1 or .txt file. .Example PS C:\> test-scriptfile C:\scripts\Remove-MyVM2.ps1 CommandType Name ModuleName ----------- ---- ---------- Cmdlet Disable-VMEventing Hyper-V Cmdlet ForEach-Object Microsoft.PowerShell.Core Cmdlet Get-VHD Hyper-V Cmdlet Get-VMSnapshot Hyper-V Cmdlet Invoke-Command Microsoft.PowerShell.Core Cmdlet New-PSSession Microsoft.PowerShell.Core Cmdlet Out-Null Microsoft.PowerShell.Core Cmdlet Out-String Microsoft.PowerShell.Utility Cmdlet Remove-Item Microsoft.PowerShell.Management Cmdlet Remove-PSSession Microsoft.PowerShell.Core Cmdlet Remove-VM Hyper-V Cmdlet Remove-VMSnapshot Hyper-V Cmdlet Write-Debug Microsoft.PowerShell.Utility Cmdlet Write-Verbose Microsoft.PowerShell.Utility Cmdlet Write-Warning Microsoft.PowerShell.Utility .EXAMPLE PS C:\> Test-ScriptFile -Path 'C:\Users\przemyslaw.klys\Documents\WindowsPowerShell\Modules\PSWinReportingV2\PSWinReportingV2.psm1' | Sort-Object -Property Source, Name | ft -AutoSize .Notes Original script provided by Jeff Hicks at (https://www.petri.com/powershell-problem-solver-find-script-commands) and https://twitter.com/donnie_taylor/status/1160920407031058432 #> [cmdletbinding()] param( [Parameter(Position = 0, Mandatory = $True, HelpMessage = "Enter the path to a PowerShell script file,", ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] [ValidatePattern( "\.(ps1|psm1|txt)$")] [ValidateScript( { Test-Path $_ })] [string]$Path ) begin { Write-Verbose "Starting $($MyInvocation.Mycommand)" Write-Verbose "Defining AST variables" New-Variable astTokens -Force New-Variable astErr -Force } process { Write-Verbose "Parsing $path" $null = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$astTokens, [ref]$astErr) $h = $astTokens | Group-Object tokenflags -AsHashTable -AsString $commandData = $h.CommandName | Where-Object { $_.text -notmatch "-TargetResource$" } | ForEach-Object { Write-Verbose "Processing $($_.text)" try { $cmd = $_.Text $resolved = $cmd | Get-Command -ErrorAction Stop if ($resolved.CommandType -eq 'Alias') { Write-Verbose "Resolving an alias" Write-Verbose "Detected the Where-Object alias '?'" if ($cmd -eq '?') { Get-Command Where-Object } else { $Resolved = $resolved.ResolvedCommandName | Get-Command [PSCustomobject]@{ CommandType = $resolved.CommandType Name = $resolved.Name ModuleName = $resolved.ModuleName Source = $resolved.Source } } } else { [PSCustomobject]@{ CommandType = $resolved.CommandType Name = $resolved.Name ModuleName = $resolved.ModuleName Source = $resolved.Source } } } catch { Write-Verbose "Command is not recognized" [PSCustomobject]@{ CommandType = "Unknown" Name = $cmd ModuleName = "Unknown" Source = "Unknown" } } } $CommandData } end { Write-Verbose -Message "Ending $($MyInvocation.Mycommand)" } } function Test-ScriptModule { [cmdletbinding()] param( [string] $ModuleName, [ValidateSet('Name', 'CommandType', 'ModuleName', 'Source')] $SortName, [switch] $Unique ) $Module = Get-Module -ListAvailable $ModuleName $Path = Join-Path -Path $Module.ModuleBase -ChildPath $Module.RootModule $Output = Test-ScriptFile -Path $Path if ($Unique) { $Output = $Output | Sort-Object -Property 'Name' -Unique:$Unique } if ($SortName) { $Output | Sort-Object -Property $SortName } else { $Output } } if ($PSVersionTable.PSEdition -eq 'Desktop' -and (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full").Release -lt 379893) { Write-Warning "This module requires .NET Framework 4.5.2 or later."; return } Export-ModuleMember -Function @('Convert-CommandsToList', 'Convert-ProjectEncoding', 'Convert-ProjectLineEnding', 'Get-MissingFunctions', 'Get-PowerShellAssemblyMetadata', 'Get-ProjectConsistency', 'Get-ProjectEncoding', 'Get-ProjectLineEnding', 'Get-ProjectVersion', 'Initialize-PortableModule', 'Initialize-PortableScript', 'Initialize-ProjectManager', 'Invoke-DotNetReleaseBuild', 'Invoke-ModuleBuild', 'New-ConfigurationArtefact', 'New-ConfigurationBuild', 'New-ConfigurationCommand', 'New-ConfigurationDocumentation', 'New-ConfigurationExecute', 'New-ConfigurationFormat', 'New-ConfigurationImportModule', 'New-ConfigurationInformation', 'New-ConfigurationManifest', 'New-ConfigurationModule', 'New-ConfigurationModuleSkip', 'New-ConfigurationPlaceHolder', 'New-ConfigurationPublish', 'New-ConfigurationTest', 'Publish-GitHubReleaseAsset', 'Publish-NugetPackage', 'Register-Certificate', 'Remove-Comments', 'Send-GitHubRelease', 'Set-ProjectVersion', 'Test-BasicModule', 'Test-ScriptFile', 'Test-ScriptModule') -Alias @('Build-Module', 'Invoke-ModuleBuilder', 'New-PrepareModule') # SIG # Begin signature block # MIItqwYJKoZIhvcNAQcCoIItnDCCLZgCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCr9qe6g5tGA48M # FS/jQ7iwNLcixGW1njidqVPdSRxcYaCCJq4wggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG # SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw # aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK # EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm # dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu # d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD # eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1 # XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld # QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS # YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm # M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT # QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx # fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD # VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq # hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4 # XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ # aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg # X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk # apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL # FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy # 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u # KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54 # zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8 # 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8 # aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w # ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT # MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1 # c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG # SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS # g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9 # /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn # HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0 # VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f # sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj # gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0 # QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv # mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T # /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk # 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r # mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E # FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n # P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG # CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu # Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln # aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v # Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV # HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB # AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp # wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl # zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ # cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe # Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j # Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh # IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6 # OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw # N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR # 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2 # VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGsDCCBJigAwIBAgIQ # CK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEV # MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t # MSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAw # MDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT # aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEF # AAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+k # jmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9 # NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9 # URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegY # E2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS # 4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJa # wv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+w # c86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eR # Gv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF2 # 3r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCK # ZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhEC # AwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2 # O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P # MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcB # AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr # BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1 # c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln # aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAH # BgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6 # mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/ # SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzY # gBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9 # kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ # 9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAew # Q3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5Lm # Tl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HA # SIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xr # y7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhR # ILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFu # v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGvDCCBKSgAwIBAgIQC65mvFq6f5WHxvnp # BOMzBDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5 # NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTI0MDkyNjAwMDAwMFoXDTM1MTEy # NTIzNTk1OVowQjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSAwHgYD # VQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQAD # ggIPADCCAgoCggIBAL5qc5/2lSGrljC6W23mWaO16P2RHxjEiDtqmeOlwf0KMCBD # Er4IxHRGd7+L660x5XltSVhhK64zi9CeC9B6lUdXM0s71EOcRe8+CEJp+3R2O8oo # 76EO7o5tLuslxdr9Qq82aKcpA9O//X6QE+AcaU/byaCagLD/GLoUb35SfWHh43rO # H3bpLEx7pZ7avVnpUVmPvkxT8c2a2yC0WMp8hMu60tZR0ChaV76Nhnj37DEYTX9R # eNZ8hIOYe4jl7/r419CvEYVIrH6sN00yx49boUuumF9i2T8UuKGn9966fR5X6kgX # j3o5WHhHVO+NBikDO0mlUh902wS/Eeh8F/UFaRp1z5SnROHwSJ+QQRZ1fisD8UTV # DSupWJNstVkiqLq+ISTdEjJKGjVfIcsgA4l9cbk8Smlzddh4EfvFrpVNnes4c16J # idj5XiPVdsn5n10jxmGpxoMc6iPkoaDhi6JjHd5ibfdp5uzIXp4P0wXkgNs+CO/C # acBqU0R4k+8h6gYldp4FCMgrXdKWfM4N0u25OEAuEa3JyidxW48jwBqIJqImd93N # Rxvd1aepSeNeREXAu2xUDEW8aqzFQDYmr9ZONuc2MhTMizchNULpUEoA6Vva7b1X # CB+1rxvbKmLqfY/M/SdV6mwWTyeVy5Z/JkvMFpnQy5wR14GJcv6dQ4aEKOX5AgMB # AAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUB # Af8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s # BwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0OBBYEFJ9X # LAN3DigVkGalY17uT5IfdqBbMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwz # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1l # U3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUFBzABhhho # dHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6Ly9jYWNl # cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZU # aW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAD2tHh92mVvjOIQS # R9lDkfYR25tOCB3RKE/P09x7gUsmXqt40ouRl3lj+8QioVYq3igpwrPvBmZdrlWB # b0HvqT00nFSXgmUrDKNSQqGTdpjHsPy+LaalTW0qVjvUBhcHzBMutB6HzeledbDC # zFzUy34VarPnvIWrqVogK0qM8gJhh/+qDEAIdO/KkYesLyTVOoJ4eTq7gj9UFAL1 # UruJKlTnCVaM2UeUUW/8z3fvjxhN6hdT98Vr2FYlCS7Mbb4Hv5swO+aAXxWUm3Wp # ByXtgVQxiBlTVYzqfLDbe9PpBKDBfk+rabTFDZXoUke7zPgtd7/fvWTlCs30VAGE # sshJmLbJ6ZbQ/xll/HjO9JbNVekBv2Tgem+mLptR7yIrpaidRJXrI+UzB6vAlk/8 # a1u7cIqV0yef4uaZFORNekUgQHTqddmsPCEIYQP7xGxZBIhdmm4bhYsVA6G2WgNF # YagLDBzpmk9104WQzYuVNsxyoVLObhx3RugaEGru+SojW4dHPoWrUhftNpFC5H7Q # EY7MhKRyrBe7ucykW7eaCuWBsBb4HOKRFVDcrZgdwaSIqMDiCLg4D+TPVgKx2EgE # deoHNHT9l3ZDBD+XgbF+23/zBjeCtxz+dL/9NWR6P2eZRi7zcEO1xwcdcqJsyz/J # ceENc2Sg8h3KeFUCS7tpFk7CrDqkMIIHXzCCBUegAwIBAgIQB8JSdCgUotar/iTq # F+XdLjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT # aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTIzMDQxNjAwMDAwMFoX # DTI2MDcwNjIzNTk1OVowZzELMAkGA1UEBhMCUEwxEjAQBgNVBAcMCU1pa2/FgsOz # dzEhMB8GA1UECgwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMSEwHwYDVQQDDBhQ # cnplbXlzxYJhdyBLxYJ5cyBFVk9URUMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw # ggIKAoICAQCUmgeXMQtIaKaSkKvbAt8GFZJ1ywOH8SwxlTus4McyrWmVOrRBVRQA # 8ApF9FaeobwmkZxvkxQTFLHKm+8knwomEUslca8CqSOI0YwELv5EwTVEh0C/Daeh # vxo6tkmNPF9/SP1KC3c0l1vO+M7vdNVGKQIQrhxq7EG0iezBZOAiukNdGVXRYOLn # 47V3qL5PwG/ou2alJ/vifIDad81qFb+QkUh02Jo24SMjWdKDytdrMXi0235CN4Rr # W+8gjfRJ+fKKjgMImbuceCsi9Iv1a66bUc9anAemObT4mF5U/yQBgAuAo3+jVB8w # iUd87kUQO0zJCF8vq2YrVOz8OJmMX8ggIsEEUZ3CZKD0hVc3dm7cWSAw8/FNzGNP # lAaIxzXX9qeD0EgaCLRkItA3t3eQW+IAXyS/9ZnnpFUoDvQGbK+Q4/bP0ib98XLf # QpxVGRu0cCV0Ng77DIkRF+IyR1PcwVAq+OzVU3vKeo25v/rntiXCmCxiW4oHYO28 # eSQ/eIAcnii+3uKDNZrI15P7VxDrkUIc6FtiSvOhwc3AzY+vEfivUkFKRqwvSSr4 # fCrrkk7z2Qe72Zwlw2EDRVHyy0fUVGO9QMuh6E3RwnJL96ip0alcmhKABGoIqSW0 # 5nXdCUbkXmhPCTT5naQDuZ1UkAXbZPShKjbPwzdXP2b8I9nQ89VSgQIDAQABo4IC # AzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0OBBYE # FHrxaiVZuDJxxEk15bLoMuFI5233MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK # BggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEz # ODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0Rp # Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j # cmwwPgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3 # dy5kaWdpY2VydC5jb20vQ1BTMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcw # AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8v # Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmlu # Z1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEB # CwUAA4ICAQC3EeHXUPhpe31K2DL43Hfh6qkvBHyR1RlD9lVIklcRCR50ZHzoWs6E # BlTFyohvkpclVCuRdQW33tS6vtKPOucpDDv4wsA+6zkJYI8fHouW6Tqa1W47YSrc # 5AOShIcJ9+NpNbKNGih3doSlcio2mUKCX5I/ZrzJBkQpJ0kYha/pUST2CbE3JroJ # f2vQWGUiI+J3LdiPNHmhO1l+zaQkSxv0cVDETMfQGZKKRVESZ6Fg61b0djvQSx51 # 0MdbxtKMjvS3ZtAytqnQHk1ipP+Rg+M5lFHrSkUlnpGa+f3nuQhxDb7N9E8hUVev # xALTrFifg8zhslVRH5/Df/CxlMKXC7op30/AyQsOQxHW1uNx3tG1DMgizpwBasrx # h6wa7iaA+Lp07q1I92eLhrYbtw3xC2vNIGdMdN7nd76yMIjdYnAn7r38wwtaJ3KY # D0QTl77EB8u/5cCs3ShZdDdyg4K7NoJl8iEHrbqtooAHOMLiJpiL2i9Yn8kQMB6/ # Q6RMO3IUPLuycB9o6DNiwQHf6Jt5oW7P09k5NxxBEmksxwNbmZvNQ65Zn3exUAKq # G+x31Egz5IZ4U/jPzRalElEIpS0rgrVg8R8pEOhd95mEzp5WERKFyXhe6nB6bSYH # v8clLAV0iMku308rpfjMiQkqS3LLzfUJ5OHqtKKQNMLxz9z185UCszGCBlMwggZP # AgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEw # PwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2 # IFNIQTM4NCAyMDIxIENBMQIQB8JSdCgUotar/iTqF+XdLjANBglghkgBZQMEAgEF # AKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgor # BgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3 # DQEJBDEiBCDRd9pRrg4t/nK0cCN8Mk1X3qq/fnB7DuFc2cuW1f7BwDANBgkqhkiG # 9w0BAQEFAASCAgBDd/TfK5Kqbr+Eyn1SLC4SfIJbV2zv88jtp/MufTJ7P4Bf4u+Z # 1+4zMLxFxsnA4blJUi4BUnki2WdCfkiEXTcemWND+IlVRqV7hqdqkDdzZe6xRlwe # 9uewe0Lwb6ZiPSOTKyXHtU4mtJLEqiUArsPAzyOhZs3bgQOs8AenTKbQ9iD9TsI/ # EwQsVCjoTZaOfOHncm5RMqFGIDgFgrvduzGat+s352VlUGz3wzYmOaNwrHNqc2O3 # fuiRhKDPzBVbaZnDAivwWmAT1XISLBY+8rF9hK7arUK9ftKDN6TZuvVOy+5B961H # 1BzOFWaGw73N/pBERzJ4X7k2RtDwikv7pwFoCSUbER82CkqediEHsswE5LfJ0b0X # 2OszOwgEpyrexwlRVKCInkGMm/4kR/qCsPJYLfpEGK6bPx8OemH7NDWz8+RSvXxp # 1xZci4VnQG3VMQP2KtU9oM2Jmu0nSH6NXL2k2KSN2rzPS5yQv6is0FuavHYh8mpX # 5V6tPphkL6HDH0uL1DOgpiEGzVMD+PygozFBOICJL7lpUXz0MODRrEsNSLdjvtQu # 66mcRMo5WQ3eOVbTjODqjjNNtZL/fTfiQn6+x5cepusw2mNFWbwf/AISVFZn/zb7 # RkA0b/lwyFkIqsX1t7YqaJDRCx2bKsEgoBU5/ytyG9xPExWeNJebfwLY8aGCAyAw # ggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVTMRcwFQYD # VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH # NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAuuZrxaun+Vh8b56QTj # MwQwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG # CSqGSIb3DQEJBTEPFw0yNTA3MDcwOTA4NDVaMC8GCSqGSIb3DQEJBDEiBCDpNiP+ # goTY/wHdts/0f1nMdMgUnBFLc9bJxTPqrtrUmjANBgkqhkiG9w0BAQEFAASCAgCd # 7LhBdc36Ct2KtmlbKN1mI8oT3uL94c5ijH3E6RjCc1RM91K/ahX/F8GhtU/zBLWG # fz+eY/+uIH+6IsfSGV4NhiDTa8E+D3nvUDj2InGaj965LTAylbrufxRxiunVn1ts # IMsbvO8SGhiGAMIU0kamflYYUHmu9UzmsBDJ0CyloiM5xrgNorezQq0zdERoGkjQ # SefSJiQZxozgmGUayszHityCptzFNdPzLWZ2OI47SvURkrPzXu82rLphjVTqGVXn # x7YCwL4ydYmeAdr9o2E8YM/TVWd0nJnFLjE56tiC9/n6f50MT87vXLMJMgJODr8/ # td7fCu1olxgChG6kq6ktEU2K/jn7tn9518TOYBNdj4ubWtm5feKDQbW+wDNnQaoA # CqL/9a8EjV2EXHD+HfLbkhTbvd7hvq6X4V9yXvliDPF+Vht14CS2s/wFVp0oTXrX # WDgMARP6nqaTM/RiXG8jCacBq6Wwe2/a1axuFt4m052K3yZIGpvVAyj5s56zuFF9 # jAtH7P3/jVsKaKt/aipePiOLhzL9cpV7NhdmJmFmyO1juqFb0le40okIMyFW74N8 # hIkTHgiFw4m/jam5tjCHeF0uVZ7Lv8LGOmtC6kGSaNrpo7LEZr9bKKo/vfxAPV5t # IAK4dmGYqhNuH4B1ZwquBHzfMQfTDi9Wu2bkIzATJw== # SIG # End signature block |