PSPublishModule.psm1
function Add-Directory { [CmdletBinding()] param( [string] $Directory ) $exists = Test-Path -Path $Directory if ($exists -eq $false) { $null = New-Item -Path $Directory -ItemType Directory -Force #$Directory } } function Copy-File { [CmdletBinding()] param ( $Source, $Destination ) if ((Test-Path $Source) -and !(Test-Path $Destination)) { Copy-Item -Path $Source -Destination $Destination } } function Copy-InternalDictionary { [cmdletbinding()] param( [System.Collections.IDictionary] $Dictionary ) # create a deep-clone of an object $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 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 { #region Convert to Hashtables and export $Text = $AllObjects | Write-PowerShellHashtable -Sort:$Sort.IsPresent $Text | Out-File -FilePath $DataFile -Encoding UTF8 #endregion Convert to Hashtables and export } } function Find-EnumsList { [CmdletBinding()] param ( [string] $ProjectPath ) if ($PSEdition -eq 'Core') { $Enums = Get-ChildItem -Path $ProjectPath\Enums\*.ps1 -ErrorAction SilentlyContinue -FollowSymlink } else { $Enums = Get-ChildItem -Path $ProjectPath\Enums\*.ps1 -ErrorAction SilentlyContinue } $EnumsList = @( $Files = Foreach ($import in @($Enums)) { "'Enums\$($import.Name)'" } $Files -join ',' ) return [string] "@($EnumsList)" } 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.Add($Name) $CleanedModules } function Format-Code { [cmdletbinding()] param( [string] $FilePath, $FormatCode ) if ($FormatCode.Enabled) { if ($FormatCode.RemoveComments) { # Write-Verbose "Removing Comments" $Output = Write-TextWithTime -Text "[+] Removing Comments - $FilePath" { Remove-Comments -FilePath $FilePath } } else { $Output = Write-TextWithTime -Text "[+] Reading file content - $FilePath" { Get-Content -LiteralPath $FilePath -Raw } } if ($null -eq $FormatCode.FormatterSettings) { $FormatCode.FormatterSettings = $Script:FormatterSettings } $Data = Write-TextWithTime -Text "[+] Formatting file - $FilePath" { # Write-Verbose "Formatting - $FilePath" try { Invoke-Formatter -ScriptDefinition $Output -Settings $FormatCode.FormatterSettings -Verbose:$false } catch { $ErrorMessage = $_.Exception.Message #Write-Warning "Merge module on file $FilePath failed. Error: $ErrorMessage" Write-Host # This is to add new line, because the first line was opened up. Write-Text "[-] Format-Code - Formatting on file $FilePath failed. Error: $ErrorMessage" -Color Red Exit } } Write-TextWithTime -Text "[+] Saving file - $FilePath" { # Resave $Final = foreach ($O in $Data) { if ($O.Trim() -ne '') { $O.Trim() } } try { $Final | Out-File -LiteralPath $FilePath -NoNewline -Encoding utf8 } catch { $ErrorMessage = $_.Exception.Message #Write-Warning "Merge module on file $FilePath failed. Error: $ErrorMessage" Write-Host # This is to add new line, because the first line was opened up. Write-Text "[-] Format-Code - Resaving file $FilePath failed. Error: $ErrorMessage" -Color Red Exit } } } } function Format-PSD1 { [cmdletbinding()] param( [string] $PSD1FilePath, $FormatCode ) if ($FormatCode.Enabled) { $Output = Get-Content -LiteralPath $PSD1FilePath -Raw if ($FormatCode.RemoveComments) { Write-Verbose "Removing Comments - $PSD1FilePath" # Remove comments $Output = Remove-Comments -ScriptContent $Output } Write-Verbose "Formatting - $PSD1FilePath" if ($null -eq $FormatCode.FormatterSettings) { $FormatCode.FormatterSettings = $Script:FormatterSettings } $Output = Invoke-Formatter -ScriptDefinition $Output -Settings $FormatCode.FormatterSettings #$Output = foreach ($O in $Output) { # if ($O.Trim() -ne '') { # $O.Trim() # } #} $Output | Out-File -LiteralPath $PSD1FilePath -NoNewline } } function Format-UsingNamespace { [CmdletBinding()] param( [string] $FilePath, [string] $FilePathSave, [string] $FilePathUsing ) 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); # Read Lines $UsingNamespaces = [System.Collections.Generic.List[string]]::new() #$AddTypes = [System.Collections.Generic.List[string]]::new() $Content = while (!$ReadFile.EndOfStream) { $Line = $ReadFile.ReadLine() if ($Line -like 'using namespace*') { $UsingNamespaces.Add($Line) #} elseif ($Line -like '*Add-Type*') { #$AddTypes.Add($Line) } else { $Line } } $ReadFile.Close() $null = New-Item -Path $FilePathSave -ItemType file -Force if ($UsingNamespaces) { # Repeat using namespaces $null = New-Item -Path $FilePathUsing -ItemType file -Force $UsingNamespaces = $UsingNamespaces.Trim() | Sort-Object -Unique $UsingNamespaces | Add-Content -LiteralPath $FilePathUsing -Encoding utf8 #$UsingNamespaces | Add-Content -LiteralPath $FilePathUsing -Encoding utf8 #$Content | Add-Content -LiteralPath $FilePathUsing -Encoding utf8 $Content | Add-Content -LiteralPath $FilePathSave -Encoding utf8 return $true } else { $Content | Add-Content -LiteralPath $FilePathSave -Encoding utf8 return $False } } } 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.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 = '' } # So we caught exception, we know the command doesn't exists # so now we check if it's one of the private commands from Approved Modules # this will allow us to integrate it regardless how it's done. 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') { # skipping because otherwise Import-Module fails if part of RequiredModules 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-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) } # Get AST for each FUNCTION $ListOfFuncionsAst = $FileAst.FindAll( { param ($ast) $ast -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $RecurseFunctionNames ) if ($AsHashtable) { # Build list of functions and their aliases as custom object # Name Value # ---- ----- # Add-ADACL {$null} # Get-ADACL {$null} # Get-ADACLOwner {$null} # Get-WinADBitlockerLapsSummary {$null} # Get-WinADDFSHealth {$null} # Get-WinADDiagnostics {$null} # Get-WinADDomain {$null} # Get-WinADDuplicateObject {$null} # Get-WinADForest {$null} # Get-WinADForestObjectsConflict {$null} # Get-WinADForestOptionalFeat... {$null} # Get-WinADForestReplication {$null} # Get-WinADForestRoles {Get-WinADRoles, Get-WinADDomainRoles} $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 } } ) #[PSCustomObject] @{ # Name = $Function.Name # Alias = $AliasTarget #} $OutputList[$Function.Name] = $AliasTarget } $OutputList } else { # This builds a list of functions and aliases together $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 } } } } } <# Measure-Command { $Files = Get-ChildItem -LiteralPath 'C:\Support\GitHub\PSWriteHTML\Public' $Functions = foreach ($_ in $Files) { Get-AliasTarget -Path $_.FullName } } Measure-Command { $Files = Get-ChildItem -LiteralPath 'C:\Support\GitHub\PSWriteHTML\Public' $Functions = foreach ($_ in $Files) { [System.Management.Automation.Language.Parser]::ParseFile($_ , [ref]$null, [ref]$null) } } #> <# $AliasDefinitions = $FileAst.FindAll( { param ($ast) $ast -is [System.Management.Automation.Language.StringConstantExpressionAst] -And $ast.Value -match '(New|Set)-Alias' }, $true) #> #Measure-Command { # Get-AliasTarget -Path 'C:\Support\GitHub\PSSharedGoods\Public\Objects\Format-Stream.ps1' #| Select-Object -ExpandProperty Alias #Get-AliasTarget -path 'C:\Support\GitHub\PSPublishModule\Private\Get-AliasTarget.ps1' # get-aliastarget -path 'C:\Support\GitHub\PSPublishModule\Private\Start-ModuleBuilding.ps1' #} #Get-AliasTarget -Path 'C:\Add-TableContent.ps1' #Get-AliasTarget -Path 'C:\Support\GitHub\PSWriteHTML\Private\Add-TableContent.ps1' #Get-FunctionNames -Path 'C:\Support\GitHub\PSWriteHTML\Private\Add-TableContent.ps1' #Get-FunctionAliases -Path 'C:\Support\GitHub\PSSharedGoods\Public\Objects\Format-Stream.ps1' function Get-FunctionAliasesFromFolder { [cmdletbinding()] param( [string] $FullProjectPath, [string[]] $Folder, [Array] $Files ) $FilesPS1 = foreach ($File in $Files) { if ($file.FullName -like "*\Public\*") { if ($File.Extension -eq '.ps1' -or $File.Extension -eq '*.psm1') { $File } } } [Array] $Content = foreach ($File in $FilesPS1) { '' Get-Content -LiteralPath $File.FullName -Raw -Encoding Default } $Code = $Content -join [System.Environment]::NewLine $AliasesToExport = Get-FunctionAliases -Content $Code -AsHashtable $AliasesToExport } function Get-FunctionNames { [cmdletbinding()] param( [string] $Path, [switch] $Recurse ) if ($Path -ne '' -and (Test-Path -LiteralPath $Path)) { $FilePath = Resolve-Path $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-RecursiveCommands { [CmdletBinding()] param( [Array] $Commands ) $Another = foreach ($Command in $Commands) { if ($Command.ScriptBlock) { Get-ScriptCommands -Code $Command.ScriptBlock -CommandsOnly } } $filter = Get-FilteredScriptCommands -Commands $Another -NotUnknown -NotCmdlet [Array] $ProcessedCommands = foreach ($_ in $Filter) { if ($_.Name -notin $ListCommands.Name) { $ListCommands.Add($_) $_ } } if ($ProcessedCommands.Count -gt 0) { Get-RecursiveCommands -Commands $ProcessedCommands } } function Get-ScriptCommands { [CmdletBinding()] param( [string] $FilePath, [alias('ScriptBlock')][scriptblock] $Code, [switch] $CommandsOnly ) $astTokens = $null $astErr = $null if ($FilePath) { $null = [System.Management.Automation.Language.Parser]::ParseFile($FilePath, [ref]$astTokens, [ref]$astErr) } else { $null = [System.Management.Automation.Language.Parser]::ParseInput($Code, [ref]$astTokens, [ref]$astErr) } $Commands = [System.Collections.Generic.List[Object]]::new() Get-AstTokens -ASTTokens $astTokens -Commands $Commands if ($CommandsOnly) { $Commands.Value | Sort-Object -Unique } else { $Commands } # $astTokens | Group-Object tokenflags -AsHashTable -AsString #$Commands = $astTokens | Where-Object { $_.TokenFlags -eq 'Command' } | Sort-Object -Property Value -Unique } function Get-AstTokens { [cmdletBinding()] param( [System.Management.Automation.Language.Token[]] $ASTTokens, [System.Collections.Generic.List[Object]] $Commands ) foreach ($_ in $astTokens) { if ($_.TokenFlags -eq 'Command' -and $_.Kind -eq 'Generic') { if ($_.Value -notin $Commands) { $Commands.Add($_) } } else { if ($_.NestedTokens) { Get-AstTokens -ASTTokens $_.NestedTokens -Commands $Commands } } } } function Get-ScriptCommandsUseless { [cmdletBinding(DefaultParameterSetName = 'File')] param ( [alias('Path')][Parameter(ParameterSetName = 'File')][string] $FilePath, [alias('ScriptBlock')][scriptblock] $Code, [switch] $CommandsOnly ) begin { $Errors = $null } process { $Errors = $null if ($Code) { $CodeRead = $Code } else { if ($FilePath -eq '') { Write-Text "[-] Something went wrong. FilePath for Get-ScriptCommands was empty. Rerun tool to verify." -Color Red } else { if (Test-Path -LiteralPath $FilePath) { $CodeRead = Get-Content -Path $FilePath -Raw -Encoding Default } } } if ($CodeRead) { #$Types = [System.Collections.Generic.List[string]]::new() $Tokens = [System.Management.Automation.PSParser]::Tokenize($CodeRead, [ref]$Errors) $Commands = foreach ($_ in $Tokens) { if ($_.Type -eq 'Command') { $_ } #else { # $Types.Add($_.Type) #} } if ($CommandsOnly) { $Commands.Content | Sort-Object -Unique } else { $Commands } } } } 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, [Array] $LibrariesStandard, [Array] $LibrariesCore, [Array] $LibrariesDefault, [System.Collections.IDictionary] $FormatCodePSM1, [System.Collections.IDictionary] $FormatCodePSD1, [System.Collections.IDictionary] $Configuration ) $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] 1st stage merging" -Color Blue $PSM1FilePath = "$ModulePathTarget\$ModuleName.psm1" $PSD1FilePath = "$ModulePathTarget\$ModuleName.psd1" if ($PSEdition -eq 'Core') { $ScriptFunctions = Get-ChildItem -Path $ModulePathSource\*.ps1 -ErrorAction SilentlyContinue -Recurse -FollowSymlink } else { $ScriptFunctions = Get-ChildItem -Path $ModulePathSource\*.ps1 -ErrorAction SilentlyContinue -Recurse } if ($Sort -eq 'ASC') { $ScriptFunctions = $ScriptFunctions | Sort-Object -Property Name } elseif ($Sort -eq 'DESC') { $ScriptFunctions = $ScriptFunctions | Sort-Object -Descending -Property Name } foreach ($FilePath in $ScriptFunctions) { $Content = Get-Content -Path $FilePath -Raw if ($Content.Count -gt 0) { # Ensure file has content $Content = $Content.Replace('$PSScriptRoot\', '$PSScriptRoot\') $Content = $Content.Replace('$PSScriptRoot\', '$PSScriptRoot\') # Fixes [IO.Path]::Combine($PSScriptRoot, 'Images') - mostly for PSTeams but should be useful for others $Content = $Content.Replace("`$PSScriptRoot,", "`$PSScriptRoot,") $Content = $Content.Replace("`$PSScriptRoot,", "`$PSScriptRoot,") try { $Content | Out-File -Append -LiteralPath $PSM1FilePath -Encoding utf8 } catch { $ErrorMessage = $_.Exception.Message #Write-Warning "Merge module on file $FilePath failed. Error: $ErrorMessage" Write-Error "Merge-Module - Merge on file $FilePath failed. Error: $ErrorMessage" Exit } } } # Using file is needed if there are 'using namespaces' - this is a workaround provided by seeminglyscience $FilePathUsing = "$ModulePathTarget\$ModuleName.Usings.ps1" $UsingInPlace = Format-UsingNamespace -FilePath $PSM1FilePath -FilePathUsing $FilePathUsing if ($UsingInPlace) { Format-Code -FilePath $FilePathUsing -FormatCode $FormatCodePSM1 $Configuration.UsingInPlace = "$ModuleName.Usings.ps1" } $TimeToExecute.Stop() Write-Text "[+] 1st stage merging [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] 2nd stage 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] $ApprovedModules = $Configuration.Options.Merge.Integrate.ApprovedModules $ModulesThatWillMissBecauseOfIntegrating = [System.Collections.Generic.List[string]]::new() [Array] $DependantRequiredModules = foreach ($_ in $RequiredModules) { [Array] $TemporaryDependant = Find-RequiredModules -Name $_ if ($_ -in $ApprovedModules) { # We basically skip dependant modules and tell the user to use it separatly # This is because if the module PSSharedGoods has requirements like PSWriteColor # and we don't integrate PSWriteColor separatly it would be skipped foreach ($ModulesTemp in $TemporaryDependant) { $ModulesThatWillMissBecauseOfIntegrating.Add($ModulesTemp) } } else { $TemporaryDependant } } $DependantRequiredModules = $DependantRequiredModules | Sort-Object -Unique $TimeToExecute.Stop() Write-Text "[+] 2nd stage required modules [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] 3rd stage missing functions" -Color Blue $MissingFunctions = Get-MissingFunctions -FilePath $PSM1FilePath -SummaryWithCommands -ApprovedModules $ApprovedModules $TimeToExecute.Stop() Write-Text "[+] 3rd stage missing functions [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] 4th stage commands used" -Color Blue #[Array] $CommandsWithoutType = $MissingFunctions.Summary | Where-Object { $_.CommandType -eq '' } | Sort-Object -Unique -Property 'Source' [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 '' } | Sort-Object -Unique -Property 'Source' 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 } } Write-Text "[+] Pre-Verification of Approved Modules" -Color DarkYellow 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 } } foreach ($Module in $ModulesToCheck.Source) { 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 }) #-join ',' 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 }) #-join ',' 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 Green $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module }) #-join ',' foreach ($F in $MyFunctions) { Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color Green } } elseif ($Module -in $RequiredModules) { Write-Text "[+] Module $Module is in required modules." -Color Green $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module }) #-join ',' 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 }) #-join ',' foreach ($F in $MyFunctions) { Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color Magenta } } else { Write-Text "[-] Module $Module is missing in required modules. Potential issue." -Color Red $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module }) #-join ',' foreach ($F in $MyFunctions) { Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color Red } } } if ($CommandsWithoutModule.Count -gt 0) { Write-Text "[-] Some commands couldn't be resolved to functions (private function maybe?). Potential issue." -Color Red foreach ($F in $CommandsWithoutModule) { Write-Text " [>] Command affected $($F.Name) (Command Type: Unknown / IsAlias: $($F.IsAlias))" -Color Red } } foreach ($Module in $ModulesThatWillMissBecauseOfIntegrating) { #Write-Text "[-] Module $Module is missing in required modules due to integration of some approved module. Potential issue." -Color Red } $TimeToExecute.Stop() Write-Text "[+] 4th stage commands used [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue if ($Configuration.Steps.BuildModule.MergeMissing -eq $true) { if (Test-Path -LiteralPath $PSM1FilePath) { $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] 5th stage merge mergable commands" -Color Blue $PSM1Content = Get-Content -LiteralPath $PSM1FilePath -Raw $IntegrateContent = @( $MissingFunctions.Functions $PSM1Content ) $IntegrateContent | Set-Content -LiteralPath $PSM1FilePath -Encoding UTF8 # Overwrite Required Modules $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 #$MissingFunctions.Functions #$MissingFunctions.Summary | Format-Table -AutoSize <# Name Source CommandType Error ScriptBlock ---- ------ ----------- ----- ----------- cmd.exe C:\Windows\system32\cmd.exe Application Import-PowerShellDataFile Microsoft.PowerShell.Utility Function ... New-MarkdownHelp platyPS Function ... Publish-Module PowerShellGet Function ... Update-MarkdownHelpModule platyPS Function ... Find-Module PowerShellGet Function ... Find-Script PowerShellGet Function ... Get-MarkdownMetadata platyPS Function ... Get-PSRepository PowerShellGet Function ... Update-MarkdownHelp platyPS Function ... #> $TimeToExecute.Stop() Write-Text "[+] 5th stage merge mergable commands [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue } } $TimeToExecuteSign = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] 6th finalizing PSM1/PSD1" -Color Blue if ($Configuration.Steps.BuildModule.LibrarySeparateFile) { $LibariesPath = "$ModulePathTarget\$ModuleName.Libraries.ps1" $ScriptsToProcessLibrary = "$ModuleName.Libraries.ps1" } else { $LibariesPath = '' } New-PSMFile -Path $PSM1FilePath ` -FunctionNames $FunctionsToExport ` -FunctionAliaes $AliasesToExport ` -AliasesAndFunctions $AliasesAndFunctions ` -LibrariesStandard $LibrariesStandard ` -LibrariesCore $LibrariesCore ` -LibrariesDefault $LibrariesDefault ` -ModuleName $ModuleName ` -UsingNamespaces:$UsingInPlace ` -LibariesPath $LibariesPath ` -InternalModuleDependencies $Configuration.Information.Manifest.InternalModuleDependencies ` -CommandModuleDependencies $Configuration.Information.Manifest.CommandModuleDependencies Format-Code -FilePath $PSM1FilePath -FormatCode $FormatCodePSM1 New-PersonalManifest -Configuration $Configuration -ManifestPath $PSD1FilePath -AddUsingsToProcess -ScriptsToProcessLibrary $ScriptsToProcessLibrary Format-Code -FilePath $PSD1FilePath -FormatCode $FormatCodePSD1 # cleans up empty directories 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 #-Verbose $TimeToExecuteSign.Stop() Write-Text "[+] 6th finalizing PSM1/PSD1 [Time: $($($TimeToExecuteSign.Elapsed).Tostring())]" -Color Blue } function New-CreateModule { [CmdletBinding()] param ( [string] $ProjectName, [string] $ModulePath, [string] $ProjectPath ) $FullProjectPath = "$projectPath\$projectName" $Folders = 'Private', 'Public', 'Examples', 'Ignore', 'Publish', 'Enums', 'Data' Add-Directory $FullProjectPath foreach ($folder in $Folders) { Add-Directory "$FullProjectPath\$folder" } Copy-File -Source "$PSScriptRoot\Data\Example-Gitignore.txt" -Destination "$FullProjectPath\.gitignore" Copy-File -Source "$PSScriptRoot\Data\Example-LicenseMIT.txt" -Destination "$FullProjectPath\License" Copy-File -Source "$PSScriptRoot\Data\Example-ModuleStarter.txt" -Destination "$FullProjectPath\$ProjectName.psm1" } function New-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 ) BEGIN { # Turn on Strict Mode to help catch syntax-related errors. # This must come after a script's/function's param section. # Forces a function to be the first non-comment code to appear in a PowerShell Script/Module. Set-StrictMode -Version Latest Set-SecurityProtocolForThread [string] $NewLine = [Environment]::NewLine if ([string]::IsNullOrEmpty($ReleaseName)) { $ReleaseName = $TagName } # Ensure that all of the given asset file paths to upload are valid. Test-AllFilePathsAndThrowErrorIfOneIsNotValid $AssetFilePaths } END { } PROCESS { # Create the hash table to return, with default values. $result = @{ } $result.Succeeded = $false $result.ReleaseCreationSucceeded = $false $result.AllAssetUploadsSucceeded = $false $result.ReleaseUrl = $null $result.ErrorMessage = $null [bool] $thereAreNoAssetsToIncludeInTheRelease = ($AssetFilePaths -eq $null) -or ($AssetFilePaths.Count -le 0) if ($thereAreNoAssetsToIncludeInTheRelease) { $result.AllAssetUploadsSucceeded = $null } $authHeader = @{ Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($GitHubAccessToken + ":x-oauth-basic")) } $releaseData = @{ tag_name = $TagName target_commitish = $Commitish name = $ReleaseName body = $ReleaseNotes draft = $IsDraft prerelease = $IsPreRelease } $createReleaseWebRequestParameters = @{ Uri = "https://api.github.com/repos/$GitHubUsername/$GitHubRepositoryName/releases" Method = 'POST' Headers = $authHeader ContentType = 'application/json' Body = (ConvertTo-Json $releaseData -Compress) } try { Write-Verbose "Sending web request to create the new Release..." $createReleaseWebRequestResults = Invoke-RestMethodAndThrowDescriptiveErrorOnFailure $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 } # Upload Url has template parameters on the end (e.g. ".../assets{?name,label}"), so remove them. [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 Send-FilesToGitHubRelease([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 = @{ # Append the name of the file to the upload url. 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 Test-AllFilePathsAndThrowErrorIfOneIsNotValid([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 Invoke-RestMethodAndThrowDescriptiveErrorOnFailure($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 Get-RestMethodExceptionDetailsOrNull([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 Convert-HashTableToNicelyFormattedString($hashTable) { [string] $nicelyFormattedString = $hashTable.Keys | ForEach-Object ` { $key = $_ $value = $hashTable.$key " $key = $value$NewLine" } return $nicelyFormattedString } function Set-SecurityProtocolForThread { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls } function New-PersonalManifest { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [string] $ManifestPath, [switch] $AddScriptsToProcess, [switch] $AddUsingsToProcess, [string] $ScriptsToProcessLibrary ) $TemporaryManifest = @{ } $Manifest = $Configuration.Information.Manifest $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.Contains('RequiredModules')) { foreach ($SubModule in $Manifest.RequiredModules) { if ($SubModule.ModuleVersion -eq 'Latest') { [Array] $AvailableModule = Get-Module -ListAvailable $SubModule.ModuleName -Verbose:$false if ($AvailableModule) { $SubModule.ModuleVersion = $AvailableModule[0].Version } else { Write-Text -Text "[-] Module $($SubModule.ModuleName) is not available, but defined as required with last version. Terminating." -Color Red Exit } } } } if ($Configuration.Options.Style.PSD1 -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 Exit } Write-TextWithTime -Text "[+] Converting $($ManifestPath) UTF8 without BOM" { (Get-Content -Path $ManifestPath -Raw) | Out-FileUtf8NoBom $ManifestPath } } else { # if ($Configuration.Steps.PublishModule.Prerelease -ne '' -or $TemporaryManifest.ExternalModuleDependencies -or $Configuration.Options.Style.PSD1 -ne 'Native') { #[string] $PSD1Path = $Configuration.Information.Manifest.Path #$FilePathPSD1 = Get-Item -Path $Configuration.Information.Manifest.Path #$Data = Import-PowerShellDataFile -Path $Configuration.Information.Manifest.Path if ($Data.ScriptsToProcess.Count -eq 0) { #$Data.Remove('ScriptsToProcess') } if ($Data.CmdletsToExport.Count -eq 0) { # $Data.Remove('CmdletsToExport') } $Data = $Manifest $Data.PrivateData = @{ PSData = [ordered]@{} } if ($Data.Path) { $Data.Remove('Path') } if ($Data.Tags) { $Data.PrivateData.PSData.Tags = $Data.Tags $Data.Remove('Tags') } if ($Data.LicenseUri) { $Data.PrivateData.PSData.LicenseUri = $Data.LicenseUri $Data.Remove('LicenseUri') } if ($Data.ProjectUri) { $Data.PrivateData.PSData.ProjectUri = $Data.ProjectUri $Data.Remove('ProjectUri') } if ($Data.IconUri) { $Data.PrivateData.PSData.IconUri = $Data.IconUri $Data.Remove('IconUri') } if ($Data.ReleaseNotes) { $Data.PrivateData.PSData.ReleaseNotes = $Data.ReleaseNotes $Data.Remove('ReleaseNotes') } $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) } } $ValidateEntriesPrivateData = @('Tags', 'LicenseUri', 'ProjectURI', 'IconUri', 'ReleaseNotes', 'Prerelease', 'RequireLicenseAcceptance', 'ExternalModuleDependencies') 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) { # Add External Module Dependencies $Data.PrivateData.PSData.ExternalModuleDependencies = $TemporaryManifest.ExternalModuleDependencies # Make sure Required Modules contains ExternalModuleDependencies $Data.RequiredModules = @( foreach ($Module in $Manifest.RequiredModules) { $Module } foreach ($Module in $TemporaryManifest.ExternalModuleDependencies) { $Module } ) } if (-not $Data.RequiredModules) { $Data.Remove('RequiredModules') } $Data | Export-PSData -DataFile $ManifestPath -Sort } } #[-] [Error: The 'C:\Users\przemyslaw.klys\Documents\WindowsPowerShell\Modules\PSPublishModule\PSPublishModule.psd1' #module cannot be imported because its manifest contains one or more members that are not valid. The valid manifest members are #('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'). Remove the members that are not valid ('Path', 'IconUri'), then try to import the module again.] function New-PrepareManifest { [CmdletBinding()] param( $ProjectName, $modulePath, $projectPath, $functionToExport, $projectUrl ) Set-Location "$projectPath\$ProjectName" $manifest = @{ Path = ".\$ProjectName.psd1" RootModule = "$ProjectName.psm1" Author = 'Przemyslaw Klys' CompanyName = 'Evotec' Copyright = 'Evotec (c) 2011-2019. 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-PSMFile { [cmdletbinding()] param( [string] $Path, [string[]] $FunctionNames, [string[]] $FunctionAliaes, [System.Collections.IDictionary] $AliasesAndFunctions, [Array] $LibrariesStandard, [Array] $LibrariesCore, [Array] $LibrariesDefault, [string] $ModuleName, [switch] $UsingNamespaces, [string] $LibariesPath, [Array] $InternalModuleDependencies, [System.Collections.IDictionary] $CommandModuleDependencies ) try { # $Content = Get-Content -LiteralPath $Path -Raw $LibraryContent = @( if ($LibrariesStandard.Count -gt 0) { foreach ($File in $LibrariesStandard) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { $Output = 'Add-Type -Path $PSScriptRoot\' + $File $Output } } } elseif ($LibrariesCore.Count -gt 0 -and $LibrariesDefault.Count -gt 0) { 'if ($PSEdition -eq ''Core'') {' foreach ($File in $LibrariesCore) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { $Output = 'Add-Type -Path $PSScriptRoot\' + $File $Output } } '} else {' foreach ($File in $LibrariesDefault) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { $Output = 'Add-Type -Path $PSScriptRoot\' + $File $Output } } '}' } elseif ($LibrariesCore.Count -gt 0) { foreach ($File in $LibrariesCore) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { $Output = 'Add-Type -Path $PSScriptRoot\' + $File $Output } } } elseif ($LibrariesDefault.Count -gt 0) { foreach ($File in $LibrariesDefault) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { $Output = 'Add-Type -Path $PSScriptRoot\' + $File $Output } } } ) 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 = @() } "" | Add-Content -Path $Path if ($LibariesPath) { $LibraryContent | Add-Content -Path $LibariesPath } else { $LibraryContent | Add-Content -Path $Path } # This allows for loading modules in PSM1 file directly if ($InternalModuleDependencies) { $ModulesText = "'$($InternalModuleDependencies -join "','")'" @" `$ModulesOptional = $ModulesText foreach (`$Module in `$ModulesOptional) { Import-Module -Name `$Module -ErrorAction SilentlyContinue } "@ | Add-Content -Path $Path } # This allows to export functions only if module loading works correctly if ($CommandModuleDependencies) { @( "`$ModuleFunctions = @{" foreach ($Module in $CommandModuleDependencies.Keys) { #$Commands = "'$($CommandModuleDependencies[$Module] -join "','")'" "$Module = @{" foreach ($Command in $($CommandModuleDependencies[$Module])) { #foreach ($Function in $AliasesAndFunctions.Keys) { $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-ModuleMember -Function @(`$FunctionsToLoad) -Alias @(`$AliasesToLoad) "@ ) | Add-Content -Path $Path } else { # this loads functions/aliases as designed "" | Add-Content -Path $Path "Export-ModuleMember -Function @($Functions) -Alias @($Aliases)" | Add-Content -Path $Path } } catch { $ErrorMessage = $_.Exception.Message #Write-Warning "New-PSM1File from $ModuleName failed build. Error: $ErrorMessage" Write-Error "New-PSM1File from $ModuleName failed build. Error: $ErrorMessage" Exit } } <# .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. #> function Out-FileUtf8NoBom { [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string] $LiteralPath, [switch] $Append, [switch] $NoClobber, [AllowNull()] [int] $Width, [Parameter(ValueFromPipeline)] $InputObject ) # Make sure that the .NET framework sees the same working dir. as PS # and resolve the input path to a full path. [System.IO.Directory]::SetCurrentDirectory($PWD) # Caveat: .NET Core doesn't support [Environment]::CurrentDirectory $LiteralPath = [IO.Path]::GetFullPath($LiteralPath) # If -NoClobber was specified, throw an exception if the target file already # exists. if ($NoClobber -and (Test-Path $LiteralPath)) { Throw [IO.IOException] "The file '$LiteralPath' already exists." } # Create a StreamWriter object. # Note that we take advantage of the fact that the StreamWriter class by default: # - uses UTF-8 encoding # - without a BOM. $sw = New-Object IO.StreamWriter $LiteralPath, $Append $htOutStringArgs = @{} if ($Width) { $htOutStringArgs += @{ Width = $Width } } # Note: By not using begin / process / end blocks, we're effectively running # in the end block, which means that all pipeline input has already # been collected in automatic variable $Input. # We must use this approach, because using | Out-String individually # in each iteration of a process block would format each input object # with an indvidual header. try { $Input | Out-String -Stream @htOutStringArgs | ForEach-Object { $sw.WriteLine($_) } } finally { $sw.Dispose() } } function Remove-Directory { [CmdletBinding()] param ( [string] $Directory ) if ($Directory) { $exists = Test-Path -Path $Directory if ($exists) { #Write-Color 'Removing directory ', $dir -Color White, Yellow try { Remove-Item -Path $Directory -Confirm:$false -Recurse -Force -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message Write-Text "[-] Can't delete folder $Directory. Fix error before contiuing: $ErrorMessage" -Color Red Exit } } else { #Write-Color 'Removing directory ', $dir, ' skipped.' -Color White, Yellow, Red } } } 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 .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 ) if ($Path -and (Test-Path -LiteralPath $Path)) { $Items = Get-ChildItem -LiteralPath $Path -Recurse foreach ($Item in $Items) { try { $Item.Delete() } catch { Write-Warning "Remove-ItemAlternative - Couldn't delete $($Item.FullName), error: $($_.Exception.Message)" } } if (-not $SkipFolder) { $Item = Get-Item -LiteralPath $Path try { $Item.Delete($true) } catch { Write-Warning "Remove-ItemAlternative - Couldn't delete $($Item.FullName), error: $($_.Exception.Message)" } } } else { Write-Warning "Remove-ItemAlternative - Path $Path doesn't exists. Skipping. " } } $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 Set-LinkedFiles { [CmdletBinding()] param( [string[]] $LinkFiles, [string] $FullModulePath, [string] $FullProjectPath, [switch] $Delete ) foreach ($file in $LinkFiles) { [string] $Path = "$FullModulePath\$file" [string] $Path2 = "$FullProjectPath\$file" if ($Delete) { if (Test-ReparsePoint -path $Path) { # Write-Color 'Removing symlink first ', $path -Color White, Yellow #Write-Verbose "Removing symlink first $path" Remove-Item $Path -Confirm:$false } } #Write-Verbose "Creating symlink from $path2 (source) to $path (target)" #Write-Color 'Creating symlink from ', $path2, ' (source) to ', $path, ' (target)' -Color White, Yellow, White, Yellow, White $null = cmd /c mklink $path $path2 } } function Start-ModuleBuilding { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration ) $DestinationPaths = @{ } 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 { # Means missing from config - send to both $DestinationPaths.Desktop = [IO.path]::Combine($Configuration.Information.DirectoryModules, $Configuration.Information.ModuleName) $DestinationPaths.Core = [IO.path]::Combine($Configuration.Information.DirectoryModulesCore, $Configuration.Information.ModuleName) } $Versioning = Step-Version -Module $Configuration.Information.ModuleName -ExpectedVersion $Configuration.Information.Manifest.ModuleVersion -Advanced $Configuration.Information.Manifest.ModuleVersion = $Versioning.Version [string] $Random = Get-Random 10000000000 [string] $FullModuleTemporaryPath = [IO.path]::GetTempPath() + '' + $Configuration.Information.ModuleName [string] $FullTemporaryPath = [IO.path]::GetTempPath() + '' + $Configuration.Information.ModuleName + "_TEMP_$Random" [string] $FullProjectPath = [IO.Path]::Combine($Configuration.Information.DirectoryProjects, $Configuration.Information.ModuleName) [string] $ProjectName = $Configuration.Information.ModuleName Write-Text '----------------------------------------------------' Write-Text "[i] Project Name: $ProjectName" -Color Yellow Write-Text "[i] PSGallery Version: $($Versioning.PSGalleryVersion)" -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 } # check if project exists if (-not (Test-Path -Path $FullProjectPath)) { Write-Text "[-] Project path doesn't exists $FullProjectPath. Terminating" -Color Red return } if ($Configuration.Steps.BuildModule.Enable -eq $true) { if ($Configuration.Steps.BuildModule.DeleteBefore -eq $true) { Remove-Directory $($DestinationPaths.Desktop) Remove-Directory $($DestinationPaths.Core) } $CurrentLocation = (Get-Location).Path Set-Location -Path $FullProjectPath Remove-Directory $FullModuleTemporaryPath Remove-Directory $FullTemporaryPath Add-Directory $FullModuleTemporaryPath Add-Directory $FullTemporaryPath # $DirectoryTypes = 'Public', 'Private', 'Lib', 'Bin', 'Enums', 'Images', 'Templates', 'Resources' $LinkDirectories = @() $LinkPrivatePublicFiles = @() # Fix required fields: $Configuration.Information.Manifest.RootModule = "$($ProjectName).psm1" # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. $Configuration.Information.Manifest.CmdletsToExport = @() # Variables to export from this module #$Configuration.Information.Manifest.VariablesToExport = @() if ($Configuration.Information.Exclude) { $Exclude = $Configuration.Information.Exclude } else { $Exclude = '.*', 'Ignore', 'Examples', 'package.json', 'Publish', 'Docs' } if ($Configuration.Information.IncludeRoot) { $IncludeFilesRoot = $Configuration.Information.IncludeRoot } else { $IncludeFilesRoot = '*.psm1', '*.psd1', 'License*' } if ($Configuration.Information.IncludePS1) { $DirectoriesWithPS1 = $Configuration.Information.IncludePS1 } else { $DirectoriesWithPS1 = 'Private', 'Public', 'Enums' } if ($Configuration.Information.IncludeAll) { $DirectoriesWithAll = $Configuration.Information.IncludeAll } else { $DirectoriesWithAll = 'Images\', 'Resources\', 'Templates\', 'Bin\', 'Lib\', 'Data\' } if ($Configuration.Steps.BuildModule.Enable -eq $true) { $PreparingFilesTime = Write-Text "[+] Preparing files and folders" -Start 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 "$FullProjectPath\*" -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 "$FullProjectPath\*" -Include $IncludeFilesRoot -File } $LinkDirectories = @( foreach ($directory in $Directories) { $RelativeDirectoryPath = (Resolve-Path -LiteralPath $directory.FullName -Relative).Replace('.\', '') $RelativeDirectoryPath = "$RelativeDirectoryPath\" $RelativeDirectoryPath } ) $AllFiles = foreach ($File in $Files) { $RelativeFilePath = (Resolve-Path -LiteralPath $File.FullName -Relative).Replace('.\', '') $RelativeFilePath } $RootFiles = foreach ($File in $FilesRoot) { $RelativeFilePath = (Resolve-Path -LiteralPath $File.FullName -Relative).Replace('.\', '') $RelativeFilePath } # Link only files in Root Directory $LinkFilesRoot = @( foreach ($File in $RootFiles | Sort-Object -Unique) { switch -Wildcard ($file) { '*.psd1' { $File } '*.psm1' { $File } 'License*' { $File } } } ) # Link only files from subfolers $LinkPrivatePublicFiles = @( foreach ($file in $AllFiles | Sort-Object -Unique) { switch -Wildcard ($file) { '*.ps1' { foreach ($dir in $DirectoriesWithPS1) { if ($file -like "$dir*") { $file } } # Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory $DirectoriesWithPS1 continue } '*.*' { #Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory $DirectoriesWithAll foreach ($dir in $DirectoriesWithAll) { if ($file -like "$dir*") { $file } } continue } } } ) $LinkPrivatePublicFiles = $LinkPrivatePublicFiles | Select-Object -Unique Write-Text -End -Time $PreparingFilesTime $AliasesAndFunctions = Write-TextWithTime -Text '[+] Preparing function and aliases names' { Get-FunctionAliasesFromFolder -FullProjectPath $FullProjectPath -Files $Files #-Folder $Configuration.Information.AliasesToExport } if ($AliasesAndFunctions -is [System.Collections.IDictionary]) { $Configuration.Information.Manifest.FunctionsToExport = $AliasesAndFunctions.Keys | Where-Object { $_ } if (-not $Configuration.Information.Manifest.FunctionsToExport) { $Configuration.Information.Manifest.FunctionsToExport = @() } # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. $Configuration.Information.Manifest.AliasesToExport = $AliasesAndFunctions.Values | ForEach-Object { $_ } | Where-Object { $_ } if (-not $Configuration.Information.Manifest.AliasesToExport) { $Configuration.Information.Manifest.AliasesToExport = @() } } else { # this is not used, as we're using Hashtable above, but maybe if we change mind we can go back $Configuration.Information.Manifest.FunctionsToExport = $AliasesAndFunctions.Name | Where-Object { $_ } if (-not $Configuration.Information.Manifest.FunctionsToExport) { $Configuration.Information.Manifest.FunctionsToExport = @() } $Configuration.Information.Manifest.AliasesToExport = $AliasesAndFunctions.Alias | ForEach-Object { $_ } | Where-Object { $_ } if (-not $Configuration.Information.Manifest.AliasesToExport) { $Configuration.Information.Manifest.AliasesToExport = @() } } Write-Text "[i] Checking for duplicates in funcions and aliases" -Color Yellow $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 } } foreach ($Alias in $DiffrenceAliases.InputObject) { Write-Text "[-] Alias $Alias is used multiple times. Fix it!" -Color Red $FoundDuplicateAliases = $true } if ($FoundDuplicateAliases) { Exit } } if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.ScriptsToProcess)) { $StartsWithEnums = "$($Configuration.Information.ScriptsToProcess)\" $FilesEnums = @( $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithEnums) } ) if ($FilesEnums.Count -gt 0) { Write-TextWithTime -Text "[+] ScriptsToProcess export $FilesEnums" $Configuration.Information.Manifest.ScriptsToProcess = $FilesEnums } #} } $PSD1FilePath = "$FullProjectPath\$ProjectName.psd1" # Copy Configuration $SaveConfiguration = Copy-InternalDictionary -Dictionary $Configuration New-PersonalManifest -Configuration $Configuration -ManifestPath $PSD1FilePath -AddScriptsToProcess # Restore configuration, as some PersonalManifest plays with those $Configuration = $SaveConfiguration Format-Code -FilePath $PSD1FilePath -FormatCode $Configuration.Options.Standard.FormatCodePSD1 if ($Configuration.Steps.BuildModule.RefreshPSD1Only) { Exit } } if ($Configuration.Steps.BuildModule.Enable -and $Configuration.Steps.BuildModule.Merge) { foreach ($Directory in $LinkDirectories) { $Dir = "$FullTemporaryPath\$Directory" Add-Directory $Dir } # Workaround to link files that are not ps1/psd1 [Array] $CompareWorkaround = foreach ($_ in $DirectoriesWithPS1) { -join ($_, '\') } $LinkDirectoriesWithSupportFiles = $LinkDirectories | Where-Object { $_ -notin $CompareWorkaround } #$LinkDirectoriesWithSupportFiles = $LinkDirectories | Where-Object { $_ -ne 'Public\' -and $_ -ne 'Private\' } foreach ($Directory in $LinkDirectoriesWithSupportFiles) { $Dir = "$FullModuleTemporaryPath\$Directory" Add-Directory $Dir } $LinkingFilesTime = Write-Text "[+] Linking files from root and sub directories" -Start Set-LinkedFiles -LinkFiles $LinkFilesRoot -FullModulePath $FullTemporaryPath -FullProjectPath $FullProjectPath Set-LinkedFiles -LinkFiles $LinkPrivatePublicFiles -FullModulePath $FullTemporaryPath -FullProjectPath $FullProjectPath Write-Text -End -Time $LinkingFilesTime # Workaround to link files that are not ps1/psd1 $FilesToLink = $LinkPrivatePublicFiles | Where-Object { $_ -notlike '*.ps1' -and $_ -notlike '*.psd1' } Set-LinkedFiles -LinkFiles $FilesToLink -FullModulePath $FullModuleTemporaryPath -FullProjectPath $FullProjectPath if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesCore)) { $StartsWithCore = "$($Configuration.Information.LibrariesCore)\" $FilesLibrariesCore = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithCore) } } if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesDefault)) { $StartsWithDefault = "$($Configuration.Information.LibrariesDefault)\" $FilesLibrariesDefault = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithDefault) } } if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesStandard)) { $StartsWithStandard = "$($Configuration.Information.LibrariesDefault)\" $FilesLibrariesStandard = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithStandard) } } Merge-Module -ModuleName $ProjectName ` -ModulePathSource $FullTemporaryPath ` -ModulePathTarget $FullModuleTemporaryPath ` -Sort $Configuration.Options.Merge.Sort ` -FunctionsToExport $Configuration.Information.Manifest.FunctionsToExport ` -AliasesToExport $Configuration.Information.Manifest.AliasesToExport ` -AliasesAndFunctions $AliasesAndFunctions ` -LibrariesStandard $FilesLibrariesStandard ` -LibrariesCore $FilesLibrariesCore ` -LibrariesDefault $FilesLibrariesDefault ` -FormatCodePSM1 $Configuration.Options.Merge.FormatCodePSM1 ` -FormatCodePSD1 $Configuration.Options.Merge.FormatCodePSD1 ` -Configuration $Configuration if ($Configuration.Steps.BuildModule.CreateFileCatalog) { # Something is wrong here for folders other than root, need investigation $TimeToExecuteSign = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] 7th stage 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 "[+] 7th stage creating file catalog [Time: $($($TimeToExecuteSign.Elapsed).Tostring())]" -Color Blue } if ($Configuration.Steps.BuildModule.SignMerged) { $TimeToExecuteSign = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] 8th stage signing files" -Color Blue $SignedFiles = Register-Certificate -LocalStore CurrentUser -Path $FullModuleTemporaryPath -Include @('*.ps1', '*.psd1', '*.psm1', '*.dll', '*.cat') -TimeStampServer 'http://timestamp.digicert.com' foreach ($File in $SignedFiles) { Write-Text " [>] File $($File.Path) with status: $($File.StatusMessage)" -Color Yellow } $TimeToExecuteSign.Stop() Write-Text "[+] 8th stage signing files [Time: $($($TimeToExecuteSign.Elapsed).Tostring())]" -Color Blue } } if ($Configuration.Steps.BuildModule.Enable -and (-not $Configuration.Steps.BuildModule.Merge)) { foreach ($Directory in $LinkDirectories) { $Dir = "$FullModuleTemporaryPath\$Directory" Add-Directory $Dir } $LinkingFilesTime = Write-Text "[+] Linking files from root and sub directories" -Start Set-LinkedFiles -LinkFiles $LinkFilesRoot -FullModulePath $FullModuleTemporaryPath -FullProjectPath $FullProjectPath Set-LinkedFiles -LinkFiles $LinkPrivatePublicFiles -FullModulePath $FullModuleTemporaryPath -FullProjectPath $FullProjectPath Write-Text -End -Time $LinkingFilesTime } # Revers Path to current locatikon Set-Location -Path $CurrentLocation if ($Configuration.Steps.BuildModule.Enable) { if ($DestinationPaths.Desktop) { Write-TextWithTime -Text "[+] Copy module to PowerShell 5 destination: $($DestinationPaths.Desktop)" { Remove-Directory -Directory $DestinationPaths.Desktop Add-Directory -Directory $DestinationPaths.Desktop Get-ChildItem -LiteralPath $FullModuleTemporaryPath | Copy-Item -Destination $DestinationPaths.Desktop -Recurse # cleans up empty directories 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 #-Verbose } } if ($DestinationPaths.Core) { Write-TextWithTime -Text "[+] Copy module to PowerShell 6/7 destination: $($DestinationPaths.Core)" { Remove-Directory -Directory $DestinationPaths.Core Add-Directory -Directory $DestinationPaths.Core Get-ChildItem -LiteralPath $FullModuleTemporaryPath | Copy-Item -Destination $DestinationPaths.Core -Recurse # cleans up empty directories 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 #-Verbose } } } } if ($Configuration.Steps.BuildModule.Enable) { if ($Configuration.Steps.BuildModule.Releases -or $Configuration.Steps.BuildModule.ReleasesUnpacked) { $TagName = "v$($Configuration.Information.Manifest.ModuleVersion)" $FileName = -join ("$TagName", '.zip') $FolderPathReleases = [System.IO.Path]::Combine($FullProjectPath, 'Releases') $ZipPath = [System.IO.Path]::Combine($FullProjectPath, 'Releases', $FileName) Write-TextWithTime -Text "[+] Compressing final merged release $ZipPath" { $null = New-Item -ItemType Directory -Path $FolderPathReleases -Force if ($DestinationPaths.Desktop) { $CompressPath = [System.IO.Path]::Combine($DestinationPaths.Desktop, '*') Compress-Archive -Path $CompressPath -DestinationPath $ZipPath -Force } if ($DestinationPaths.Core -and -not $DestinationPaths.Desktop) { $CompressPath = [System.IO.Path]::Combine($DestinationPaths.Core, '*') Compress-Archive -Path $CompressPath -DestinationPath $ZipPath -Force } } if ($Configuration.Steps.BuildModule.ReleasesUnpacked) { $FolderPathReleasesUnpacked = [System.IO.Path]::Combine($FullProjectPath, 'ReleasesUnpacked', $TagName ) Write-TextWithTime -Text "[+] Copying final merged release to $FolderPathReleasesUnpacked" { try { if (Test-Path -Path $FolderPathReleasesUnpacked) { Remove-Item -LiteralPath $FolderPathReleasesUnpacked -Force -Confirm:$false -Recurse } $null = New-Item -ItemType Directory -Path $FolderPathReleasesUnpacked -Force if ($DestinationPaths.Desktop) { Copy-Item -LiteralPath $DestinationPaths.Desktop -Recurse -Destination $FolderPathReleasesUnpacked -Force } if ($DestinationPaths.Core -and -not $DestinationPaths.Desktop) { Copy-Item -LiteralPath $DestinationPaths.Core -Recurse -Destination $FolderPathReleasesUnpacked -Force } } catch { $ErrorMessage = $_.Exception.Message #Write-Warning "Merge module on file $FilePath failed. Error: $ErrorMessage" Write-Host # This is to add new line, because the first line was opened up. Write-Text "[-] Format-Code - Copying final merged release to $FolderPathReleasesUnpacked failed. Error: $ErrorMessage" -Color Red Exit } } } } } # Import Modules Section, useful to check before publishing if ($Configuration.Steps.ImportModules) { $TemporaryVerbosePreference = $VerbosePreference $VerbosePreference = $false if ($Configuration.Steps.ImportModules.RequiredModules) { Write-TextWithTime -Text '[+] Importing modules - REQUIRED' { foreach ($Module in $Configuration.Information.Manifest.RequiredModules) { Import-Module -Name $Module -Force -ErrorAction Stop -Verbose:$false } } } if ($Configuration.Steps.ImportModules.Self) { Write-TextWithTime -Text '[+] Importing module - SELF' { Import-Module -Name $ProjectName -Force -ErrorAction Stop -Verbose:$false } } $VerbosePreference = $TemporaryVerbosePreference } if ($Configuration.Steps.PublishModule.Enabled) { Write-TextWithTime -Text "[+] Publishing Module to PowerShellGallery" { try { if ($Configuration.Options.PowerShellGallery.FromFile) { $ApiKey = Get-Content -Path $Configuration.Options.PowerShellGallery.ApiKey #New-PublishModule -ProjectName $Configuration.Information.ModuleName -ApiKey $ApiKey -RequireForce $Configuration.Steps.PublishModule.RequireForce Publish-Module -Name $Configuration.Information.ModuleName -Repository PSGallery -NuGetApiKey $ApiKey -Force:$Configuration.Steps.PublishModule.RequireForce -Verbose -ErrorAction Stop } else { #New-PublishModule -ProjectName $Configuration.Information.ModuleName -ApiKey $Configuration.Options.PowerShellGallery.ApiKey -RequireForce $Configuration.Steps.PublishModule.RequireForce Publish-Module -Name $Configuration.Information.ModuleName -Repository PSGallery -NuGetApiKey $Configuration.Options.PowerShellGallery.ApiKey -Force:$Configuration.Steps.PublishModule.RequireForce -Verbose -ErrorAction Stop } } catch { $ErrorMessage = $_.Exception.Message Write-Host # This is to add new line, because the first line was opened up. Write-Text "[-] Publishing Module - failed. Error: $ErrorMessage" -Color Red Exit } } } if ($Configuration.Steps.PublishModule.GitHub) { $TagName = "v$($Configuration.Information.Manifest.ModuleVersion)" $FileName = -join ("$TagName", '.zip') $FolderPathReleases = [System.IO.Path]::Combine($FullProjectPath, 'Releases') $ZipPath = [System.IO.Path]::Combine($FullProjectPath, 'Releases', $FileName) if ($Configuration.Options.GitHub.FromFile) { $GitHubAccessToken = Get-Content -LiteralPath $Configuration.Options.GitHub.ApiKey } else { $GitHubAccessToken = $Configuration.Options.GitHub.ApiKey } if ($GitHubAccessToken) { if ($Configuration.Options.GitHub.RepositoryName) { $GitHubRepositoryName = $Configuration.Options.GitHub.RepositoryName } else { $GitHubRepositoryName = $ProjectName } if (Test-Path -LiteralPath $ZipPath) { if ($Configuration.Steps.PublishModule.Prerelease -ne '') { $IsPreRelease = $true } else { $IsPreRelease = $false } $StatusGithub = New-GitHubRelease -GitHubUsername $Configuration.Options.GitHub.UserName -GitHubRepositoryName $GitHubRepositoryName -GitHubAccessToken $GitHubAccessToken -TagName $TagName -AssetFilePaths $ZipPath -IsPreRelease $IsPreRelease 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 } } } } if ($Configuration.Steps.BuildDocumentation) { # Support for old way of building documentation -> converts to new one if ($Configuration.Steps.BuildDocumentation -is [bool]) { $TemporaryBuildDocumentation = $Configuration.Steps.BuildDocumentation $Configuration.Steps.BuildDocumentation = @{ Enable = $TemporaryBuildDocumentation } } # Real documentation process 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 } [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 everything / Refresh Count #$null = Remove-Item -Path $DocumentationPath -Force -Recurse <# $ItemToDelete = Get-ChildItem -Path $DocumentationPath foreach ($Item in $ItemToDelete) { $Item.Delete() } $ItemToDelete = Get-Item -Path $DocumentationPath $ItemToDelete.Delete($true) #> Remove-ItemAlternative -Path $DocumentationPath -SkipFolder #$null = New-Item -Path "$FullProjectPath\Docs" -ItemType Directory -Force [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 #Start-Sleep -Seconds 1 # this is temporary workaround - due to diff output on update if ($Configuration.Options.Documentation.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 } } } } # Cleanup temp directory Write-Text "[+] Cleaning up directories created in TEMP directory" -Color Yellow Remove-Directory $FullModuleTemporaryPath Remove-Directory $FullTemporaryPath } function Step-Version { [cmdletBinding()] param( [string] $Module, [Parameter(Mandatory)][string] $ExpectedVersion, [switch] $Advanced ) $Version = $null $VersionCheck = [version]::TryParse($ExpectedVersion, [ref] $Version) if ($VersionCheck) { # Don't do anything, return what user wanted to get anyways @{ Version = $ExpectedVersion PSGalleryVersion = 'Not aquired, no auto versioning.' } } else { if ($Module) { try { $ModuleGallery = Find-Module -Name $Module -ErrorAction Stop -Verbose:$false -WarningAction SilentlyContinue $CurrentVersion = [version] $ModuleGallery.Version } catch { #throw "Couldn't find module $Module to asses version information. Terminating." $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) { $VersionToUpgrade = '' } else { $VersionToUpgrade = $CurrentVersion.$StepType } if ($VersionToUpgrade -eq '') { $ExpectedVersion = 1 } else { $ExpectedVersion = $CurrentVersion.$StepType + 1 } $PreparedVersion.$StepType = $ExpectedVersion $Numbers = foreach ($Key in $PreparedVersion.Keys) { if ($PreparedVersion[$Key]) { $PreparedVersion[$Key] } } $ProposedVersion = $Numbers -join '.' $FinalVersion = $null $VersionCheck = [version]::TryParse($ProposedVersion, [ref] $FinalVersion) if ($VersionCheck) { if ($Advanced) { [ordered] @{ Version = $ProposedVersion PSGalleryVersion = $CurrentVersion } } else { $ProposedVersion } } else { throw "Couldn't properly verify version is version. Terminating." } } } <# 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' #> function Test-ReparsePoint { [CmdletBinding()] param ( [string]$path ) $file = Get-Item $path -Force -ea SilentlyContinue return [bool]($file.Attributes -band [IO.FileAttributes]::ReparsePoint) } function Write-PowerShellHashtable { [cmdletbinding()] <# .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 scriptBlock Determines if a string or a scriptblock is returned .Example # Corrects the presentation of a PowerShell hashtable @{Foo='Bar';Baz='Bing';Boo=@{Bam='Blang'}} | Write-PowerShellHashtable .Outputs [string] .Outputs [ScriptBlock] .Link https://github.com/StartAutomating/Pipeworks about_hash_tables #> [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]) { #region Indent $scriptString = "" $indent = $depth * 4 $scriptString += "@{ " #endregion Indent #region Include $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 # Write-Verbose "$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) { #} elseif ($value -and $value.GetType -and ($value.GetType().IsArray -or $value -is [Collections.IList])) { $value = foreach ($v in $value) { if ($v -is [System.Collections.IDictionary]) { Write-PowerShellHashtable $v } elseif ($v -is [Object] -and $v -isnot [string]) { Write-PowerShellHashtable $v } 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 } $value = $value -join "," } elseif ($value -is [System.Collections.IDictionary]) { $value = "$(Write-PowerShellHashtable $value)" } 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 } else { $value = $valueString } } $scriptString += "$value " } $scriptString += " " * ($depth - 1) * 4 $scriptString += "}" if ($AsScriptBlock) { [ScriptBlock]::Create($scriptString) } else { $scriptString } #endregion Include } } } function Write-Text { [CmdletBinding()] param( [string] $Text, [System.ConsoleColor] $Color = [System.ConsoleColor]::Cyan, [System.ConsoleColor] $ColorTime = [System.ConsoleColor]::Green, [switch] $Start, [switch] $End, [System.Diagnostics.Stopwatch] $Time ) 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, [string] $Text, [switch] $Continue, [System.ConsoleColor] $Color = [System.ConsoleColor]::Cyan, [System.ConsoleColor] $ColorTime = [System.ConsoleColor]::Green, [System.ConsoleColor] $ColorError = [System.ConsoleColor]::Red ) Write-Host "$Text" -NoNewline -ForegroundColor $Color $Time = [System.Diagnostics.Stopwatch]::StartNew() if ($null -ne $Content) { try { & $Content } catch { $ErrorMessage = $_.Exception.Message } } $TimeToExecute = $Time.Elapsed.ToString() if ($ErrorMessage) { Write-Host " [Time: $TimeToExecute]" -ForegroundColor $ColorError Write-Host "[-] [Error: $ErrorMessage]" -ForegroundColor $ColorError $Time.Stop() Exit } else { Write-Host " [Time: $TimeToExecute]" -ForegroundColor $ColorTime } if (-not $Continue) { $Time.Stop() } } 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-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 -CommandsOnly } elseif ($Code) { $CommandsUsedInCode = Get-ScriptCommands -CommandsOnly -Code $Code } else { return } if ($IgnoreFunctions.Count -gt 0) { $Result = foreach ($_ in $CommandsUsedInCode) { if ($IgnoreFunctions -notcontains $_) { $_ } } } else { $Result = $CommandsUsedInCode } #$FilteredCommands = Get-FilteredScriptCommands -Commands $Result -NotUnknown -NotCmdlet -Functions $Functions -NotApplication -FilePath $FilePath [Array] $FilteredCommands = Get-FilteredScriptCommands -Commands $Result -Functions $Functions -FilePath $FilePath -ApprovedModules $ApprovedModules foreach ($_ in $FilteredCommands) { $ListCommands.Add($_) } # Ensures even one object is array [Array] $FilteredCommandsName = foreach ($Name in $FilteredCommands.Name) { $Name } # this gets commands along their ScriptBlock # $FilteredCommands = Get-RecursiveCommands -Commands $FilteredCommands [Array] $FunctionsOutput = foreach ($_ in $ListCommands) { if ($_.ScriptBlock) { if ($ApprovedModules.Count -gt 0 -and $_.Source -in $ApprovedModules) { "function $($_.Name) { $($_.ScriptBlock) }" } elseif ($ApprovedModules.Count -eq 0) { #"function $($_.Name) { $($_.ScriptBlock) }" } } } 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 Initialize-PortableScript { [cmdletBinding()] param( [string] $FilePath, [string] $OutputPath, [Array] $ApprovedModules ) $Output = Get-MissingFunctions -FilePath $FilePath -SummaryWithCommands -ApprovedModules $ApprovedModules $Script = Get-Content -LiteralPath $FilePath $FinalScript = @( $Output.Functions $Script ) $FinalScript | Set-Content -LiteralPath $OutputPath $Output } function New-PrepareModule { [CmdletBinding(DefaultParameterSetName = 'Existing')] param ( [Parameter(ParameterSetName = 'New')][string] $Path, [Parameter(ParameterSetName = 'New')][string] $ProjectName, [Parameter(ParameterSetName = 'Existing')][System.Collections.IDictionary] $Configuration ) Write-Host "[i] Module Building Initializing..." -ForegroundColor Yellow $GlobalTime = [System.Diagnostics.Stopwatch]::StartNew() if ($Configuration) { if (-not $Configuration.Information.DirectoryModulesCore) { #$Configuration.Information.DirectoryModulesCore = "$Env:USERPROFILE\Documents\PowerShell\Modules" $Configuration.Information.DirectoryModulesCore = "$([Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments))\PowerShell\Modules" } if (-not $Configuration.Information.DirectoryModules) { #$Configuration.Information.DirectoryModules = "$Env:USERPROFILE\Documents\WindowsPowerShell\Modules" $Configuration.Information.DirectoryModules = "$([Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments))\WindowsPowerShell\Modules" } if ($Configuration.Steps.BuildModule.Enable -or $Configuration.Steps.BuildModule.EnableDesktop -or $Configuration.Steps.BuildModule.EnableCore -or $Configuration.Steps.BuildDocumentation -eq $true) { Start-ModuleBuilding -Configuration $Configuration } } if ($Path -and $ProjectName) { if (-not (Test-Path -Path $Path)) { Write-Text "[-] Path $Path doesn't exists. This shouldn't be the case." -Color Red } else { $FullProjectPath = [io.path]::Combine($Path, $ProjectName) $Folders = 'Private', 'Public', 'Examples', 'Ignore', 'Publish', 'Enums', 'Data' Add-Directory -Directory $FullProjectPath foreach ($folder in $Folders) { $SubFolder = [io.path]::Combine($FullProjectPath, $Folder) Add-Directory -Directory $SubFolder } Copy-File -Source "$PSScriptRoot\Data\Example-Gitignore.txt" -Destination "$FullProjectPath\.gitignore" Copy-File -Source "$PSScriptRoot\Data\Example-LicenseMIT.txt" -Destination "$FullProjectPath\License" Copy-File -Source "$PSScriptRoot\Data\Example-ModuleStarter.txt" -Destination "$FullProjectPath\$ProjectName.psm1" } } $Execute = "$($GlobalTime.Elapsed.Days) days, $($GlobalTime.Elapsed.Hours) hours, $($GlobalTime.Elapsed.Minutes) minutes, $($GlobalTime.Elapsed.Seconds) seconds, $($GlobalTime.Elapsed.Milliseconds) milliseconds" Write-Host "[i] Module Building " -NoNewline -ForegroundColor Yellow Write-Host "[Time Total: $Execute]" -ForegroundColor Green } function Register-Certificate { [cmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, ParameterSetName = 'PFX')][string] $CertificatePFX, [Parameter(Mandatory, ParameterSetName = 'Store')][ValidateSet('LocalMachine', 'CurrentUser')][string] $LocalStore, [Parameter(ParameterSetName = 'Store')][string] $Thumbprint, [Parameter(Mandatory)][string] $Path, [string] $TimeStampServer = 'http://timestamp.digicert.com', [ValidateSet('All', 'NonRoot', 'Signer')] [string] $IncludeChain = 'All', [string[]] $Include = @('*.ps1', '*.psd1', '*.psm1', '*.dll', '*.cat') ) if ($PSBoundParameters.Keys -contains 'LocalStore') { $cert = Get-ChildItem -Path "Cert:\$LocalStore\My" -CodeSigningCert if ($Cert.Count -eq 0) { Write-Warning "Register-Certificate - No certificates found in store." } elseif ($Cert.Count -eq 1) { $Certificate = $Cert } else { if ($Thumbprint) { $Certificate = $Cert | Where-Object { $_.Thumbprint -eq $Thumbprint } if (-not $Certificate) { Write-Warning "Register-Certificate - No certificates found by that thumbprint" return } } else { $CodeError = "Get-ChildItem -Path Cert:\$LocalStore\My -CodeSigningCert" Write-Warning "Register-Certificate - More than one certificate found in store. Provide Thumbprint for expected certificate" Write-Warning "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 "Register-Certificate - No certificates found for PFX" return } } } if ($Certificate -and $Path) { if (Test-Path -LiteralPath $Path) { 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 } } } function Remove-Comments { # We are not restricting scriptblock type as Tokenize() can take several types [CmdletBinding()] Param ( [string] $FilePath, [parameter( ValueFromPipeline = $True )] $Scriptblock, [string] $ScriptContent ) if ($PSBoundParameters['FilePath']) { $ScriptBlockString = [IO.File]::ReadAllText((Resolve-Path $FilePath)) $ScriptBlock = [ScriptBlock]::Create($ScriptBlockString) } elseif ($PSBoundParameters['ScriptContent']) { $ScriptBlock = [ScriptBlock]::Create($ScriptContent) } else { # Convert the scriptblock to a string so that it can be referenced with array notation #$ScriptBlockString = $ScriptBlock.ToString() } # Convert input to a single string if needed $OldScript = $ScriptBlock -join [environment]::NewLine # If no work to do # We're done If ( -not $OldScript.Trim( " `n`r`t" ) ) { return } # Use the PowerShell tokenizer to break the script into identified tokens $Tokens = [System.Management.Automation.PSParser]::Tokenize( $OldScript, [ref]$Null ) # Define useful, allowed comments $AllowedComments = @( 'requires' '.SYNOPSIS' '.DESCRIPTION' '.PARAMETER' '.EXAMPLE' '.INPUTS' '.OUTPUTS' '.NOTES' '.LINK' '.COMPONENT' '.ROLE' '.FUNCTIONALITY' '.FORWARDHELPCATEGORY' '.REMOTEHELPRUNSPACE' '.EXTERNALHELP' ) # Strip out the Comments, but not useful comments # (Bug: This will break comment-based help that uses leading # instead of multiline <#, # because only the headings will be left behind.) $Tokens = $Tokens.ForEach{ If ( $_.Type -ne 'Comment' ) { $_ } Else { $CommentText = $_.Content.Substring( $_.Content.IndexOf( '#' ) + 1 ) $FirstInnerToken = [System.Management.Automation.PSParser]::Tokenize( $CommentText, [ref]$Null ) | Where-Object { $_.Type -ne 'NewLine' } | Select-Object -First 1 If ( $FirstInnerToken.Content -in $AllowedComments ) { $_ } } } # Initialize script string #$NewScriptText = '' $SkipNext = $False $ScriptProcessing = @( # If there are at least 2 tokens to process... If ( $Tokens.Count -gt 1 ) { # For each token (except the last one)... ForEach ( $i in ( 0..($Tokens.Count - 2) ) ) { # If token is not a line continuation and not a repeated new line or semicolon... If (-not $SkipNext -and $Tokens[$i ].Type -ne 'LineContinuation' -and ( $Tokens[$i ].Type -notin ( 'NewLine', 'StatementSeparator' ) -or $Tokens[$i + 1].Type -notin ( 'NewLine', 'StatementSeparator', 'GroupEnd' ) ) ) { # Add Token to new script # For string and variable, reference old script to include $ and quotes If ( $Tokens[$i].Type -in ( 'String', 'Variable' ) ) { $OldScript.Substring( $Tokens[$i].Start, $Tokens[$i].Length ) } Else { $Tokens[$i].Content } # If the token does not never require a trailing space # And the next token does not never require a leading space # And this token and the next are on the same line # And this token and the next had white space between them in the original... If ($Tokens[$i ].Type -notin ( 'NewLine', 'GroupStart', 'StatementSeparator' ) -and $Tokens[$i + 1].Type -notin ( 'NewLine', 'GroupEnd', 'StatementSeparator' ) -and $Tokens[$i].EndLine -eq $Tokens[$i + 1].StartLine -and $Tokens[$i + 1].StartColumn - $Tokens[$i].EndColumn -gt 0 ) { # Add a space to new script ' ' } # If the next token is a new line or semicolon following # an open parenthesis or curly brace, skip it $SkipNext = $Tokens[$i].Type -eq 'GroupStart' -and $Tokens[$i + 1].Type -in ( 'NewLine', 'StatementSeparator' ) } # Else (Token is a line continuation or a repeated new line or semicolon)... Else { # [Do not include it in the new script] # If the next token is a new line or semicolon following # an open parenthesis or curly brace, skip it $SkipNext = $SkipNext -and $Tokens[$i + 1].Type -in ( 'NewLine', 'StatementSeparator' ) } } } # If there is a last token to process... If ( $Tokens ) { # Add last token to new script # For string and variable, reference old script to include $ and quotes If ( $Tokens[$i].Type -in ( 'String', 'Variable' ) ) { $OldScript.Substring( $Tokens[-1].Start, $Tokens[-1].Length ) } Else { $Tokens[-1].Content } } ) [string] $NewScriptText = $ScriptProcessing -join '' # Trim any leading new lines from the new script $NewScriptText = $NewScriptText.TrimStart( "`n`r;" ) #return [scriptblock]::Create( $NewScriptText ) # Return the new script as the same type as the input If ( $Scriptblock.Count -eq 1 ) { If ( $Scriptblock[0] -is [scriptblock] ) { # Return single scriptblock return [scriptblock]::Create( $NewScriptText ) } Else { # Return single string return $NewScriptText } } Else { # Return array of strings return $NewScriptText.Split( "`n`r", [System.StringSplitOptions]::RemoveEmptyEntries ) } } 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) #group tokens and turn into a hashtable $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" #manually handle "?" because Get-Command and Get-Alias won't. Write-Verbose "Detected the Where-Object alias '?'" if ($cmd -eq '?') { Get-Command Where-Object } else { # Since we're dealing with alias we need to recheck $Resolved = $resolved.ResolvedCommandName | Get-Command [PSCustomobject]@{ CommandType = $resolved.CommandType Name = $resolved.Name ModuleName = $resolved.ModuleName Source = $resolved.Source } } } else { #$resolved [PSCustomobject]@{ CommandType = $resolved.CommandType Name = $resolved.Name ModuleName = $resolved.ModuleName Source = $resolved.Source } } } Catch { Write-Verbose "Command is not recognized" #create a custom object for unknown commands [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 } } Export-ModuleMember -Function @('Get-GitLog', 'Get-MissingFunctions', 'Initialize-PortableScript', 'New-PrepareModule', 'Register-Certificate', 'Remove-Comments', 'Test-ScriptFile', 'Test-ScriptModule') -Alias @() # SIG # Begin signature block # MIIgQAYJKoZIhvcNAQcCoIIgMTCCIC0CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU+PvZfiq7xy1tKjvZUpEafJ9X # mTqgghtvMIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0B # AQUFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD # VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk # IElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQsw # CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu # ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg # Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg # +XESpa7cJpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lT # XDGEKvYPmDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5 # a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g # 0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1 # roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf # GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0G # A1UdDgQWBBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLL # gjEtUYunpyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3 # cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmr # EthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+ # fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5Q # Z7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu # 838fYxAe+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw # 8jCCBTAwggQYoAMCAQICEAQJGBtf1btmdVNDtW+VUAgwDQYJKoZIhvcNAQELBQAw # ZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBS # b290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowcjELMAkGA1UE # BhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2lj # ZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUg # U2lnbmluZyBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPjTsxx/ # DhGvZ3cH0wsxSRnP0PtFmbE620T1f+Wondsy13Hqdp0FLreP+pJDwKX5idQ3Gde2 # qvCchqXYJawOeSg6funRZ9PG+yknx9N7I5TkkSOWkHeC+aGEI2YSVDNQdLEoJrsk # acLCUvIUZ4qJRdQtoaPpiCwgla4cSocI3wz14k1gGL6qxLKucDFmM3E+rHCiq85/ # 6XzLkqHlOzEcz+ryCuRXu0q16XTmK/5sy350OTYNkO/ktU6kqepqCquE86xnTrXE # 94zRICUj6whkPlKWwfIPEvTFjg/BougsUfdzvL2FsWKDc0GCB+Q4i2pzINAPZHM8 # np+mM6n9Gd8lk9ECAwEAAaOCAc0wggHJMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYD # VR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHkGCCsGAQUFBwEBBG0w # azAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUF # BzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVk # SURSb290Q0EuY3J0MIGBBgNVHR8EejB4MDqgOKA2hjRodHRwOi8vY3JsNC5kaWdp # Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMDqgOKA2hjRodHRw # Oi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3Js # ME8GA1UdIARIMEYwOAYKYIZIAYb9bAACBDAqMCgGCCsGAQUFBwIBFhxodHRwczov # L3d3dy5kaWdpY2VydC5jb20vQ1BTMAoGCGCGSAGG/WwDMB0GA1UdDgQWBBRaxLl7 # KgqjpepxA8Bg+S32ZXUOWDAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823I # DzANBgkqhkiG9w0BAQsFAAOCAQEAPuwNWiSz8yLRFcgsfCUpdqgdXRwtOhrE7zBh # 134LYP3DPQ/Er4v97yrfIFU3sOH20ZJ1D1G0bqWOWuJeJIFOEKTuP3GOYw4TS63X # X0R58zYUBor3nEZOXP+QsRsHDpEV+7qvtVHCjSSuJMbHJyqhKSgaOnEoAjwukaPA # JRHinBRHoXpoaK+bp1wgXNlxsQyPu6j4xRJon89Ay0BEpRPw5mQMJQhCMrI2iiQC # /i9yfhzXSUWW6Fkd6fp0ZGuy62ZD2rOwjNXpDd32ASDOmTFjPQgaGLOBm0/GkxAG # /AeB+ova+YJJ92JuoVP6EpQYhS6SkepobEQysmah5xikmmRR7zCCBT0wggQloAMC # AQICEATV3B9I6snYUgC6zZqbKqcwDQYJKoZIhvcNAQELBQAwcjELMAkGA1UEBhMC # VVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0 # LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2ln # bmluZyBDQTAeFw0yMDA2MjYwMDAwMDBaFw0yMzA3MDcxMjAwMDBaMHoxCzAJBgNV # BAYTAlBMMRIwEAYDVQQIDAnFmmzEhXNraWUxETAPBgNVBAcTCEthdG93aWNlMSEw # HwYDVQQKDBhQcnplbXlzxYJhdyBLxYJ5cyBFVk9URUMxITAfBgNVBAMMGFByemVt # eXPFgmF3IEvFgnlzIEVWT1RFQzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC # ggEBAL+ygd4sga4ZC1G2xXvasYSijwWKgwapZ69wLaWaZZIlY6YvXTGQnIUnk+Tg # 7EoT7mQiMSaeSPOrn/Im6N74tkvRfQJXxY1cnt3U8//U5grhh/CULdd6M3/Z4h3n # MCq7LQ1YVaa4MYub9F8WOdXO84DANoNVG/t7YotL4vzqZil3S9pHjaidp3kOXGJc # vxrCPAkRFBKvUmYo23QPFa0Rd0qA3bFhn97WWczup1p90y2CkOf28OVOOObv1fNE # EqMpLMx0Yr04/h+LPAAYn6K4YtIu+m3gOhGuNc3B+MybgKePAeFIY4EQzbqvCMy1 # iuHZb6q6ggRyqrJ6xegZga7/gV0CAwEAAaOCAcUwggHBMB8GA1UdIwQYMBaAFFrE # uXsqCqOl6nEDwGD5LfZldQ5YMB0GA1UdDgQWBBQYsTUn6BxQICZOCZA0CxS0TZSU # ZjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwdwYDVR0fBHAw # bjA1oDOgMYYvaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1j # cy1nMS5jcmwwNaAzoDGGL2h0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFz # c3VyZWQtY3MtZzEuY3JsMEwGA1UdIARFMEMwNwYJYIZIAYb9bAMBMCowKAYIKwYB # BQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQQBMIGE # BggrBgEFBQcBAQR4MHYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0 # LmNvbTBOBggrBgEFBQcwAoZCaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0Rp # Z2lDZXJ0U0hBMkFzc3VyZWRJRENvZGVTaWduaW5nQ0EuY3J0MAwGA1UdEwEB/wQC # MAAwDQYJKoZIhvcNAQELBQADggEBAJq9bM+JbCwEYuMBtXoNAfH1SRaMLXnLe0py # VK6el0Z1BtPxiNcF4iyHqMNVD4iOrgzLEVzx1Bf/sYycPEnyG8Gr2tnl7u1KGSjY # enX4LIXCZqNEDQCeTyMstNv931421ERByDa0wrz1Wz5lepMeCqXeyiawqOxA9fB/ # 106liR12vL2tzGC62yXrV6WhD6W+s5PpfEY/chuIwVUYXp1AVFI9wi2lg0gaTgP/ # rMfP1wfVvaKWH2Bm/tU5mwpIVIO0wd4A+qOhEia3vn3J2Zz1QDxEprLcLE9e3Gmd # G5+8xEypTR23NavhJvZMgY2kEXBEKEEDaXs0LoPbn6hMcepR2A4wggZqMIIFUqAD # AgECAhADAZoCOv9YsWvW1ermF/BmMA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNVBAYT # AlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2Vy # dC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IEFzc3VyZWQgSUQgQ0EtMTAeFw0xNDEw # MjIwMDAwMDBaFw0yNDEwMjIwMDAwMDBaMEcxCzAJBgNVBAYTAlVTMREwDwYDVQQK # EwhEaWdpQ2VydDElMCMGA1UEAxMcRGlnaUNlcnQgVGltZXN0YW1wIFJlc3BvbmRl # cjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKNkXfx8s+CCNeDg9sYq # 5kl1O8xu4FOpnx9kWeZ8a39rjJ1V+JLjntVaY1sCSVDZg85vZu7dy4XpX6X51Id0 # iEQ7Gcnl9ZGfxhQ5rCTqqEsskYnMXij0ZLZQt/USs3OWCmejvmGfrvP9Enh1DqZb # FP1FI46GRFV9GIYFjFWHeUhG98oOjafeTl/iqLYtWQJhiGFyGGi5uHzu5uc0LzF3 # gTAfuzYBje8n4/ea8EwxZI3j6/oZh6h+z+yMDDZbesF6uHjHyQYuRhDIjegEYNu8 # c3T6Ttj+qkDxss5wRoPp2kChWTrZFQlXmVYwk/PJYczQCMxr7GJCkawCwO+k8IkR # j3cCAwEAAaOCAzUwggMxMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYG # A1UdJQEB/wQMMAoGCCsGAQUFBwMIMIIBvwYDVR0gBIIBtjCCAbIwggGhBglghkgB # hv1sBwEwggGSMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20v # Q1BTMIIBZAYIKwYBBQUHAgIwggFWHoIBUgBBAG4AeQAgAHUAcwBlACAAbwBmACAA # dABoAGkAcwAgAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAGMAbwBuAHMAdABpAHQA # dQB0AGUAcwAgAGEAYwBjAGUAcAB0AGEAbgBjAGUAIABvAGYAIAB0AGgAZQAgAEQA # aQBnAGkAQwBlAHIAdAAgAEMAUAAvAEMAUABTACAAYQBuAGQAIAB0AGgAZQAgAFIA # ZQBsAHkAaQBuAGcAIABQAGEAcgB0AHkAIABBAGcAcgBlAGUAbQBlAG4AdAAgAHcA # aABpAGMAaAAgAGwAaQBtAGkAdAAgAGwAaQBhAGIAaQBsAGkAdAB5ACAAYQBuAGQA # IABhAHIAZQAgAGkAbgBjAG8AcgBwAG8AcgBhAHQAZQBkACAAaABlAHIAZQBpAG4A # IABiAHkAIAByAGUAZgBlAHIAZQBuAGMAZQAuMAsGCWCGSAGG/WwDFTAfBgNVHSME # GDAWgBQVABIrE5iymQftHt+ivlcNK2cCzTAdBgNVHQ4EFgQUYVpNJLZJMp1KKnka # g0v0HonByn0wfQYDVR0fBHYwdDA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQu # Y29tL0RpZ2lDZXJ0QXNzdXJlZElEQ0EtMS5jcmwwOKA2oDSGMmh0dHA6Ly9jcmw0 # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRENBLTEuY3JsMHcGCCsGAQUF # BwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEG # CCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRB # c3N1cmVkSURDQS0xLmNydDANBgkqhkiG9w0BAQUFAAOCAQEAnSV+GzNNsiaBXJuG # ziMgD4CH5Yj//7HUaiwx7ToXGXEXzakbvFoWOQCd42yE5FpA+94GAYw3+puxnSR+ # /iCkV61bt5qwYCbqaVchXTQvH3Gwg5QZBWs1kBCge5fH9j/n4hFBpr1i2fAnPTgd # KG86Ugnw7HBi02JLsOBzppLA044x2C/jbRcTBu7kA7YUq/OPQ6dxnSHdFMoVXZJB # 2vkPgdGZdA0mxA5/G7X1oPHGdwYoFenYk+VVFvC7Cqsc21xIJ2bIo4sKHOWV2q7E # LlmgYd3a822iYemKC23sEhi991VUQAOSK2vCUcIKSK+w1G7g9BQKOhvjjz3Kr2qN # e9zYRDCCBs0wggW1oAMCAQICEAb9+QOWA63qAArrPye7uhswDQYJKoZIhvcNAQEF # BQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE # CxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJ # RCBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTIxMTExMDAwMDAwMFowYjELMAkG # A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp # Z2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgQXNzdXJlZCBJRCBDQS0xMIIB # IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6IItmfnKwkKVpYBzQHDSnlZU # XKnE0kEGj8kz/E1FkVyBn+0snPgWWd+etSQVwpi5tHdJ3InECtqvy15r7a2wcTHr # zzpADEZNk+yLejYIA6sMNP4YSYL+x8cxSIB8HqIPkg5QycaH6zY/2DDD/6b3+6LN # b3Mj/qxWBZDwMiEWicZwiPkFl32jx0PdAug7Pe2xQaPtP77blUjE7h6z8rwMK5nQ # xl0SQoHhg26Ccz8mSxSQrllmCsSNvtLOBq6thG9IhJtPQLnxTPKvmPv2zkBdXPao # 8S+v7Iki8msYZbHBc63X8djPHgp0XEK4aH631XcKJ1Z8D2KkPzIUYJX9BwSiCQID # AQABo4IDejCCA3YwDgYDVR0PAQH/BAQDAgGGMDsGA1UdJQQ0MDIGCCsGAQUFBwMB # BggrBgEFBQcDAgYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCDCCAdIGA1Ud # IASCAckwggHFMIIBtAYKYIZIAYb9bAABBDCCAaQwOgYIKwYBBQUHAgEWLmh0dHA6 # Ly93d3cuZGlnaWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5odG0wggFkBggr # BgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgAaQBzACAA # QwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQAZQBzACAA # YQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcAaQBDAGUA # cgB0ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwAeQBpAG4A # ZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBoACAA # bABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAgAGEAcgBlACAA # aQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIAeQAgAHIA # ZQBmAGUAcgBlAG4AYwBlAC4wCwYJYIZIAYb9bAMVMBIGA1UdEwEB/wQIMAYBAf8C # AQAweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4oDaG # NGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RD # QS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFz # c3VyZWRJRFJvb3RDQS5jcmwwHQYDVR0OBBYEFBUAEisTmLKZB+0e36K+Vw0rZwLN # MB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBBQUA # A4IBAQBGUD7Jtygkpzgdtlspr1LPUukxR6tWXHvVDQtBs+/sdR90OPKyXGGinJXD # UOSCuSPRujqGcq04eKx1XRcXNHJHhZRW0eu7NoR3zCSl8wQZVann4+erYs37iy2Q # wsDStZS9Xk+xBdIOPRqpFFumhjFiqKgz5Js5p8T1zh14dpQlc+Qqq8+cdkvtX8JL # FuRLcEwAiR78xXm8TBJX/l/hHrwCXaj++wc4Tw3GXZG5D2dFzdaD7eeSDY2xaYxP # +1ngIw/Sqq4AfO6cQg7PkdcntxbuD8O9fAqg7iwIVYUiuOsYGk38KiGtSTGDR5V3 # cdyxG0tLHBCcdxTBnU8vWpUIKRAmMYIEOzCCBDcCAQEwgYYwcjELMAkGA1UEBhMC # VVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0 # LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2ln # bmluZyBDQQIQBNXcH0jqydhSALrNmpsqpzAJBgUrDgMCGgUAoHgwGAYKKwYBBAGC # NwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor # BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQUi8mcZezS # u22d31v4MCWQkFxEctkwDQYJKoZIhvcNAQEBBQAEggEAjMl4A7War9LHzJif6IR6 # l+P1kgyriXc91Q233Qohg47tQefPYNeSqjzTk9dJZ+b3fZgAt6BAjonw2aJgxXEA # C0cLo5eS3ck/jGELWWVgOoatKlzPt520JTL520je99I0hFUQNfXUNkQ9RYk4C25Q # TRn/jPcJKoqvkfzpTrBu32Q8XyyQguT7dS7+XuO7IvpLSeuMAqWZJ2HuZL5WqbU4 # STQjHcoSI66ZlSaI/B7zFSkako/wvlOt66RfTRXaA5LZxnMXI2NGGkPOD8rp/S2g # 5vfsi4dKA3pBJlwbxXDBR5J5gqzyaaZZ007g6+QxRILKZ1FrW4uvT+Ejj05Wgqy9 # k6GCAg8wggILBgkqhkiG9w0BCQYxggH8MIIB+AIBATB2MGIxCzAJBgNVBAYTAlVT # MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j # b20xITAfBgNVBAMTGERpZ2lDZXJ0IEFzc3VyZWQgSUQgQ0EtMQIQAwGaAjr/WLFr # 1tXq5hfwZjAJBgUrDgMCGgUAoF0wGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAc # BgkqhkiG9w0BCQUxDxcNMjAxMjA2MTY1ODUwWjAjBgkqhkiG9w0BCQQxFgQUhGec # FJPne4KpQqxiUSUIDEQgpPQwDQYJKoZIhvcNAQEBBQAEggEAMP6vnZoMvhBWynOi # wHb+zaBA7RzyeIUwTcyA3jQv0W26ybrIwTxSXsduhq+1fFdlIfX61l0NOuyUNIad # cKactKUMyFK3xjwfscAng+t8PK0eZtycvOQfDr0Brz9Jedufv9zSCCicP90OFD1p # oTROmdezaEc7R11xNggLyIjYFgH4Z0jGYhrcj1g8/VWBbfq0gkuV4qWBYNBFDewz # 7Zt/ZJfmAOVJ4HD9g25qizZ0V0DSHpNv1yJrkrhS+N1R0v/ul+N630DOPHp9utmA # yMpHIyYb9HCOZygdMdqYvNbRfy8fmh5gBgO0kXEOygDBTdAzcMe3/C2jmaahDYqM # eTs48A== # SIG # End signature block |