PSPublishModule.psm1
function Add-Artefact { [CmdletBinding()] param( [string] $ModuleName, [string] $ModuleVersion, [string] $ArtefactName, [alias('IncludeTagName')][nullable[bool]] $IncludeTag, [nullable[bool]] $LegacyName, [nullable[bool]] $CopyMainModule, [nullable[bool]] $CopyRequiredModules, [string] $ProjectPath, [string] $Destination, [string] $DestinationMainModule, [string] $DestinationRequiredModules, [nullable[bool]] $DestinationFilesRelative, [alias('DestinationDirectoriesRelative')][nullable[bool]] $DestinationFoldersRelative, [alias('FilesOutput')][System.Collections.IDictionary] $Files, [alias('DirectoryOutput')][System.Collections.IDictionary] $Folders, [array] $RequiredModules, [string] $TagName, [string] $FileName, [nullable[bool]] $ZipIt, [string] $DestinationZip, [bool] $ConvertToScript, [string] $ScriptMerge, [System.Collections.IDictionary] $Configuration, [string] $ID ) $ResolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Destination) if ($ConvertToScript) { Write-TextWithTime -Text "Converting merged release to script" -PreAppend Plus -SpacesBefore ' ' { $convertToScriptSplat = @{ Enabled = $true IncludeTagName = $IncludeTag ModuleName = $ModuleName Destination = $DestinationMainModule ScriptMerge = $ScriptMerge } Remove-EmptyValue -Hashtable $convertToScriptSplat Copy-ArtefactToScript @convertToScriptSplat $copyRequiredModuleSplat = @{ Enabled = $CopyRequiredModules RequiredModules = $RequiredModules Destination = $DestinationRequiredModules } Copy-ArtefactRequiredModule @copyRequiredModuleSplat $copyArtefactRequiredFoldersSplat = @{ FoldersInput = $Folders ProjectPath = $ProjectPath Destination = $Destination DestinationRelative = $DestinationFoldersRelative } Copy-ArtefactRequiredFolders @copyArtefactRequiredFoldersSplat $copyArtefactRequiredFilesSplat = @{ FilesInput = $Files ProjectPath = $ProjectPath Destination = $Destination DestinationRelative = $DestinationFilesRelative } Copy-ArtefactRequiredFiles @copyArtefactRequiredFilesSplat } } else { Write-TextWithTime -Text "Copying merged release to $ResolvedDestination" -PreAppend Addition -SpacesBefore ' ' { $copyMainModuleSplat = @{ Enabled = $true IncludeTagName = $IncludeTag ModuleName = $ModuleName Destination = $DestinationMainModule } Copy-ArtefactMainModule @copyMainModuleSplat $copyRequiredModuleSplat = @{ Enabled = $CopyRequiredModules RequiredModules = $RequiredModules Destination = $DestinationRequiredModules } Copy-ArtefactRequiredModule @copyRequiredModuleSplat $copyArtefactRequiredFoldersSplat = @{ FoldersInput = $Folders ProjectPath = $ProjectPath Destination = $Destination DestinationRelative = $DestinationFoldersRelative } Copy-ArtefactRequiredFolders @copyArtefactRequiredFoldersSplat $copyArtefactRequiredFilesSplat = @{ FilesInput = $Files ProjectPath = $ProjectPath Destination = $Destination DestinationRelative = $DestinationFilesRelative } Copy-ArtefactRequiredFiles @copyArtefactRequiredFilesSplat } } if ($ZipIt -and $DestinationZip) { $ResolvedDestinationZip = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationZip) Write-TextWithTime -Text "Zipping merged release to $ResolvedDestinationZip" -PreAppend Information -SpacesBefore ' ' { $zipSplat = @{ Source = $ResolvedDestination Destination = $ResolvedDestinationZip Configuration = $Configuration LegacyName = if ($Configuration.Steps.BuildModule.Releases -is [bool]) { $true } else { $false } ModuleName = $ModuleName ModuleVersion = $ModuleVersion IncludeTag = $IncludeTag ArtefactName = $ArtefactName ID = $ID } Compress-Artefact @zipSplat } Write-TextWithTime -Text "Removing temporary files from $ResolvedDestination" -SpacesBefore ' ' -PreAppend Minus { Remove-ItemAlternative -Path $ResolvedDestination -SkipFolder -Exclude '*.zip' } -ColorBefore Yellow -ColorTime Green -ColorError Red -Color Yellow } } function Add-Directory { [CmdletBinding()] param( [string] $Directory ) $exists = Test-Path -Path $Directory if ($exists -eq $false) { $null = New-Item -Path $Directory -ItemType Directory -Force } } function Compress-Artefact { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [string] $Source, [string] $Destination, [string] $ModuleName, [string] $ModuleVersion, [nullable[bool]] $IncludeTag, [nullable[bool]] $LegacyName, [string] $ArtefactName, [string] $ID ) # if pre-release is set, we want to use it in the name if ($Configuration.CurrentSettings.PreRelease) { $ModuleVersionWithPreRelease = "$($ModuleVersion)-$($Configuration.CurrentSettings.PreRelease)" $TagModuleVersionWithPreRelease = "v$($ModuleVersionWithPreRelease)" } else { $ModuleVersionWithPreRelease = $ModuleVersion $TagModuleVersionWithPreRelease = "v$($ModuleVersion)" } if ($LegacyName) { # This is to support same, old configuration and not break existing projects $FileName = -join ("v$($ModuleVersion)", '.zip') } elseif ($ArtefactName) { $TagName = "v$($ModuleVersion)" $FileName = $ArtefactName $FileName = $FileName.Replace('{ModuleName}', $ModuleName) $FileName = $FileName.Replace('<ModuleName>', $ModuleName) $FileName = $FileName.Replace('{ModuleVersion}', $ModuleVersion) $FileName = $FileName.Replace('<ModuleVersion>', $ModuleVersion) $FileName = $FileName.Replace('{ModuleVersionWithPreRelease}', $ModuleVersionWithPreRelease) $FileName = $FileName.Replace('<ModuleVersionWithPreRelease>', $ModuleVersionWithPreRelease) $FileName = $FileName.Replace('{TagModuleVersionWithPreRelease}', $TagModuleVersionWithPreRelease) $FileName = $FileName.Replace('<TagModuleVersionWithPreRelease>', $TagModuleVersionWithPreRelease) $FileName = $FileName.Replace('{TagName}', $TagName) $FileName = $FileName.Replace('<TagName>', $TagName) # if user specified a file extension, we don't want to add .zip extension $FileName = if ($FileName.EndsWith(".zip")) { $FileName } else { -join ($FileName, '.zip') } } else { if ($IncludeTag) { $TagName = "v$($ModuleVersion)" } else { $TagName = '' } if ($TagName) { $FileName = -join ($ModuleName, ".$TagName", '.zip') } else { $FileName = -join ($ModuleName, '.zip') } } $ZipPath = [System.IO.Path]::Combine($Destination, $FileName) $ZipPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ZipPath) $Configuration.CurrentSettings.ArtefactZipName = $FileName $Configuration.CurrentSettings.ArtefactZipPath = $ZipPath if ($ID) { # ID was provided $ZipPackage = [ordered] @{ 'Id' = $ID 'ZipName' = $FileName 'ZipPath' = $ZipPath } $Configuration.CurrentSettings['Artefact'] += $ZipPackage } else { if (-not $Configuration.CurrentSettings['ArtefactDefault']) { $Configuration.CurrentSettings['ArtefactDefault'] = [ordered] @{ 'Id' = 'Default' 'ZipName' = $FileName 'ZipPath' = $ZipPath } } } $Success = Write-TextWithTime -Text "Compressing final merged release $ZipPath" { $null = New-Item -ItemType Directory -Path $Destination -Force # Keep in mind we're skipping hidden files, as compress-archive doesn't support those # and I don't feel like rewritting it myself :-) # but i believe we should not be copying them in the first place # from what I saw most are PowerShellGet cache files [Array] $DirectoryToCompress = Get-ChildItem -Path $Source -Directory -ErrorAction SilentlyContinue [Array] $FilesToCompress = Get-ChildItem -Path $Source -File -Exclude '*.zip' -ErrorAction SilentlyContinue if ($DirectoryToCompress.Count -gt 0 -and $FilesToCompress.Count -gt 0) { Compress-Archive -Path @($DirectoryToCompress.FullName + $FilesToCompress.FullName) -DestinationPath $ZipPath -Force -ErrorAction Stop } elseif ($DirectoryToCompress.Count -gt 0) { Compress-Archive -Path $DirectoryToCompress.FullName -DestinationPath $ZipPath -Force -ErrorAction Stop } elseif ($FilesToCompress.Count -gt 0) { Compress-Archive -Path $FilesToCompress.FullName -DestinationPath $ZipPath -Force -ErrorAction Stop } } -PreAppend Addition -SpacesBefore ' ' -Color Yellow -ColorTime Green -ColorBefore Yellow -ColorError Red if ($Success -eq $false) { return $false } } function Convert-HashTableToNicelyFormattedString { [CmdletBinding()] param( [System.Collections.IDictionary] $hashTable ) [string] $nicelyFormattedString = $hashTable.Keys | ForEach-Object ` { $key = $_ $value = $hashTable.$key " $key = $value$NewLine" } return $nicelyFormattedString } function Convert-RequiredModules { <# .SYNOPSIS Converts the RequiredModules section of the manifest to the correct format .DESCRIPTION Converts the RequiredModules section of the manifest to the correct format Fixes the ModuleVersion and Guid if set to 'Latest' or 'Auto' .PARAMETER Configuration The configuration object of the module .EXAMPLE Convert-RequiredModules -Configuration $Configuration .NOTES General notes #> [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration ) $Manifest = $Configuration.Information.Manifest if ($Manifest.Contains('RequiredModules')) { foreach ($SubModule in $Manifest.RequiredModules) { if ($SubModule -is [string]) { #[Array] $AvailableModule = Get-Module -ListAvailable $SubModule -Verbose:$false } else { [Array] $AvailableModule = Get-Module -ListAvailable $SubModule.ModuleName -Verbose:$false if ($SubModule.ModuleVersion -in 'Latest', 'Auto') { if ($AvailableModule) { $SubModule.ModuleVersion = $AvailableModule[0].Version.ToString() } else { Write-Text -Text "[-] Module $($SubModule.ModuleName) is not available, but defined as required with last version. Terminating." -Color Red return $false } } if ($SubModule.Guid -in 'Latest', 'Auto') { if ($AvailableModule) { $SubModule.Guid = $AvailableModule[0].Guid.ToString() } else { Write-Text -Text "[-] Module $($SubModule.ModuleName) is not available, but defined as required with last version. Terminating." -Color Red return $false } } } } } } function Copy-ArtefactMainModule { [CmdletBinding()] param( [switch] $Enabled, [nullable[bool]] $IncludeTagName, [string] $ModuleName, [string] $Destination ) if (-not $Enabled) { return } if ($IncludeTagName) { $NameOfDestination = [io.path]::Combine($Destination, $ModuleName, $TagName) } else { $NameOfDestination = [io.path]::Combine($Destination, $ModuleName) } $ResolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($NameOfDestination) Write-TextWithTime -PreAppend Addition -Text "Copying main module to $ResolvedDestination" -Color Yellow { if (Test-Path -Path $NameOfDestination) { Remove-ItemAlternative -LiteralPath $NameOfDestination } $null = New-Item -ItemType Directory -Path $Destination -Force if ($DestinationPaths.Desktop) { Copy-Item -LiteralPath $DestinationPaths.Desktop -Recurse -Destination $ResolvedDestination -Force } elseif ($DestinationPaths.Core) { Copy-Item -LiteralPath $DestinationPaths.Core -Recurse -Destination $ResolvedDestination -Force } } -SpacesBefore ' ' } function Copy-ArtefactRequiredFiles { [CmdletBinding()] param( [System.Collections.IDictionary] $FilesInput, [string] $ProjectPath, [string] $Destination, [nullable[bool]] $DestinationRelative ) foreach ($File in $FilesInput.Keys) { if ($FilesInput[$File] -is [string]) { $FullFilePath = [System.IO.Path]::Combine($ProjectPath, $File) if (Test-Path -Path $FullFilePath) { if ($DestinationRelative) { $DestinationPath = [System.IO.Path]::Combine($Destination, $FilesInput[$File]) } else { $DestinationPath = $FilesInput[$File] } $ResolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath) Write-TextWithTime -Text "Copying file $FullFilePath to $ResolvedDestination" { $DirectoryPath = [Io.Path]::GetDirectoryName($ResolvedDestination) $null = New-Item -ItemType Directory -Force -ErrorAction Stop -Path $DirectoryPath Copy-Item -LiteralPath $FullFilePath -Destination $ResolvedDestination -Force -ErrorAction Stop } -PreAppend Addition -SpacesBefore ' ' -Color Yellow } else { Write-TextWithTime -Text "File $FullFilePath does not exist" -PreAppend Plus -SpacesBefore ' ' -Color Red -ColorTime Red -ColorBefore Red return $false } } elseif ($FilesInput[$File] -is [System.Collections.IDictionary]) { if ($FilesInput[$File].Enabled -eq $true) { if ($FilesInput[$File].Source) { $FullFilePath = [System.IO.Path]::Combine($ProjectPath, $FilesInput[$File].Source) if (Test-Path -Path $FullFilePath) { if ($FilesInput[$File].DestinationRelative) { $DestinationPath = [System.IO.Path]::Combine($Destination, $FilesInput[$File].Destination) } else { $DestinationPath = $FilesInput[$File].Destination } $ResolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath) Write-TextWithTime -Text "Copying file $FullFilePath to $ResolvedDestination" { $DirectoryPath = [Io.Path]::GetDirectoryName($ResolvedDestination) $null = New-Item -ItemType Directory -Force -ErrorAction Stop -Path $DirectoryPath Copy-Item -LiteralPath $FullFilePath -Destination $ResolvedDestination -Force -ErrorAction Stop } -PreAppend Addition -SpacesBefore ' ' -Color Yellow } else { Write-TextWithTime -Text "File $FullFilePath does not exist" -PreAppend Plus -SpacesBefore ' ' -Color Red -ColorTime Red -ColorBefore Red return $false } } } } } } function Copy-ArtefactRequiredFolders { [CmdletBinding()] param( [System.Collections.IDictionary] $FoldersInput, [string] $ProjectPath, [string] $Destination, [nullable[bool]] $DestinationRelative ) } function Copy-ArtefactRequiredModule { [CmdletBinding()] param( [switch] $Enabled, [Array] $RequiredModules, [string] $Destination ) if (-not $Enabled) { return } if (-not (Test-Path -LiteralPath $Destination)) { New-Item -ItemType Directory -Path $Destination -Force } foreach ($Module in $RequiredModules) { if ($Module.ModuleName) { Write-TextWithTime -PreAppend Addition -Text "Copying required module $($Module.ModuleName)" -Color Yellow { $ModulesFound = Get-Module -ListAvailable -Name $Module.ModuleName if ($ModulesFound.Count -gt 0) { $PathToPSD1 = if ($Module.ModuleVersion -eq 'Latest') { $ModulesFound[0].Path } else { $FoundModule = foreach ($M in $ModulesFound) { if ($M.Version -eq $Module.ModuleVersion) { $M.Path break } } if (-not $FoundModule) { # we tried to find exact version, but it was not found # we use the latest version instead $ModulesFound[0].Path } else { $FoundModule } } $FolderToCopy = [System.IO.Path]::GetDirectoryName($PathToPSD1) $ItemInformation = Get-Item -LiteralPath $FolderToCopy if ($ItemInformation.Name -ne $Module.ModuleName) { $NewPath = [io.path]::Combine($Destination, $Module.ModuleName) if (Test-Path -LiteralPath $NewPath) { Remove-Item -LiteralPath $NewPath -Recurse -Force -ErrorAction Stop } Copy-Item -LiteralPath $FolderToCopy -Destination $NewPath -Recurse -Force -ErrorAction Stop } else { Copy-Item -LiteralPath $FolderToCopy -Destination $Destination -Recurse -Force } } } -SpacesBefore ' ' } } } function Copy-ArtefactToScript { [CmdletBinding()] param( [switch] $Enabled, [nullable[bool]] $IncludeTagName, [string] $ModuleName, [string] $Destination, [string] $ScriptMerge ) if (-not $Enabled) { return } if ($IncludeTagName) { $NameOfDestination = [io.path]::Combine($Destination, $ModuleName, $TagName) } else { $NameOfDestination = [io.path]::Combine($Destination, $ModuleName) } $ResolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($NameOfDestination) Write-TextWithTime -PreAppend Addition -Text "Copying main module to $ResolvedDestination" -Color Yellow { if (Test-Path -Path $NameOfDestination) { Remove-ItemAlternative -LiteralPath $NameOfDestination } $null = New-Item -ItemType Directory -Path $Destination -Force if ($DestinationPaths.Desktop) { Copy-Item -LiteralPath $DestinationPaths.Desktop -Recurse -Destination $ResolvedDestination -Force } elseif ($DestinationPaths.Core) { Copy-Item -LiteralPath $DestinationPaths.Core -Recurse -Destination $ResolvedDestination -Force } } -SpacesBefore ' ' Write-TextWithTime -PreAppend Addition -Text "Cleaning up main module" -Color Yellow { $PSD1 = [io.path]::Combine($ResolvedDestination, "$ModuleName.psd1") Remove-Item -LiteralPath $PSD1 -Force -ErrorAction Stop $PSM1 = [io.path]::Combine($ResolvedDestination, "$ModuleName.psm1") Rename-Item -LiteralPath $PSM1 -NewName "$ModuleName.ps1" -Force -ErrorAction Stop $PS1 = [io.path]::Combine($ResolvedDestination, "$ModuleName.ps1") $Content = Get-Content -LiteralPath $PS1 -ErrorAction Stop # Find the index of the line that contains "# Export functions and aliases as required" starting from the bottom of the file $index = ($content | Select-String -Pattern "# Export functions and aliases as required" -SimpleMatch | Select-Object -Last 1).LineNumber # Remove all lines below the index, including that line $content = $content[0..($index - 2)] if ($ScriptMerge) { $content += $ScriptMerge } # Output the updated content Set-Content -LiteralPath $PS1 -Value $content -Force -ErrorAction Stop -Encoding UTF8 } -SpacesBefore ' ' } function Copy-DictionaryManual { [CmdletBinding()] param( [System.Collections.IDictionary] $Dictionary ) $clone = @{} foreach ($Key in $Dictionary.Keys) { $value = $Dictionary.$Key $clonedValue = switch ($Dictionary.$Key) { { $null -eq $_ } { $null continue } { $_ -is [System.Collections.IDictionary] } { Copy-DictionaryManual -Dictionary $_ continue } { $type = $_.GetType() $type.IsPrimitive -or $type.IsValueType -or $_ -is [string] } { $_ continue } default { $_ | Select-Object -Property * } } if ($value -is [System.Collections.IList]) { $clone[$Key] = @($clonedValue) } else { $clone[$Key] = $clonedValue } } $clone } function Copy-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-NetFramework { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER RequireVersion Parameter description .EXAMPLE Find-NetFramework -RequireVersion 4.8 .NOTES General notes #> [cmdletBinding()] param( [string] $RequireVersion ) if ($RequireVersion) { $Framework = [ordered] @{ '4.5' = 378389 '4.5.1' = 378675 '4.5.2' = 379893 '4.6' = 393295 '4.6.1' = 394254 '4.6.2' = 394802 '4.7' = 460798 '4.7.1' = 461308 '4.7.2' = 461808 '4.8' = 528040 } $DetectVersion = $Framework[$RequireVersion] "if (`$PSVersionTable.PSEdition -eq 'Desktop' -and (Get-ItemProperty `"HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full`").Release -lt $DetectVersion) { Write-Warning `"This module requires .NET Framework $RequireVersion or later.`"; return } " } } function Find-RequiredModules { [cmdletbinding()] param( [string] $Name ) $Module = Get-Module -ListAvailable $Name -ErrorAction SilentlyContinue -Verbose:$false $AllModules = if ($Module) { [Array] $RequiredModules = $Module.RequiredModules.Name if ($null -ne $RequiredModules) { $null } $RequiredModules foreach ($_ in $RequiredModules) { Find-RequiredModules -Name $_ } } [Array] $ListModules = $AllModules | Where-Object { $null -ne $_ } if ($null -ne $ListModules) { [array]::Reverse($ListModules) } $CleanedModules = [System.Collections.Generic.List[string]]::new() foreach ($_ in $ListModules) { if ($CleanedModules -notcontains $_) { $CleanedModules.Add($_) } } $CleanedModules } function Format-Code { [cmdletbinding()] param( [string] $FilePath, [System.Collections.IDictionary] $FormatCode ) if ($FormatCode.Enabled) { if ($FormatCode.RemoveComments) { # Write-Verbose "Removing Comments" $ContentBefore = Get-Content -LiteralPath $FilePath -Encoding UTF8 Write-Text "[i] Removing Comments - Lines in code before: $($ContentBefore.Count)" -Color Yellow $Output = Write-TextWithTime -Text "[+] Removing Comments - $FilePath" { Remove-Comments -FilePath $FilePath } if ($Output -and $Output.StartPosition -and $Output.StartPosition.EndLine -gt 1) { Write-Text "[i] Removing Comments - Lines in code after: $($Output.StartPosition.EndLine)" -Color Yellow } else { Write-Text "[i] Removing Comments - Lines in code after: 0" -Color Red } } else { $Output = Write-TextWithTime -Text "Reading file content - $FilePath" { Get-Content -LiteralPath $FilePath -Raw -Encoding UTF8 } -PreAppend Plus -SpacesBefore ' ' } if ($null -eq $FormatCode.FormatterSettings) { $FormatCode.FormatterSettings = $Script:FormatterSettings } $Data = Write-TextWithTime -Text "Formatting file - $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." -Color Red Write-Text " [-] Format-Code - Error: $ErrorMessage" -Color Red Write-Text " [-] Format-Code - This is most likely related to a bug in PSScriptAnalyzer running inside VSCode. Please try running outside of VSCode when using formatting." -Color Red return $false } } -PreAppend Plus -SpacesBefore ' ' if ($Data -eq $false) { return $false } Write-TextWithTime -Text "Saving file - $FilePath" { # Resave $Final = foreach ($O in $Data) { if ($O.Trim() -ne '') { $O.Trim() } } try { $Final | Out-File -LiteralPath $FilePath -NoNewline -Encoding utf8 -ErrorAction Stop } 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 return $false } } -PreAppend Plus -SpacesBefore ' ' } } 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 | Out-File -Append -LiteralPath $FilePathUsing -Encoding utf8 $Content | Out-File -Append -LiteralPath $FilePathSave -Encoding utf8 return $true } else { $Content | Out-File -Append -LiteralPath $FilePathSave -Encoding utf8 return $False } } } 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-Encoding { [cmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)][Alias('FullName')][string] $Path ) process { $bom = New-Object -TypeName System.Byte[](4) $file = New-Object System.IO.FileStream($Path, 'Open', 'Read') $null = $file.Read($bom, 0, 4) $file.Close() $file.Dispose() $enc = [Text.Encoding]::ASCII if ($bom[0] -eq 0x2b -and $bom[1] -eq 0x2f -and $bom[2] -eq 0x76) { $enc = [Text.Encoding]::UTF7 } if ($bom[0] -eq 0xff -and $bom[1] -eq 0xfe) { $enc = [Text.Encoding]::Unicode } if ($bom[0] -eq 0xfe -and $bom[1] -eq 0xff) { $enc = [Text.Encoding]::BigEndianUnicode } if ($bom[0] -eq 0x00 -and $bom[1] -eq 0x00 -and $bom[2] -eq 0xfe -and $bom[3] -eq 0xff) { $enc = [Text.Encoding]::UTF32 } if ($bom[0] -eq 0xef -and $bom[1] -eq 0xbb -and $bom[2] -eq 0xbf) { $enc = [Text.Encoding]::UTF8 } [PSCustomObject]@{ Encoding = $enc Path = $Path } } } function Get-FilteredScriptCommands { [CmdletBinding()] param( [Array] $Commands, [switch] $NotCmdlet, [switch] $NotUnknown, [switch] $NotApplication, [string[]] $Functions, [string] $FilePath, [string[]] $ApprovedModules ) if ($Functions.Count -eq 0) { $Functions = Get-FunctionNames -Path $FilePath } $Commands = $Commands | Where-Object { $_ -notin $Functions } $Commands = $Commands | Sort-Object -Unique $Scan = foreach ($Command in $Commands) { try { $IsAlias = $false $Data = Get-Command -Name $Command -ErrorAction Stop if ($Data.Source -eq 'PSPublishModule') { # we need to exclude PSPublishModule from any processing # this is because it has advantage of being in the same scope # this means it's functions would be preferred over any other if ($Data.Source -notin $ApprovedModules) { throw } } if ($Data.CommandType -eq 'Alias') { $Data = Get-Command -Name $Data.Definition $IsAlias = $true } [PSCustomObject] @{ Name = $Data.Name Source = $Data.Source CommandType = $Data.CommandType IsAlias = $IsAlias IsPrivate = $false Error = '' ScriptBlock = $Data.ScriptBlock } } catch { $CurrentOutput = [PSCustomObject] @{ Name = $Command Source = '' CommandType = '' IsAlias = $IsAlias IsPrivate = $false Error = $_.Exception.Message ScriptBlock = '' } # 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 UTF8 } $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 = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) [System.Management.Automation.Language.Parser]::ParseFile(($FilePath), [ref]$null, [ref]$null).FindAll( { param($c)$c -is [Management.Automation.Language.FunctionDefinitionAst] }, $Recurse).Name } } function Get-GitLog { # Source https://gist.github.com/thedavecarroll/3245449f5ff893e51907f7aa13e33ebe # Author: thedavecarroll/Get-GitLog.ps1 [CmdLetBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(ParameterSetName = 'Default', Mandatory)] [Parameter(ParameterSetName = 'SourceTarget', Mandatory)] [ValidateScript({ Resolve-Path -Path $_ | Test-Path })] [string]$GitFolder, [Parameter(ParameterSetName = 'SourceTarget', Mandatory)] [string]$StartCommitId, [Parameter(ParameterSetName = 'SourceTarget')] [string]$EndCommitId = 'HEAD' ) Push-Location try { Set-Location -Path $GitFolder $GitCommand = Get-Command -Name git -ErrorAction Stop } catch { $PSCmdlet.ThrowTerminatingError($_) } if ($StartCommitId) { $GitLogCommand = '"{0}" log --oneline --format="%H`t%h`t%ai`t%an`t%ae`t%ci`t%cn`t%ce`t%s`t%f" {1}...{2} 2>&1' -f $GitCommand.Source, $StartCommitId, $EndCommitId } else { $GitLogCommand = '"{0}" log --oneline --format="%H`t%h`t%ai`t%an`t%ae`t%ci`t%cn`t%ce`t%s`t%f" 2>&1' -f $GitCommand.Source } Write-Verbose -Message $GitLogCommand $GitLog = Invoke-Expression -Command "& $GitLogCommand" -ErrorAction SilentlyContinue Pop-Location if ($GitLog[0] -notmatch 'fatal:') { $GitLog | ConvertFrom-Csv -Delimiter "`t" -Header 'CommitId', 'ShortCommitId', 'AuthorDate', 'AuthorName', 'AuthorEmail', 'CommitterDate', 'CommitterName', 'ComitterEmail', 'CommitMessage', 'SafeCommitMessage' } else { if ($GitLog[0] -like "fatal: ambiguous argument '*...*'*") { Write-Warning -Message 'Unknown revision. Please check the values for StartCommitId or EndCommitId; omit the parameters to retrieve the entire log.' } else { Write-Error -Category InvalidArgument -Message ($GitLog -join "`n") } } } function Get-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-RequiredModule { [cmdletbinding()] param( [string] $Path, [string] $Name ) $PrimaryModule = Get-ChildItem -LiteralPath "$Path\$Name" -Filter '*.psd1' -Recurse -ErrorAction SilentlyContinue -Depth 1 if ($PrimaryModule) { $Module = Get-Module -ListAvailable $PrimaryModule.FullName -ErrorAction SilentlyContinue -Verbose:$false if ($Module) { [Array] $RequiredModules = $Module.RequiredModules.Name if ($null -ne $RequiredModules) { $null } $RequiredModules foreach ($_ in $RequiredModules) { Get-RequiredModule -Path $Path -Name $_ } } } else { Write-Warning "Initialize-ModulePortable - Modules to load not found in $Path" } } function Get-RestMethodExceptionDetailsOrNull { [CmdletBinding()] param( [Exception] $restMethodException ) try { $responseDetails = @{ ResponseUri = $exception.Response.ResponseUri StatusCode = $exception.Response.StatusCode StatusDescription = $exception.Response.StatusDescription ErrorMessage = $exception.Message } [string] $responseDetailsAsNicelyFormattedString = Convert-HashTableToNicelyFormattedString $responseDetails [string] $errorInfo = "Request Details:" + $NewLine + $requestDetailsAsNicelyFormattedString $errorInfo += $NewLine $errorInfo += "Response Details:" + $NewLine + $responseDetailsAsNicelyFormattedString return $errorInfo } catch { return $null } } function Get-ScriptCommands { [CmdletBinding()] param( [string] $FilePath, [alias('ScriptBlock')][scriptblock] $Code, [switch] $CommandsOnly ) $astTokens = $null $astErr = $null if ($FilePath) { $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-ScriptsContentAndTryReplace { <# .SYNOPSIS Gets script content and replaces $PSScriptRoot\..\..\ with $PSScriptRoot\ .DESCRIPTION Gets script content and replaces $PSScriptRoot\..\..\ with $PSScriptRoot\ .PARAMETER Files Parameter description .PARAMETER OutputPath Parameter description .EXAMPLE Get-ScriptsContentAndTryReplace -Files 'C:\Support\GitHub\PSWriteHTML\Private\Get-HTMLLogos.ps1' -OutputPath "C:\Support\GitHub\PSWriteHTML\Private\Get-HTMLLogos1.ps1" .NOTES Often in code people would use relative paths to get to the root of the module. This is all great but the path changes during merge. So we fix this by replacing $PSScriptRoot\..\..\ with $PSScriptRoot\ While in best case they should always use $MyInvocation.MyCommand.Module.ModuleBase It's not always possible. So this is a workaround. Very bad workaround, but it works, but may have unintended consequences. $Content = @( '$PSScriptRoot\..\..\Build\Manage-PSWriteHTML.ps1' '$PSScriptRoot\..\Build\Manage-PSWriteHTML.ps1' '$PSScriptRoot\Build\Manage-PSWriteHTML.ps1' "[IO.Path]::Combine(`$PSScriptRoot, '..', 'Images')" "[IO.Path]::Combine(`$PSScriptRoot,'..','Images')" ) $Content = $Content -replace [regex]::Escape('$PSScriptRoot\..\..\'), '$PSScriptRoot\' -replace [regex]::Escape('$PSScriptRoot\..\'), '$PSScriptRoot\' $Content = $Content -replace [regex]::Escape("`$PSScriptRoot, '..',"), '$PSScriptRoot,' -replace [regex]::Escape("`$PSScriptRoot,'..',"), '$PSScriptRoot,' $Content #> [cmdletbinding()] param( [string[]] $Files, [string] $OutputPath, [switch] $DoNotAttemptToFixRelativePaths ) if ($DoNotAttemptToFixRelativePaths) { Write-TextWithTime -Text "Without expanding variables (`$PSScriptRoot\..\.. etc.)" { foreach ($FilePath in $Files) { $Content = Get-Content -Path $FilePath -Raw -Encoding utf8 if ($Content.Count -gt 0) { try { $Content | Out-File -Append -LiteralPath $OutputPath -Encoding utf8 } catch { $ErrorMessage = $_.Exception.Message Write-Text "[-] Get-ScriptsContentAndTryReplace - Merge on file $FilePath failed. Error: $ErrorMessage" -Color Red return $false } } } } -PreAppend Plus -Color Green -SpacesBefore " " -ColorTime Green } else { Write-TextWithTime -Text "Replacing expandable variables (`$PSScriptRoot\..\.. etc.)" { foreach ($FilePath in $Files) { $Content = Get-Content -Path $FilePath -Raw -Encoding utf8 if ($Content.Count -gt 0) { # $MyInvocation.MyCommand.Module.ModuleBase # $ModuleBase = $MyInvocation.MyCommand.Module.ModuleBase # $ModuleInvocation = $MyInvocation # Ensure file has content # $Content = $Content.Replace('$PSScriptRoot\..\..\', '$PSScriptRoot\') # $Content = $Content.Replace('$PSScriptRoot\..\', '$PSScriptRoot\') #$Content = $Content -replace [regex]::Escape('$PSScriptRoot\..\..\'), '\$PSScriptRoot\\' -replace [regex]::Escape('$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,") #$Content = $Content -replace [regex]::Escape("`$PSScriptRoot, '..',"), "\`$PSScriptRoot," -replace [regex]::Escape("`$PSScriptRoot,'..',"), "\`$PSScriptRoot," # this is a very big hack, which excludes this file from being fixed, as it breaks the script if (-not $FilePath.EndsWith('Get-ScriptsContentAndTryReplace.ps1')) { $Content = $Content -replace [regex]::Escape('$PSScriptRoot\..\..\'), '$PSScriptRoot\' $Content = $Content -replace [regex]::Escape('$PSScriptRoot\..\'), '$PSScriptRoot\' $Content = $Content -replace [regex]::Escape("`$PSScriptRoot, '..',"), '$PSScriptRoot,' $Content = $Content -replace [regex]::Escape("`$PSScriptRoot,'..',"), '$PSScriptRoot,' } } try { $Content | Out-File -Append -LiteralPath $OutputPath -Encoding utf8 } catch { $ErrorMessage = $_.Exception.Message Write-Text "[-] Get-ScriptsContentAndTryReplace - Merge on file $FilePath failed. Error: $ErrorMessage" -Color Red return $false } } } -PreAppend Information -Color Magenta -SpacesBefore " " -ColorBefore Magenta -ColorTime Magenta } } function Import-ValidCertificate { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] param( [parameter(Mandatory, ParameterSetName = 'FilePath')][string] $FilePath, [parameter(Mandatory, ParameterSetName = 'Base64')][string] $CertificateAsBase64, [parameter(Mandatory)][string] $PfxPassword ) if ($FilePath -and (Test-Path -LiteralPath $FilePath)) { $TemporaryFile = $FilePath } elseif ($CertificateAsBase64) { $TemporaryFile = [io.path]::GetTempFileName() if ($PSVersionTable.PSEdition -eq 'Core') { Set-Content -AsByteStream -Value $([System.Convert]::FromBase64String($CertificateAsBase64)) -Path $TemporaryFile -ErrorAction Stop } else { Set-Content -Value $([System.Convert]::FromBase64String($CertificateAsBase64)) -Path $TemporaryFile -Encoding Byte -ErrorAction Stop } } else { return $false } if ($TemporaryFile) { $CodeSigningCert = Import-PfxCertificate -FilePath $pfxCertFilePath -Password $($PfxPassword | ConvertTo-SecureString -AsPlainText -Force) -CertStoreLocation Cert:\CurrentUser\My -ErrorAction Stop if ($CodeSigningCert) { return $CodeSigningCert } else { return $false } } else { return $false } } function Initialize-InternalTests { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [string] $Type ) if ($Configuration.Options.$Type.TestsPath -and (Test-Path -LiteralPath $Configuration.Options.$Type.TestsPath)) { Write-TextWithTime -PreAppend Plus -Text "Running tests ($Type)" { $TestsResult = Invoke-Pester -Script $Configuration.Options.$Type.TestsPath -Verbose -PassThru Write-Host if (-not $TestsResult) { if ($Configuration.Options.$Type.Force) { Write-Text "[e] Tests ($Type) failed, but Force was used to skip failed tests. Continuing" -Color Red } else { Write-Text "[e] Tests ($Type) failed. Terminating." -Color Red return $false } } elseif ($TestsResult.FailedCount -gt 0) { if ($Configuration.Options.$Type.Force) { Write-Text "[e] Tests ($Type) failed, but Force was used to skip failed tests. Continuing" -Color Red } else { Write-Text "[e] Tests ($Type) failed (failedCount $($TestsResult.FailedCount)). Terminating." -Color Red return $false } } } -Color Blue } else { Write-Text "[e] Tests ($Type) are enabled, but the path to tests doesn't exits. Terminating." -Color Red return $false } } function Invoke-RestMethodAndThrowDescriptiveErrorOnFailure { [cmdletbinding()] param( [System.Collections.IDictionary] $requestParametersHashTable ) $requestDetailsAsNicelyFormattedString = Convert-HashTableToNicelyFormattedString $requestParametersHashTable Write-Verbose "Making web request with the following parameters:$NewLine$requestDetailsAsNicelyFormattedString" try { $webRequestResult = Invoke-RestMethod @requestParametersHashTable } catch { [Exception] $exception = $_.Exception [string] $errorMessage = Get-RestMethodExceptionDetailsOrNull -restMethodException $exception if ([string]::IsNullOrWhiteSpace($errorMessage)) { $errorMessage = $exception.ToString() } throw "An unexpected error occurred while making web request:$NewLine$errorMessage" } Write-Verbose "Web request returned the following result:$NewLine$webRequestResult" return $webRequestResult } function Merge-Module { [CmdletBinding()] param ( [string] $ModuleName, [string] $ModulePathSource, [string] $ModulePathTarget, [Parameter(Mandatory = $false, ValueFromPipeline = $false)] [ValidateSet("ASC", "DESC", "NONE", '')] [string] $Sort = 'NONE', [string[]] $FunctionsToExport, [string[]] $AliasesToExport, [System.Collections.IDictionary] $AliasesAndFunctions, [Array] $LibrariesStandard, [Array] $LibrariesCore, [Array] $LibrariesDefault, [System.Collections.IDictionary] $FormatCodePSM1, [System.Collections.IDictionary] $FormatCodePSD1, [System.Collections.IDictionary] $Configuration, [string[]] $DirectoriesWithPS1, [string[]] $ClassesPS1, [System.Collections.IDictionary] $IncludeAsArray ) $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] Merging files into PSM1" -Color Blue $PSM1FilePath = "$ModulePathTarget\$ModuleName.psm1" $PSD1FilePath = "$ModulePathTarget\$ModuleName.psd1" # [Array] $ClassesFunctions = foreach ($Directory in $DirectoriesWithPS1) { # if ($PSEdition -eq 'Core') { # Get-ChildItem -Path $ModulePathSource\$Directory\*.ps1 -ErrorAction SilentlyContinue -Recurse -FollowSymlink # } else { # Get-ChildItem -Path $ModulePathSource\$Directory\*.ps1 -ErrorAction SilentlyContinue -Recurse # } # } [Array] $ArrayIncludes = foreach ($VariableName in $IncludeAsArray.Keys) { $FilePathVariables = [System.IO.Path]::Combine($ModulePathSource, $IncludeAsArray[$VariableName], "*.ps1") [Array] $FilesInternal = if ($PSEdition -eq 'Core') { Get-ChildItem -Path $FilePathVariables -ErrorAction SilentlyContinue -Recurse -FollowSymlink } else { Get-ChildItem -Path $FilePathVariables -ErrorAction SilentlyContinue -Recurse } "$VariableName = @(" foreach ($Internal in $FilesInternal) { Get-Content -Path $Internal.FullName -Raw -Encoding utf8 } ")" } # If dot source classes option is enabled we treat classes into separete file, and that means we need to exclude it from standard case if ($Configuration.Steps.BuildModule.ClassesDotSource) { [Array] $ListDirectoriesPS1 = foreach ($Dir in $DirectoriesWithPS1) { if ($Dir -ne $ClassesPS1) { $Dir } } } else { [Array] $ListDirectoriesPS1 = $DirectoriesWithPS1 } [Array] $ScriptFunctions = foreach ($Directory in $ListDirectoriesPS1) { if ($PSEdition -eq 'Core') { Get-ChildItem -Path $ModulePathSource\$Directory\*.ps1 -ErrorAction SilentlyContinue -Recurse -FollowSymlink } else { Get-ChildItem -Path $ModulePathSource\$Directory\*.ps1 -ErrorAction SilentlyContinue -Recurse } } [Array] $ClassesFunctions = foreach ($Directory in $ClassesPS1) { if ($PSEdition -eq 'Core') { Get-ChildItem -Path $ModulePathSource\$Directory\*.ps1 -ErrorAction SilentlyContinue -Recurse -FollowSymlink } else { Get-ChildItem -Path $ModulePathSource\$Directory\*.ps1 -ErrorAction SilentlyContinue -Recurse } } if ($Sort -eq 'ASC') { $ScriptFunctions = $ScriptFunctions | Sort-Object -Property Name $ClassesFunctions = $ClassesFunctions | Sort-Object -Property Name } elseif ($Sort -eq 'DESC') { $ScriptFunctions = $ScriptFunctions | Sort-Object -Descending -Property Name $ClassesFunctions = $ClassesFunctions | Sort-Object -Descending -Property Name } if ($ArrayIncludes.Count -gt 0) { $ArrayIncludes | Out-File -Append -LiteralPath $PSM1FilePath -Encoding utf8 } $Success = Get-ScriptsContentAndTryReplace -Files $ScriptFunctions -OutputPath $PSM1FilePath -DoNotAttemptToFixRelativePaths:$Configuration.Steps.BuildModule.DoNotAttemptToFixRelativePaths if ($Success -eq $false) { return $false } # 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) { $Success = Format-Code -FilePath $FilePathUsing -FormatCode $FormatCodePSM1 if ($Success -eq $false) { return $false } $Configuration.UsingInPlace = "$ModuleName.Usings.ps1" } $TimeToExecute.Stop() Write-Text "[+] Merging files into PSM1 [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] Detecting required modules" -Color Blue $RequiredModules = @( if ($Configuration.Information.Manifest.RequiredModules.Count -gt 0) { if ($Configuration.Information.Manifest.RequiredModules[0] -is [System.Collections.IDictionary]) { $Configuration.Information.Manifest.RequiredModules.ModuleName } else { $Configuration.Information.Manifest.RequiredModules } } if ($Configuration.Information.Manifest.ExternalModuleDependencies.Count -gt 0) { $Configuration.Information.Manifest.ExternalModuleDependencies } ) [Array] $ApprovedModules = $Configuration.Options.Merge.Integrate.ApprovedModules | Sort-Object -Unique $ModulesThatWillMissBecauseOfIntegrating = [System.Collections.Generic.List[string]]::new() [Array] $DependantRequiredModules = foreach ($Module in $RequiredModules) { [Array] $TemporaryDependant = Find-RequiredModules -Name $Module if ($TemporaryDependant.Count -gt 0) { if ($Module -in $ApprovedModules) { # 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 "[+] Detecting required modules [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] Searching for missing functions" -Color Blue $MissingFunctions = Get-MissingFunctions -FilePath $PSM1FilePath -SummaryWithCommands -ApprovedModules $ApprovedModules $TimeToExecute.Stop() Write-Text "[+] Searching for missing functions [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] Detecting commands used" -Color Blue #[Array] $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 } } $TimeToExecute.Stop() Write-Text "[+] Detecting commands used [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue Write-TextWithTime -Text "Pre-Verification of approved modules" { foreach ($ApprovedModule in $ApprovedModules) { $ApprovedModuleStatus = Get-Module -Name $ApprovedModule -ListAvailable if ($ApprovedModuleStatus) { Write-Text " [>] Approved module $ApprovedModule exists - can be used for merging." -Color Green } else { Write-Text " [>] Approved module $ApprovedModule doesn't exists. Potentially issue with merging." -Color Red } } } -PreAppend Plus $TerminateEarly = $false $Success = Write-TextWithTime -Text "Analyze required, approved modules" { foreach ($Module in $ModulesToCheck.Source | Sort-Object) { if ($Module -in $RequiredModules -and $Module -in $ApprovedModules) { Write-Text " [+] Module $Module is in required modules with ability to merge." -Color DarkYellow $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module }) foreach ($F in $MyFunctions) { if ($F.IsPrivate) { Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsPrivate: $($F.IsPrivate))" -Color Magenta } else { Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsPrivate: $($F.IsPrivate))" -Color DarkYellow } } } elseif ($Module -in $DependantRequiredModules -and $Module -in $ApprovedModules) { Write-Text " [+] Module $Module is in dependant required module within required modules with ability to merge." -Color DarkYellow $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module }) foreach ($F in $MyFunctions) { Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color DarkYellow } } elseif ($Module -in $DependantRequiredModules) { Write-Text " [+] Module $Module is in dependant required module within required modules." -Color DarkGray $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module }) foreach ($F in $MyFunctions) { Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color DarkGray } } elseif ($Module -in $RequiredModules) { Write-Text " [+] Module $Module is in required modules." -Color Green $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module }) foreach ($F in $MyFunctions) { Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color Green } } elseif ($Module -notin $RequiredModules -and $Module -in $ApprovedModules) { Write-Text " [+] Module $Module is missing in required module, but it's in approved modules." -Color Magenta $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module }) foreach ($F in $MyFunctions) { Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color Magenta } } else { [Array] $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module }) if ($Configuration.Options.Merge.ModuleSkip.Force -eq $true) { Write-Text " [-] Module $Module is missing in required modules. Non-critical issue as per configuration (force used)." -Color Gray foreach ($F in $MyFunctions) { Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate)). Ignored by configuration." -Color Gray } } else { if ($Module -in $Configuration.Options.Merge.ModuleSkip.IgnoreModuleName) { Write-Text " [-] Module $Module is missing in required modules. Non-critical issue as per configuration (skipped module)." -Color Gray foreach ($F in $MyFunctions) { Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate)). Ignored by configuration." -Color Gray } } else { $FoundProblem = $false foreach ($F in $MyFunctions) { if ($F.Name -notin $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) { $FoundProblem = $true } } if (-not $FoundProblem) { Write-Text " [-] Module $Module is missing in required modules. Non-critical issue as per configuration (skipped functions)." -Color Gray foreach ($F in $MyFunctions) { if ($F.Name -in $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) { Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate)). Ignored by configuration." -Color Gray } else { Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color Red } } } else { $TerminateEarly = $true Write-Text " [-] Module $Module is missing in required modules. Potential issue. Fix configuration required." -Color Red foreach ($F in $MyFunctions) { if ($F.Name -in $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) { Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate)). Ignored by configuration." -Color Gray } else { Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color Red } } } } } } } if ($CommandsWithoutModule.Count -gt 0) { $FoundProblem = $false foreach ($F in $CommandsWithoutModule) { if ($F.Name -notin $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) { $FoundProblem = $true } } if ($FoundProblem) { Write-Text " [-] Some commands couldn't be resolved to functions (private function maybe?). Potential issue." -Color Red foreach ($F in $CommandsWithoutModule) { if ($F.Name -notin $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) { $TerminateEarly = $true Write-Text " [>] Command affected $($F.Name) (Command Type: Unknown / IsAlias: $($F.IsAlias))" -Color Red } else { Write-Text " [>] Command affected $($F.Name) (Command Type: Unknown / IsAlias: $($F.IsAlias)). Ignored by configuration." -Color Gray } } } else { Write-Text " [-] Some commands couldn't be resolved to functions (private function maybe?). Non-critical issue as per configuration (skipped functions)." -Color Gray foreach ($F in $CommandsWithoutModule) { if ($F.Name -in $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) { Write-Text " [>] Command affected $($F.Name) (Command Type: Unknown / IsAlias: $($F.IsAlias)). Ignored by configuration." -Color Gray } else { # this shouldn't happen, but just in case 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 #} if ($TerminateEarly) { Write-Text " [-] Some commands are missing in required modules. Fix this issue or use New-ConfigurationModuleSkip to skip verification." -Color Red return $false } } -PreAppend Plus if ($Success -eq $false) { return $false } if ($Configuration.Steps.BuildModule.MergeMissing -eq $true) { if (Test-Path -LiteralPath $PSM1FilePath) { $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] Merge mergable commands" -Color Blue $PSM1Content = Get-Content -LiteralPath $PSM1FilePath -Raw -Encoding UTF8 $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 $TimeToExecute.Stop() Write-Text "[+] Merge mergable commands [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue } } $TimeToExecuteSign = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] Finalizing PSM1/PSD1" -Color Blue # lets set the defaults to disabled value if ($null -eq $Configuration.Steps.BuildModule.DebugDLL) { $Configuration.Steps.BuildModule.DebugDLL = $false } $LibraryContent = @( if ($LibrariesStandard.Count -gt 0) { foreach ($File in $LibrariesStandard) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $File $Output } } } elseif ($LibrariesCore.Count -gt 0 -and $LibrariesDefault.Count -gt 0) { 'if ($PSEdition -eq ''Core'') {' if ($LibrariesCore.Count -gt 0) { foreach ($File in $LibrariesCore) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $File $Output } } } '} else {' if ($LibrariesDefault.Count -gt 0) { foreach ($File in $LibrariesDefault) { $Extension = $File.Substring($File.Length - 4, 4) if ($Extension -eq '.dll') { $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $File $Output } } } '}' } ) # Add libraries (DLL) into separate file and either dot source it or load as script processing in PSD1 or both (for whatever reason) if ($LibraryContent.Count -gt 0) { if ($Configuration.Steps.BuildModule.LibrarySeparateFile -eq $true) { $LibariesPath = "$ModulePathTarget\$ModuleName.Libraries.ps1" $ScriptsToProcessLibrary = "$ModuleName.Libraries.ps1" } if ($Configuration.Steps.BuildModule.LibraryDotSource -eq $true) { $LibariesPath = "$ModulePathTarget\$ModuleName.Libraries.ps1" $DotSourcePath = ". `$PSScriptRoot\$ModuleName.Libraries.ps1" } if ($LibariesPath) { $LibraryContent | Out-File -Append -LiteralPath $LibariesPath -Encoding utf8 } } if ($ClassesFunctions.Count -gt 0) { $ClassesPath = "$ModulePathTarget\$ModuleName.Classes.ps1" $DotSourceClassPath = ". `$PSScriptRoot\$ModuleName.Classes.ps1" $Success = Get-ScriptsContentAndTryReplace -Files $ClassesFunctions -OutputPath $ClassesPath -DoNotAttemptToFixRelativePaths:$Configuration.Steps.BuildModule.DoNotAttemptToFixRelativePaths if ($Success -eq $false) { return $false } } # Adjust PSM1 file by adding dot sourcing or directly libraries to the PSM1 file if ($LibariesPath -gt 0 -or $ClassesPath -gt 0 -or $Configuration.Steps.BuildModule.ResolveBinaryConflicts) { $PSM1Content = Get-Content -LiteralPath $PSM1FilePath -Raw -Encoding UTF8 $IntegrateContent = @( # add resolve conflicting binary option if ($Configuration.Steps.BuildModule.ResolveBinaryConflicts -is [System.Collections.IDictionary]) { New-DLLResolveConflict -ProjectName $Configuration.Steps.BuildModule.ResolveBinaryConflicts.ProjectName } elseif ($Configuration.Steps.BuildModule.ResolveBinaryConflicts -eq $true) { New-DLLResolveConflict } if ($LibraryContent.Count -gt 0) { if ($DotSourcePath) { "# Dot source all libraries by loading external file" $DotSourcePath "" } if (-not $LibariesPath) { "# Load all types" $LibraryContent "" } } if ($ClassesPath) { "# Dot source all classes by loading external file" $DotSourceClassPath "" } $PSM1Content ) $IntegrateContent | Set-Content -LiteralPath $PSM1FilePath -Encoding UTF8 } if ($Configuration.Information.Manifest.DotNetFrameworkVersion) { Find-NetFramework -RequireVersion $Configuration.Information.Manifest.DotNetFrameworkVersion | Out-File -Append -LiteralPath $PSM1FilePath -Encoding UTF8 } # Finalize PSM1 by adding export functions/aliases and internal modules loading $Success = 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 if ($Success -eq $false) { return $false } # Format standard PSM1 file $Success = Format-Code -FilePath $PSM1FilePath -FormatCode $FormatCodePSM1 if ($Success -eq $false) { return $false } # Format libraries PS1 file if ($LibariesPath) { $Success = Format-Code -FilePath $LibariesPath -FormatCode $FormatCodePSM1 if ($Success -eq $false) { return $false } } # Build PSD1 file New-PersonalManifest -Configuration $Configuration -ManifestPath $PSD1FilePath -AddUsingsToProcess -ScriptsToProcessLibrary $ScriptsToProcessLibrary -OnMerge # Format PSD1 file $Success = Format-Code -FilePath $PSD1FilePath -FormatCode $FormatCodePSD1 if ($Success -eq $false) { return $false } # 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 "[+] 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-DLLCodeOutput { [CmdletBinding()] param( [string] $File, [bool] $DebugDLL ) if ($DebugDLL) { $Output = @" `$FoundErrors = @( try { `$ImportName = "`$PSScriptRoot\$File" Add-Type -Path `$ImportName -ErrorAction Stop } catch [System.Reflection.ReflectionTypeLoadException] { Write-Warning "Processing `$(`$ImportName) Exception: `$(`$_.Exception.Message)" `$LoaderExceptions = `$(`$_.Exception.LoaderExceptions) | Sort-Object -Unique foreach (`$E in `$LoaderExceptions) { Write-Warning "Processing `$(`$ImportName) LoaderExceptions: `$(`$E.Message)" } `$true } catch { Write-Warning "Processing `$(`$ImportName) Exception: `$(`$_.Exception.Message)" `$LoaderExceptions = `$(`$_.Exception.LoaderExceptions) | Sort-Object -Unique foreach (`$E in `$LoaderExceptions) { Write-Warning "Processing `$(`$ImportName) LoaderExceptions: `$(`$E.Message)" } `$true } ) if (`$FoundErrors.Count -gt 0) { Write-Warning "Importing module failed. Fix errors before continuing." break } "@ } else { $Output = 'Add-Type -Path $PSScriptRoot\' + $File } $Output } function New-DLLResolveConflict { [CmdletBinding()] param( [string] $ProjectName ) if ($ProjectName) { $StandardName = "'$ProjectName'" } else { $StandardName = '$myInvocation.MyCommand.Name.Replace(".psm1", "")' } $Output = @" # Get library name, from the PSM1 file name `$LibraryName = $StandardName `$Library = "`$LibraryName.dll" `$Class = "`$LibraryName.Initialize" `$AssemblyFolders = Get-ChildItem -Path `$PSScriptRoot\Lib -Directory -ErrorAction SilentlyContinue # Lets find which libraries we need to load `$Default = `$false `$Core = `$false `$Standard = `$false foreach (`$A in `$AssemblyFolders.Name) { if (`$A -eq 'Default') { `$Default = `$true } elseif (`$A -eq 'Core') { `$Core = `$true } elseif (`$A -eq 'Standard') { `$Standard = `$true } } if (`$Standard -and `$Core -and `$Default) { `$FrameworkNet = 'Default' `$Framework = 'Standard' } elseif (`$Standard -and `$Core) { `$Framework = 'Standard' `$FrameworkNet = 'Standard' } elseif (`$Core -and `$Default) { `$Framework = 'Core' `$FrameworkNet = 'Default' } elseif (`$Standard -and `$Default) { `$Framework = 'Standard' `$FrameworkNet = 'Default' } elseif (`$Standard) { `$Framework = 'Standard' `$FrameworkNet = 'Standard' } elseif (`$Core) { `$Framework = 'Core' `$FrameworkNet = '' } elseif (`$Default) { `$Framework = '' `$FrameworkNet = 'Default' } else { Write-Error -Message 'No assemblies found' } if (`$PSEdition -eq 'Core') { `$LibFolder = `$Framework } else { `$LibFolder = `$FrameworkNet } try { `$ImportModule = Get-Command -Name Import-Module -Module Microsoft.PowerShell.Core if (-not (`$Class -as [type])) { & `$ImportModule ([IO.Path]::Combine(`$PSScriptRoot, 'Lib', `$LibFolder, `$Library)) -ErrorAction Stop } else { `$Type = "`$Class" -as [Type] & `$importModule -Force -Assembly (`$Type.Assembly) } } catch { Write-Warning -Message "Importing module `$Library failed. Fix errors before continuing. Error: `$(`$_.Exception.Message)" `$true } "@ $Output } function New-PersonalManifest { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [string] $ManifestPath, [switch] $AddScriptsToProcess, [switch] $AddUsingsToProcess, [string] $ScriptsToProcessLibrary, [switch] $UseWildcardForFunctions, [switch] $OnMerge ) $TemporaryManifest = [ordered] @{ } $Manifest = $Configuration.Information.Manifest if ($UseWildcardForFunctions) { $Manifest.FunctionsToExport = @("*") $Manifest.AliasesToExport = @("*") } $Manifest.Path = $ManifestPath if (-not $AddScriptsToProcess) { $Manifest.ScriptsToProcess = @() } if ($AddUsingsToProcess -and $Configuration.UsingInPlace -and -not $ScriptsToProcessLibrary) { $Manifest.ScriptsToProcess = @($Configuration.UsingInPlace) } elseif ($AddUsingsToProcess -and $Configuration.UsingInPlace -and $ScriptsToProcessLibrary) { $Manifest.ScriptsToProcess = @($Configuration.UsingInPlace, $ScriptsToProcessLibrary) } elseif ($ScriptsToProcessLibrary) { $Manifest.ScriptsToProcess = @($ScriptsToProcessLibrary) } if ($Manifest.Contains('ExternalModuleDependencies')) { $TemporaryManifest.ExternalModuleDependencies = $Manifest.ExternalModuleDependencies $Manifest.Remove('ExternalModuleDependencies') } if ($Manifest.Contains('InternalModuleDependencies')) { $TemporaryManifest.InternalModuleDependencies = $Manifest.InternalModuleDependencies $Manifest.Remove('InternalModuleDependencies') } if ($Manifest.Contains('CommandModuleDependencies')) { $TemporaryManifest.CommandModuleDependencies = $Manifest.CommandModuleDependencies $Manifest.Remove('CommandModuleDependencies') } if ($Manifest.PreRelease) { $Configuration.CurrentSettings.PreRelease = $Manifest.PreRelease } if ($OnMerge) { if ($Configuration.Options.Merge.Style.PSD1) { $PSD1Style = $Configuration.Options.Merge.Style.PSD1 } } else { if ($Configuration.Options.Standard.Style.PSD1) { $PSD1Style = $Configuration.Options.Standard.Style.PSD1 } } if (-not $PSD1Style) { if ($Configuration.Options.Style.PSD1) { $PSD1Style = $Configuration.Options.Style.PSD1 } else { $PSD1Style = 'Minimal' } } if ($PSD1Style -eq 'Native' -and $Configuration.Steps.PublishModule.Prerelease -eq '' -and (-not $TemporaryManifest.ExternalModuleDependencies)) { if ($Manifest.ModuleVersion) { New-ModuleManifest @Manifest } else { Write-Text -Text '[-] Module version is not available. Terminating.' -Color Red return $false } Write-TextWithTime -Text "[i] Converting $($ManifestPath) UTF8 without BOM" { (Get-Content -Path $ManifestPath -Raw -Encoding utf8) | Out-FileUtf8NoBom $ManifestPath } } else { if ($PSD1Style -eq 'Native') { Write-Text -Text '[-] Native PSD1 style is not available when using PreRelease or ExternalModuleDependencies. Switching to Minimal.' -Color Yellow } 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') } $ValidateEntriesPrivateData = @('Tags', 'LicenseUri', 'ProjectURI', 'IconUri', 'ReleaseNotes', 'Prerelease', 'RequireLicenseAcceptance', 'ExternalModuleDependencies') foreach ($Entry in [string[]] $Data.Keys) { if ($Entry -in $ValidateEntriesPrivateData) { $Data.PrivateData.PSData.$Entry = $Data.$Entry $Data.Remove($Entry) } } $ValidDataEntries = @('ModuleToProcess', 'NestedModules', 'GUID', 'Author', 'CompanyName', 'Copyright', 'ModuleVersion', 'Description', 'PowerShellVersion', 'PowerShellHostName', 'PowerShellHostVersion', 'CLRVersion', 'DotNetFrameworkVersion', 'ProcessorArchitecture', 'RequiredModules', 'TypesToProcess', 'FormatsToProcess', 'ScriptsToProcess', 'PrivateData', 'RequiredAssemblies', 'ModuleList', 'FileList', 'FunctionsToExport', 'VariablesToExport', 'AliasesToExport', 'CmdletsToExport', 'DscResourcesToExport', 'CompatiblePSEditions', 'HelpInfoURI', 'RootModule', 'DefaultCommandPrefix') foreach ($Entry in [string[]] $Data.Keys) { if ($Entry -notin $ValidDataEntries) { Write-Text -Text "[-] Removing wrong entries from PSD1 - $Entry" -Color Red $Data.Remove($Entry) } } foreach ($Entry in [string[]] $Data.PrivateData.PSData.Keys) { if ($Entry -notin $ValidateEntriesPrivateData) { Write-Text -Text "[-] Removing wrong entries from PSD1 Private Data - $Entry" -Color Red $Data.PrivateData.PSData.Remove($Entry) } } # Old way of setting prerelease 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) { if ($Module -is [System.Collections.IDictionary]) { # Lets rewrite module to retain proper order always $Module = [ordered] @{ ModuleName = $Module.ModuleName ModuleVersion = $Module.ModuleVersion Guid = $Module.Guid } Remove-EmptyValue -Hashtable $Module $Module } else { $Module } } foreach ($Module in $TemporaryManifest.ExternalModuleDependencies) { if ($Module -is [System.Collections.IDictionary]) { # Lets rewrite module to retain proper order always $Module = [ordered] @{ ModuleName = $Module.ModuleName ModuleVersion = $Module.ModuleVersion Guid = $Module.Guid } Remove-EmptyValue -Hashtable $Module $Module } else { $Module } } ) } if (-not $Data.RequiredModules) { $Data.Remove('RequiredModules') } $Data | Export-PSData -DataFile $ManifestPath -Sort } } function New-PrepareManifest { [CmdletBinding()] param( [string] $ProjectName, [string] $ModulePath, [string] $ProjectPath, $FunctionToExport, [string] $ProjectUrl ) Set-Location "$projectPath\$ProjectName" $manifest = @{ Path = ".\$ProjectName.psd1" RootModule = "$ProjectName.psm1" Author = 'Przemyslaw Klys' CompanyName = 'Evotec' Copyright = 'Evotec (c) 2011-2022. All rights reserved.' Description = "Simple project" FunctionsToExport = $functionToExport CmdletsToExport = '' VariablesToExport = '' AliasesToExport = '' FileList = "$ProjectName.psm1", "$ProjectName.psd1" HelpInfoURI = $projectUrl ProjectUri = $projectUrl } New-ModuleManifest @manifest } function New-PrepareStructure { [CmdletBinding()] param( [System.Collections.IDictionary]$Configuration, [scriptblock] $Settings, [string] $PathToProject, [string] $ModuleName ) # Lets precreate structure if it's not available if (-not $Configuration) { $Configuration = [ordered] @{} } if (-not $Configuration.Information) { $Configuration.Information = [ordered] @{} } if (-not $Configuration.Information.Manifest) { # if it's not provided, we try to get it from PSD1 file $PathToPSD1 = [io.path]::Combine($PathToProject, $ModuleName + '.psd1') if (Test-Path -LiteralPath $PathToPSD1) { try { $Configuration.Information.Manifest = Import-PowerShellDataFile -Path $PathToPSD1 -ErrorAction Stop # lets reset whatever is in PSD1 that we load $Configuration.Information.Manifest.RequiredModules = $null } catch { Write-Text "[-] Reading $PathToPSD1 failed. Error: $($_.Exception.Message)" -Color Red return $false } } else { $Configuration.Information.Manifest = [ordered] @{} } } # This deals with OneDrive redirection or similar if (-not $Configuration.Information.DirectoryModulesCore) { $Configuration.Information.DirectoryModulesCore = "$([Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments))\PowerShell\Modules" } # This deals with OneDrive redirection or similar if (-not $Configuration.Information.DirectoryModules) { $Configuration.Information.DirectoryModules = "$([Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments))\WindowsPowerShell\Modules" } # This is to use within module between different stages # kind of temporary settings storage if (-not $Configuration.CurrentSettings) { $Configuration.CurrentSettings = [ordered] @{} } if (-not $Configuration.CurrentSettings['Artefact']) { $Configuration.CurrentSettings['Artefact'] = @() } if ($ModuleName) { $Configuration.Information.ModuleName = $ModuleName } if ($ExcludeFromPackage) { $Configuration.Information.Exclude = $ExcludeFromPackage } if ($IncludeRoot) { $Configuration.Information.IncludeRoot = $IncludeRoot } if ($IncludePS1) { $Configuration.Information.IncludePS1 = $IncludePS1 } if ($IncludeAll) { $Configuration.Information.IncludeAll = $IncludeAll } if ($IncludeCustomCode) { $Configuration.Information.IncludeCustomCode = $IncludeCustomCode } if ($IncludeToArray) { $Configuration.Information.IncludeToArray = $IncludeToArray } if ($LibrariesCore) { $Configuration.Information.LibrariesCore = $LibrariesCore } if ($LibrariesDefault) { $Configuration.Information.LibrariesDefault = $LibrariesDefault } if ($LibrariesStandard) { $Configuration.Information.LibrariesStandard = $LibrariesStandard } if ($DirectoryProjects) { $Configuration.Information.DirectoryProjects = $Path } if ($FunctionsToExportFolder) { $Configuration.Information.FunctionsToExport = $FunctionsToExportFolder } if ($AliasesToExportFolder) { $Configuration.Information.AliasesToExport = $AliasesToExportFolder } if (-not $Configuration.Options) { $Configuration.Options = [ordered] @{} } if (-not $Configuration.Options.Merge) { $Configuration.Options.Merge = [ordered] @{} } if (-not $Configuration.Options.Merge.Integrate) { $Configuration.Options.Merge.Integrate = [ordered] @{} } if (-not $Configuration.Options.Standard) { $Configuration.Options.Standard = [ordered] @{} } if (-not $Configuration.Options.Signing) { $Configuration.Options.Signing = [ordered] @{} } if (-not $Configuration.Steps) { $Configuration.Steps = [ordered] @{} } if (-not $Configuration.Steps.PublishModule) { $Configuration.Steps.PublishModule = [ordered] @{} } if (-not $Configuration.Steps.ImportModules) { $Configuration.Steps.ImportModules = [ordered] @{} } if (-not $Configuration.Steps.BuildModule) { $Configuration.Steps.BuildModule = [ordered] @{} } if (-not $Configuration.Steps.BuildModule.Releases) { $Configuration.Steps.BuildModule.Releases = [ordered] @{} } if (-not $Configuration.Steps.BuildModule.ReleasesUnpacked) { $Configuration.Steps.BuildModule.ReleasesUnpacked = [ordered] @{} } if (-not $Configuration.Steps.BuildLibraries) { $Configuration.Steps.BuildLibraries = [ordered] @{} } if (-not $Configuration.Information.Manifest.CommandModuleDependencies) { $Configuration.Information.Manifest.CommandModuleDependencies = [ordered] @{} } if (-not $Configuration.Steps.BuildModule.Artefacts) { $Configuration.Steps.BuildModule.Artefacts = [System.Collections.Generic.List[System.Collections.IDictionary]]::new() } if (-not $Configuration.Steps.BuildModule.GitHubNugets) { $Configuration.Steps.BuildModule.GitHubNugets = [System.Collections.Generic.List[System.Collections.IDictionary]]::new() } if (-not $Configuration.Steps.BuildModule.GalleryNugets) { $Configuration.Steps.BuildModule.GalleryNugets = [System.Collections.Generic.List[System.Collections.IDictionary]]::new() } # Fix required fields: $Configuration.Information.Manifest.RootModule = "$($ModuleName).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 = @() Write-TextWithTime -Text "Reading configuration" { if ($Settings) { $ExecutedSettings = & $Settings foreach ($Setting in $ExecutedSettings) { if ($Setting.Type -eq 'RequiredModule') { if ($Configuration.Information.Manifest.RequiredModules -isnot [System.Collections.Generic.List[System.Object]]) { $Configuration.Information.Manifest.RequiredModules = [System.Collections.Generic.List[System.Object]]::new() } $Configuration.Information.Manifest.RequiredModules.Add($Setting.Configuration) } elseif ($Setting.Type -eq 'ExternalModule') { if ($Configuration.Information.Manifest.ExternalModuleDependencies -isnot [System.Collections.Generic.List[System.Object]]) { $Configuration.Information.Manifest.ExternalModuleDependencies = [System.Collections.Generic.List[System.Object]]::new() } $Configuration.Information.Manifest.ExternalModuleDependencies.Add($Setting.Configuration) } elseif ($Setting.Type -eq 'ApprovedModule') { if ($Configuration.Options.Merge.Integrate.ApprovedModules -isnot [System.Collections.Generic.List[System.Object]]) { $Configuration.Options.Merge.Integrate.ApprovedModules = [System.Collections.Generic.List[System.Object]]::new() } $Configuration.Options.Merge.Integrate.ApprovedModules.Add($Setting.Configuration) } elseif ($Setting.Type -eq 'ModuleSkip') { $Configuration.Options.Merge.ModuleSkip = $Setting.Configuration } elseif ($Setting.Type -eq 'Manifest') { foreach ($Key in $Setting.Configuration.Keys) { $Configuration.Information.Manifest[$Key] = $Setting.Configuration[$Key] } } elseif ($Setting.Type -eq 'Information') { foreach ($Key in $Setting.Configuration.Keys) { $Configuration.Information[$Key] = $Setting.Configuration[$Key] } } elseif ($Setting.Type -eq 'Formatting') { foreach ($Key in $Setting.Options.Keys) { if (-not $Configuration.Options[$Key]) { $Configuration.Options[$Key] = [ordered] @{} } foreach ($Entry in $Setting.Options[$Key].Keys) { $Configuration.Options[$Key][$Entry] = $Setting.Options[$Key][$Entry] } } } elseif ($Setting.Type -eq 'Command') { $Configuration.Information.Manifest.CommandModuleDependencies[$Setting.Configuration.ModuleName] = @($Setting.Configuration.CommandName) } elseif ($Setting.Type -eq 'Documentation') { $Configuration.Options.Documentation = $Setting.Configuration } elseif ($Setting.Type -eq 'BuildDocumentation') { $Configuration.Steps.BuildDocumentation = $Setting.Configuration #} elseif ($Setting.Type -eq 'GitHub') { #} elseif ($Setting.Type -eq 'PowerShellGallery') { #} elseif ($Setting.Type -eq 'PowerShellGalleryPublishing') { } elseif ($Setting.Type -eq 'TestsBeforeMerge') { $Configuration.Options.TestsBeforeMerge = $Setting.Configuration } elseif ($Setting.Type -eq 'TestsAfterMerge') { $Configuration.Options.TestsAfterMerge = $Setting.Configuration } elseif ($Setting.Type -eq 'GitHubPublishing') { $Configuration.Steps.BuildModule.Nugets.Add($Setting.Configuration) #} elseif ($Setting.Type -eq 'ImportModules') { #} elseif ($Setting.Type -eq 'Releases') { } elseif ($Setting.Type -in 'GalleryNuget') { $Configuration.Steps.BuildModule.GalleryNugets.Add($Setting.Configuration) } elseif ($Setting.Type -in 'GitHubNuget') { $Configuration.Steps.BuildModule.GitHubNugets.Add($Setting.Configuration) } elseif ($Setting.Type -in 'Unpacked', 'Packed', 'Script', 'ScriptPacked') { $Configuration.Steps.BuildModule.Artefacts.Add($Setting.Configuration) } elseif ($Setting.Type -eq 'Build') { foreach ($Key in $Setting.BuildModule.Keys) { $Configuration.Steps.BuildModule[$Key] = $Setting.BuildModule[$Key] } } elseif ($Setting.Type -eq 'BuildLibraries') { foreach ($Key in $Setting.BuildLibraries.Keys) { $Configuration.Steps.BuildLibraries[$Key] = $Setting.BuildLibraries[$Key] } } elseif ($Setting.Type -eq 'Options') { foreach ($Key in $Setting.Options.Keys) { if (-not $Configuration.Options[$Key]) { $Configuration.Options[$Key] = [ordered] @{} } foreach ($Entry in $Setting.Options[$Key].Keys) { $Configuration.Options[$Key][$Entry] = $Setting.Options[$Key][$Entry] } } } } } } -PreAppend Information # lets set some defaults if (-not $Configuration.Options.Merge.Sort) { $Configuration.Options.Merge.Sort = 'None' } if (-not $Configuration.Options.Standard.Sort) { $Configuration.Options.Standard.Sort = 'None' } # We build module or do other stuff with it $Success = Start-ModuleBuilding -Configuration $Configuration -PathToProject $PathToProject if ($Success -eq $false) { return $false } } 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 ) Write-TextWithTime -Text "Adding alises/functions to load in PSM1 file - $Path" -PreAppend Plus { if ($FunctionNames.Count -gt 0) { $Functions = ($FunctionNames | Sort-Object -Unique) -join "','" $Functions = "'$Functions'" } else { $Functions = @() } if ($FunctionAliaes.Count -gt 0) { $Aliases = ($FunctionAliaes | Sort-Object -Unique) -join "','" $Aliases = "'$Aliases'" } else { $Aliases = @() } "" | Out-File -Append -LiteralPath $Path -Encoding utf8 # This allows for loading modules in PSM1 file directly if ($InternalModuleDependencies.Count -gt 0) { @( "# Added internal module loading to cater for special cases " "" ) | Out-File -Append -LiteralPath $Path -Encoding utf8 $ModulesText = "'$($InternalModuleDependencies -join "','")'" @" `$ModulesOptional = $ModulesText foreach (`$Module in `$ModulesOptional) { Import-Module -Name `$Module -ErrorAction SilentlyContinue } "@ | Out-File -Append -LiteralPath $Path -Encoding utf8 } # This allows to export functions only if module loading works correctly if ($CommandModuleDependencies -and $CommandModuleDependencies.Keys.Count -gt 0) { @( "`$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) "@ ) | Out-File -Append -LiteralPath $Path -Encoding utf8 } else { # this loads functions/aliases as designed #"" | Out-File -Append -LiteralPath $Path -Encoding utf8 "# Export functions and aliases as required" | Out-File -Append -LiteralPath $Path -Encoding utf8 "Export-ModuleMember -Function @($Functions) -Alias @($Aliases)" | Out-File -Append -LiteralPath $Path -Encoding utf8 } } -SpacesBefore ' ' } function Out-FileUtf8NoBom { <# .SYNOPSIS Outputs to a UTF-8-encoded file *without a BOM* (byte-order mark). .DESCRIPTION Mimics the most important aspects of Out-File: * Input objects are sent to Out-String first. * -Append allows you to append to an existing file, -NoClobber prevents overwriting of an existing file. * -Width allows you to specify the line width for the text representations of input objects that aren't strings. However, it is not a complete implementation of all Out-String parameters: * Only a literal output path is supported, and only as a parameter. * -Force is not supported. Caveat: *All* pipeline input is buffered before writing output starts, but the string representations are generated and written to the target file one by one. .NOTES The raison d'être for this advanced function is that, as of PowerShell v5, Out-File still lacks the ability to write UTF-8 files without a BOM: using -Encoding UTF8 invariably prepends a BOM. #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string] $LiteralPath, [switch] $Append, [switch] $NoClobber, [AllowNull()] [int] $Width, [Parameter(ValueFromPipeline)] $InputObject ) # 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 Register-DataForInitialModule { [CmdletBinding()] param( [Parameter(Mandatory)][string] $FilePath, [Parameter(Mandatory)][string] $ModuleName, [Parameter(Mandatory)][string] $Guid ) $BuildModule = Get-Content -Path $FilePath -Raw $BuildModule = $BuildModule -replace "\`$GUID", $Guid $BuildModule = $BuildModule -replace "\`$ModuleName", $ModuleName Set-Content -Path $FilePath -Value $BuildModule -Encoding utf8 } function Remove-Directory { [CmdletBinding()] param ( [string] $Directory ) if ($Directory) { $Exists = Test-Path -LiteralPath $Directory if ($Exists) { try { Remove-Item -Path $Directory -Confirm:$false -Recurse -Force -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message Write-Text "[e] Can't delete folder $Directory. Fix error before continuing: $ErrorMessage" -Color Red return $false } } } } function Remove-EmptyValue { [alias('Remove-EmptyValues')] [CmdletBinding()] param( [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable, [string[]] $ExcludeParameter, [switch] $Recursive, [int] $Rerun, [switch] $DoNotRemoveNull, [switch] $DoNotRemoveEmpty, [switch] $DoNotRemoveEmptyArray, [switch] $DoNotRemoveEmptyDictionary ) foreach ($Key in [string[]] $Hashtable.Keys) { if ($Key -notin $ExcludeParameter) { if ($Recursive) { if ($Hashtable[$Key] -is [System.Collections.IDictionary]) { if ($Hashtable[$Key].Count -eq 0) { if (-not $DoNotRemoveEmptyDictionary) { $Hashtable.Remove($Key) } } else { Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } } if ($Rerun) { for ($i = 0; $i -lt $Rerun; $i++) { Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive } } } function Remove-ItemAlternative { <# .SYNOPSIS Removes all files and folders within given path .DESCRIPTION Removes all files and folders within given path. Workaround for Access to the cloud file is denied issue .PARAMETER Path Path to file/folder .PARAMETER SkipFolder Do not delete top level folder .PARAMETER Exclude Skip files/folders matching given pattern .EXAMPLE Remove-ItemAlternative -Path "C:\Support\GitHub\GpoZaurr\Docs" .EXAMPLE Remove-ItemAlternative -Path "C:\Support\GitHub\GpoZaurr\Docs" .NOTES General notes #> [cmdletbinding()] param( [alias('LiteralPath')][string] $Path, [switch] $SkipFolder, [string[]] $Exclude ) if ($Path -and (Test-Path -LiteralPath $Path)) { $getChildItemSplat = @{ Path = $Path Recurse = $true Force = $true File = $true Exclude = $Exclude } Remove-EmptyValue -Hashtable $getChildItemSplat $Items = Get-ChildItem @getChildItemSplat foreach ($Item in $Items) { try { $Item.Delete() } catch { Write-Warning "Remove-ItemAlternative - Couldn't delete $($Item.FullName), error: $($_.Exception.Message)" } } $getChildItemSplat = @{ Path = $Path Recurse = $true Force = $true Exclude = $Exclude } Remove-EmptyValue -Hashtable $getChildItemSplat $Items = Get-ChildItem @getChildItemSplat | Sort-Object -Descending -Property 'FullName' foreach ($Item in $Items) { try { $Item.Delete() } catch { Write-Warning "Remove-ItemAlternative - Couldn't delete $($Item.FullName), error: $($_.Exception.Message)" } } if (-not $SkipFolder.IsPresent) { $Item = Get-Item -LiteralPath $Path try { $Item.Delete($true) } catch { 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 Send-FilesToGitHubRelease { [cmdletbinding()] param( [string[]] $filePathsToUpload, [string] $urlToUploadFilesTo, $authHeader ) [int] $numberOfFilesToUpload = $filePathsToUpload.Count [int] $numberOfFilesUploaded = 0 $filePathsToUpload | ForEach-Object { $filePath = $_ $fileName = Get-Item $filePath | Select-Object -ExpandProperty Name $uploadAssetWebRequestParameters = @{ # 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 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 Copy-Item -Path $Path2 -Destination $Path -Force -Recurse -Confirm:$false #$null = cmd /c mklink $path $path2 } } function Start-ArtefactsBuilding { [CmdletBinding()] param( [System.Collections.IDictionary] $ChosenArtefact, [System.Collections.IDictionary] $Configuration, [string] $FullProjectPath, [System.Collections.IDictionary] $DestinationPaths, [ValidateSet('ReleasesUnpacked', 'Releases')][string] $Type ) if ($Artefact) { $Artefact = $ChosenArtefact $ChosenType = $Artefact.Type $ID = if ($ChosenArtefact.ID) { $ChosenArtefact.ID } else { $null } } elseif ($Type) { if ($Configuration.Steps.BuildModule.$Type) { $Artefact = $Configuration.Steps.BuildModule.$Type $ChosenType = $Type } else { $Artefact = $null } $ID = $null } else { $ID = $null $Artefact = $null } if ($ID) { $TextToDisplay = "Preparing Artefact of type '$ChosenType' (ID: $ID)" } else { $TextToDisplay = "Preparing Artefact of type '$ChosenType'" } # If artefact is not enabled, we don't want to do anything if ($null -eq $Artefact -or $Artefact.Count -eq 0) { return } Write-TextWithTime -Text $TextToDisplay -PreAppend Information -SpacesBefore ' ' { if ($Artefact -or $Artefact.Enabled) { if ($Artefact -is [System.Collections.IDictionary]) { if ($Artefact.Path) { if ($Artefact.Relative -eq $false) { $FolderPathReleases = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Artefact.Path) } else { $FolderPathReleases = [System.IO.Path]::Combine($FullProjectPath, $Artefact.Path) } } else { $FolderPathReleases = [System.IO.Path]::Combine($FullProjectPath, $Type) } } else { # default values $FolderPathReleases = [System.IO.Path]::Combine($FullProjectPath, $Type) } if ($Artefact.RequiredModules.ModulesPath) { $DirectPathForPrimaryModule = $Artefact.RequiredModules.ModulesPath } elseif ($Artefact.RequiredModules.Path) { $DirectPathForPrimaryModule = $Artefact.RequiredModules.Path } elseif ($Artefact.Path) { $DirectPathForPrimaryModule = $Artefact.Path } else { $DirectPathForPrimaryModule = $FolderPathReleases } if ($Artefact.RequiredModules.Path) { $DirectPathForRequiredModules = $Artefact.RequiredModules.Path } elseif ($Artefact.RequiredModules.ModulesPath) { $DirectPathForRequiredModules = $Artefact.RequiredModules.ModulesPath } elseif ($Artefact.Path) { $DirectPathForRequiredModules = $Artefact.Path } else { $DirectPathForRequiredModules = $FolderPathReleases } if ($Artefact -eq $true -or $Artefact.Enabled) { if ($Artefact -is [System.Collections.IDictionary]) { if ($DirectPathForPrimaryModule) { if ($Artefact.Relative -eq $false) { $CurrentModulePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DirectPathForPrimaryModule) } else { $CurrentModulePath = [System.IO.Path]::Combine($FullProjectPath, $DirectPathForPrimaryModule) } } else { $CurrentModulePath = [System.IO.Path]::Combine($FullProjectPath, $ChosenType) } if ($DirectPathForRequiredModules) { if ($Artefact.Relative -eq $false) { $RequiredModulesPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DirectPathForRequiredModules) } else { $RequiredModulesPath = [System.IO.Path]::Combine($FullProjectPath, $DirectPathForRequiredModules) } } else { $RequiredModulesPath = $ArtefactsPath } $ArtefactsPath = $Artefact.Path } else { # default values $ArtefactsPath = [System.IO.Path]::Combine($FullProjectPath, $ChosenType, $TagName) $RequiredModulesPath = $ArtefactsPath $CurrentModulePath = $ArtefactsPath } # we try to set some defaults just in case some settings are not available (mainly because user is using non-DSL model) # this is to make sure that if user is using relative paths we can still use them for copying files/folders if ($null -eq $Artefact.DestinationFilesRelative) { if ($null -ne $Artefact.Relative) { $Artefact.DestinationFilesRelative = $Artefact.Relative } } if ($null -eq $Artefact.DestinationDirectoriesRelative) { if ($null -ne $Artefact.Relative) { $Artefact.DestinationDirectoriesRelative = $Artefact.Relative } } $SplatArtefact = @{ ModuleName = $Configuration.Information.ModuleName ModuleVersion = $Configuration.Information.Manifest.ModuleVersion LegacyName = if ($Artefact -is [bool]) { $true } else { $false } CopyMainModule = $true CopyRequiredModules = $Artefact.RequiredModules -eq $true -or $Artefact.RequiredModules.Enabled ProjectPath = $FullProjectPath Destination = $ArtefactsPath DestinationMainModule = $CurrentModulePath DestinationRequiredModules = $RequiredModulesPath RequiredModules = $Configuration.Information.Manifest.RequiredModules Files = $Artefact.FilesOutput Folders = $Artefact.DirectoryOutput DestinationFilesRelative = $Artefact.DestinationFilesRelative DestinationDirectoriesRelative = $Artefact.DestinationDirectoriesRelative Configuration = $Configuration IncludeTag = $Artefact.IncludeTagName ArtefactName = $Artefact.ArtefactName ZipIt = if ($ChosenType -in 'Packed', 'Releases', 'ScriptPacked') { $true } else { $false } ConvertToScript = if ($ChosenType -in 'ScriptPacked', 'Script') { $true } else { $false } DestinationZip = $CurrentModulePath ScriptMerge = $Artefact.ScriptMerge ID = if ($ChosenArtefact.ID) { $ChosenArtefact.ID } else { $null } } Remove-EmptyValue -Hashtable $SplatArtefact Add-Artefact @SplatArtefact } } } -ColorBefore Yellow -ColorTime Yellow -Color Yellow } function Start-DocumentationBuilding { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [string] $FullProjectPath, [string] $ProjectName ) # 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-ItemAlternative -Path $DocumentationPath -SkipFolder [Array] $Files = Get-ChildItem -Path $DocumentationPath } } if ($Files.Count -eq 0) { try { $null = New-MarkdownHelp -Module $ProjectName -WithModulePage -OutputFolder $DocumentationPath -ErrorAction Stop -WarningVariable +WarningVariablesMarkdown -WarningAction SilentlyContinue -ExcludeDontShow } catch { Write-Text "[-] Documentation warning: $($_.Exception.Message)" -Color Yellow } $null = Move-Item -Path "$DocumentationPath\$ProjectName.md" -Destination $ReadMePath -ErrorAction SilentlyContinue #Start-Sleep -Seconds 1 # this is temporary workaround - due to diff output on update if ($Configuration.Steps.BuildDocumentation.UpdateWhenNew) { try { $null = Update-MarkdownHelpModule $DocumentationPath -RefreshModulePage -ModulePagePath $ReadMePath -ErrorAction Stop -WarningVariable +WarningVariablesMarkdown -WarningAction SilentlyContinue -ExcludeDontShow } catch { Write-Text "[-] Documentation warning: $($_.Exception.Message)" -Color Yellow } } } foreach ($_ in $WarningVariablesMarkdown) { Write-Text "[-] Documentation warning: $_" -Color Yellow } } } } function Start-ImportingModules { [CmdletBinding()] param( [string] $ProjectName, [System.Collections.IDictionary] $Configuration ) $TemporaryVerbosePreference = $VerbosePreference if ($null -ne $ImportModules.Verbose) { $VerbosePreference = $true } else { $VerbosePreference = $false } if ($Configuration.Steps.ImportModules.RequiredModules) { Write-TextWithTime -Text 'Importing modules (as defined in dependencies)' { foreach ($Module in $Configuration.Information.Manifest.RequiredModules) { if ($Module -is [System.Collections.IDictionary]) { Write-Text " [>] Importing required module - $($Module.ModuleName)" -Color Yellow if ($Module.ModuleVersion) { Import-Module -Name $Module.ModuleName -MinimumVersion $Module.ModuleVersion -Force -ErrorAction Stop -Verbose:$VerbosePreference } elseif ($Module.ModuleName) { Import-Module -Name $Module.ModuleName -Force -ErrorAction Stop -Verbose:$VerbosePreference } } elseif ($Module -is [string]) { Write-Text " [>] Importing required module - $($Module)" -Color Yellow Import-Module -Name $Module -Force -ErrorAction Stop -Verbose:$VerbosePreference } } } -PreAppend 'Information' } if ($Configuration.Steps.ImportModules.Self) { Write-TextWithTime -Text 'Importing module - SELF' { Import-Module -Name $ProjectName -Force -ErrorAction Stop -Verbose:$VerbosePreference } -PreAppend 'Information' } $VerbosePreference = $TemporaryVerbosePreference } function Start-LibraryBuilding { [CmdletBinding()] param( [string] $ModuleName, [string] $RootDirectory, [string] $Version, [System.Collections.IDictionary] $LibraryConfiguration ) if ($LibraryConfiguration.Count -eq 0) { return } if ($LibraryConfiguration.Enable -ne $true) { return } $TranslateFrameworks = [ordered] @{ 'NetStandard2.0' = 'Standard' 'netStandard2.1' = 'Standard' 'net472' = 'Default' 'net48' = 'Default' 'net470' = 'Default' 'netcoreapp3.1' = 'Core' } if ($LibraryConfiguration.Configuration) { $Configuration = $LibraryConfiguration.Configuration } else { $Configuration = 'Release' } if ($LibraryConfiguration.ProjectName) { $ModuleName = $LibraryConfiguration.ProjectName } $ModuleProjectFile = [System.IO.Path]::Combine($RootDirectory, "Sources", $ModuleName, "$ModuleName.csproj") $SourceFolder = [System.IO.Path]::Combine($RootDirectory, "Sources", $ModuleName) $ModuleBinFolder = [System.IO.Path]::Combine($RootDirectory, "Lib") if (Test-Path -LiteralPath $ModuleBinFolder) { $Items = Get-ChildItem -LiteralPath $ModuleBinFolder -Recurse -Force $Items | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue } $null = New-Item -Path $ModuleBinFolder -ItemType Directory -Force Push-Location -Path $SourceFolder [xml] $ProjectInformation = Get-Content -Raw -LiteralPath $ModuleProjectFile -Encoding UTF8 $SupportedFrameworks = foreach ($PropertyGroup in $ProjectInformation.Project.PropertyGroup) { if ($PropertyGroup.TargetFrameworks) { $PropertyGroup.TargetFrameworks -split ";" } } foreach ($Framework in $TranslateFrameworks.Keys) { if ($SupportedFrameworks.Contains($Framework.ToLower()) -and $LibraryConfiguration.Framework.Contains($Framework.ToLower())) { Write-Text "[+] Building $Framework ($Configuration)" dotnet publish --configuration $Configuration --verbosity q -nologo -p:Version=$Version --framework $Framework if ($LASTEXITCODE) { Write-Host # This is to add new line, because the first line was opened up. Write-Text "[-] Building $Framework - failed. Error: $LASTEXITCODE" -Color Red Exit } } else { continue } $PublishDirFolder = [System.IO.Path]::Combine($SourceFolder, "bin", $Configuration, $Framework, "publish", "*") $ModuleBinFrameworkFolder = [System.IO.Path]::Combine($ModuleBinFolder, $TranslateFrameworks[$Framework]) New-Item -Path $ModuleBinFrameworkFolder -ItemType Directory -ErrorAction SilentlyContinue | Out-Null try { Copy-Item -Path $PublishDirFolder -Destination $ModuleBinFrameworkFolder -Recurse -Filter "*.dll" -ErrorAction Stop } catch { Write-Text "[-] Copying $PublishDirFolder to $ModuleBinFrameworkFolder failed. Error: $($_.Exception.Message)" -Color Red } } Pop-Location } function Start-ModuleBuilding { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [string] $PathToProject ) $DestinationPaths = [ordered] @{ } if ($Configuration.Information.Manifest.CompatiblePSEditions) { if ($Configuration.Information.Manifest.CompatiblePSEditions -contains 'Desktop') { $DestinationPaths.Desktop = [IO.path]::Combine($Configuration.Information.DirectoryModules, $Configuration.Information.ModuleName) } if ($Configuration.Information.Manifest.CompatiblePSEditions -contains 'Core') { $DestinationPaths.Core = [IO.path]::Combine($Configuration.Information.DirectoryModulesCore, $Configuration.Information.ModuleName) } } else { # 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) } [string] $Random = Get-Random 10000000000 [string] $FullModuleTemporaryPath = [IO.path]::GetTempPath() + '' + $Configuration.Information.ModuleName [string] $FullTemporaryPath = [IO.path]::GetTempPath() + '' + $Configuration.Information.ModuleName + "_TEMP_$Random" if ($Configuration.Information.DirectoryProjects) { [string] $FullProjectPath = [IO.Path]::Combine($Configuration.Information.DirectoryProjects, $Configuration.Information.ModuleName) } else { [string] $FullProjectPath = $PathToProject } [string] $ProjectName = $Configuration.Information.ModuleName $PSD1FilePath = "$FullProjectPath\$ProjectName.psd1" $PSM1FilePath = "$FullProjectPath\$ProjectName.psm1" if ($Configuration.Information.Manifest.ModuleVersion) { if ($Configuration.Steps.BuildModule.LocalVersion) { $Versioning = Step-Version -Module $Configuration.Information.ModuleName -ExpectedVersion $Configuration.Information.Manifest.ModuleVersion -Advanced -LocalPSD1 $PSD1FilePath } else { $Versioning = Step-Version -Module $Configuration.Information.ModuleName -ExpectedVersion $Configuration.Information.Manifest.ModuleVersion -Advanced } $Configuration.Information.Manifest.ModuleVersion = $Versioning.Version } else { # lets fake the version if there's no PSD1, and there's no version in config $Configuration.Information.Manifest.ModuleVersion = 1.0.0 } Write-Text '----------------------------------------------------' Write-Text "[i] Project/Module Name: $ProjectName" -Color Yellow if ($Configuration.Steps.BuildModule.LocalVersion) { Write-Text "[i] Current Local Version: $($Versioning.CurrentVersion)" -Color Yellow } else { Write-Text "[i] Current PSGallery Version: $($Versioning.CurrentVersion)" -Color Yellow } Write-Text "[i] Expected Version: $($Configuration.Information.Manifest.ModuleVersion)" -Color Yellow Write-Text "[i] Full module temporary path: $FullModuleTemporaryPath" -Color Yellow Write-Text "[i] Full project path: $FullProjectPath" -Color Yellow Write-Text "[i] Full temporary path: $FullTemporaryPath" -Color Yellow Write-Text "[i] PSScriptRoot: $PSScriptRoot" -Color Yellow Write-Text "[i] Current PSEdition: $PSEdition" -Color Yellow Write-Text "[i] Destination Desktop: $($DestinationPaths.Desktop)" -Color Yellow Write-Text "[i] Destination Core: $($DestinationPaths.Core)" -Color Yellow Write-Text '----------------------------------------------------' if (-not $Configuration.Steps.BuildModule) { Write-Text '[-] Section BuildModule is missing. Terminating.' -Color Red return $false } # We need to make sure module name is set, otherwise bad things will happen if (-not $Configuration.Information.ModuleName) { Write-Text '[-] Section Information.ModuleName is missing. Terminating.' -Color Red return $false } # check if project exists if (-not (Test-Path -Path $FullProjectPath)) { Write-Text "[-] Project path doesn't exists $FullProjectPath. Terminating" -Color Red return $false } Start-LibraryBuilding -RootDirectory $FullProjectPath -Version $Configuration.Information.Manifest.ModuleVersion -ModuleName $ProjectName -LibraryConfiguration $Configuration.Steps.BuildLibraries # Verify if manifest contains required modules and fix it if nessecary Convert-RequiredModules -Configuration $Configuration if ($Configuration.Steps.BuildModule.Enable -eq $true) { $CurrentLocation = (Get-Location).Path $Success = Start-PreparingStructure -Configuration $Configuration -FullProjectPath $FullProjectPath -FullTemporaryPath $FullTemporaryPath -FullModuleTemporaryPath $FullModuleTemporaryPath -DestinationPaths $DestinationPaths if ($Success -eq $false) { return $false } $Variables = Start-PreparingVariables -Configuration $Configuration -FullProjectPath $FullProjectPath if ($Variables -eq $false) { return $false } # lets build variables for later use $LinkDirectories = $Variables.LinkDirectories $LinkFilesRoot = $Variables.LinkFilesRoot $LinkPrivatePublicFiles = $Variables.LinkPrivatePublicFiles $DirectoriesWithClasses = $Variables.DirectoriesWithClasses $DirectoriesWithPS1 = $Variables.DirectoriesWithPS1 $Files = $Variables.Files $AliasesAndFunctions = Start-PreparingFunctionsAndAliases -Configuration $Configuration -FullProjectPath $FullProjectPath -Files $Files if ($AliasesAndFunctions -eq $false) { return $false } # Copy Configuration $SaveConfiguration = Copy-DictionaryManual -Dictionary $Configuration if ($Configuration.Steps.BuildModule.UseWildcardForFunctions) { $Success = New-PersonalManifest -Configuration $Configuration -ManifestPath $PSD1FilePath -AddScriptsToProcess -UseWildcardForFunctions:$Configuration.Steps.BuildModule.UseWildcardForFunctions if ($Success -eq $false) { return $false } } else { $Success = New-PersonalManifest -Configuration $Configuration -ManifestPath $PSD1FilePath -AddScriptsToProcess if ($Success -eq $false) { return $false } } # Restore configuration, as some PersonalManifest plays with those $Configuration = $SaveConfiguration $Success = Format-Code -FilePath $PSD1FilePath -FormatCode $Configuration.Options.Standard.FormatCodePSD1 if ($Success -eq $false) { return $false } $Success = Format-Code -FilePath $PSM1FilePath -FormatCode $Configuration.Options.Standard.FormatCodePSM1 if ($Success -eq $false) { return $false } if ($Configuration.Steps.BuildModule.RefreshPSD1Only) { return } if ($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 ($Configuration.Information.LibrariesStandard) { # User provided option, we don't care } elseif ($Configuration.Information.LibrariesCore -and $Configuration.Information.LibrariesDefault) { # User provided option for core and default we don't care } else { # user hasn't provided any option, we set it to default $Configuration.Information.LibrariesStandard = "Lib\Standard" $Configuration.Information.LibrariesCore = "Lib\Core" $Configuration.Information.LibrariesDefault = "Lib\Default" } if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesCore)) { # if ($Framework -eq 'Core') { $StartsWithCore = "$($Configuration.Information.LibrariesCore)\" # } else { # $StartsWithCore = "$($Configuration.Information.LibrariesStandard)\" # } # $FilesLibrariesCore = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithCore) } } if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesDefault)) { # if ($FrameworkNet -eq 'Default') { $StartsWithDefault = "$($Configuration.Information.LibrariesDefault)\" # } else { # $StartsWithDefault = "$($Configuration.Information.LibrariesStandard)\" # } # $FilesLibrariesDefault = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithDefault) } } # if ($StartsWithCore -eq $StartsWithDefault) { # $FilesLibrariesStandard = $FilesLibrariesCore # } if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesStandard)) { $StartsWithStandard = "$($Configuration.Information.LibrariesStandard)\" } $CoreFiles = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithCore) } $DefaultFiles = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithDefault) } $StandardFiles = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithStandard) } $Default = $false $Core = $false $Standard = $false if ($CoreFiles.Count -gt 0) { $Core = $true } if ($DefaultFiles.Count -gt 0) { $Default = $true } if ($StandardFiles.Count -gt 0) { $Standard = $true } if ($Standard -and $Core -and $Default) { $FrameworkNet = 'Default' $Framework = 'Standard' } elseif ($Standard -and $Core) { $Framework = 'Standard' $FrameworkNet = 'Standard' } elseif ($Core -and $Default) { $Framework = 'Core' $FrameworkNet = 'Default' } elseif ($Standard -and $Default) { $Framework = 'Standard' $FrameworkNet = 'Default' } elseif ($Standard) { $Framework = 'Standard' $FrameworkNet = 'Standard' } elseif ($Core) { $Framework = 'Core' $FrameworkNet = '' } elseif ($Default) { $Framework = '' $FrameworkNet = 'Default' } if ($Framework -eq 'Core') { $FilesLibrariesCore = $CoreFiles } elseif ($Framework -eq 'Standard') { $FilesLibrariesCore = $StandardFiles } if ($FrameworkNet -eq 'Default') { $FilesLibrariesDefault = $DefaultFiles } elseif ($FrameworkNet -eq 'Standard') { $FilesLibrariesDefault = $StandardFiles } if ($FrameworkNet -eq 'Standard' -and $Framework -eq 'Standard') { $FilesLibrariesStandard = $FilesLibrariesCore } $Success = 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 -DirectoriesWithPS1 $DirectoriesWithPS1 ` -ClassesPS1 $DirectoriesWithClasses -IncludeAsArray $Configuration.Information.IncludeAsArray if ($Success -eq $false) { return $false } if ($Configuration.Steps.BuildModule.CreateFileCatalog) { # Something is wrong here for folders other than root, need investigation $TimeToExecuteSign = [System.Diagnostics.Stopwatch]::StartNew() Write-Text "[+] Creating file catalog" -Color Blue $TimeToExecuteSign = [System.Diagnostics.Stopwatch]::StartNew() $CategoryPaths = @( $FullModuleTemporaryPath $NotEmptyPaths = (Get-ChildItem -Directory -Path $FullModuleTemporaryPath -Recurse).FullName if ($NotEmptyPaths) { $NotEmptyPaths } ) foreach ($CatPath in $CategoryPaths) { $CatalogFile = [io.path]::Combine($CatPath, "$ProjectName.cat") $FileCreated = New-FileCatalog -Path $CatPath -CatalogFilePath $CatalogFile -CatalogVersion 2.0 if ($FileCreated) { Write-Text " [>] Catalog file covering $CatPath was created $($FileCreated.Name)" -Color Yellow } } $TimeToExecuteSign.Stop() Write-Text "[+] Creating file catalog [Time: $($($TimeToExecuteSign.Elapsed).Tostring())]" -Color Blue } $SuccessFullSigning = Start-ModuleSigning -Configuration $Configuration -FullModuleTemporaryPath $FullModuleTemporaryPath if ($SuccessFullSigning -eq $false) { return $false } } if (-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 $Success = if ($Configuration.Steps.BuildModule.Enable) { if ($DestinationPaths.Desktop) { Write-TextWithTime -Text "Copy module to PowerShell 5 destination: $($DestinationPaths.Desktop)" { $Success = Remove-Directory -Directory $DestinationPaths.Desktop if ($Success -eq $false) { return $false } Add-Directory -Directory $DestinationPaths.Desktop Get-ChildItem -LiteralPath $FullModuleTemporaryPath | Copy-Item -Destination $DestinationPaths.Desktop -Recurse # 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 } -PreAppend Plus } if ($DestinationPaths.Core) { Write-TextWithTime -Text "Copy module to PowerShell 6/7 destination: $($DestinationPaths.Core)" { $Success = Remove-Directory -Directory $DestinationPaths.Core if ($Success -eq $false) { return $false } Add-Directory -Directory $DestinationPaths.Core Get-ChildItem -LiteralPath $FullModuleTemporaryPath | Copy-Item -Destination $DestinationPaths.Core -Recurse # 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 } -PreAppend Plus } } if ($Success -eq $false) { return $false } Write-TextWithTime -Text "Building artefacts" -PreAppend Information { # Old configuration still supported $Success = Start-ArtefactsBuilding -Configuration $Configuration -FullProjectPath $FullProjectPath -DestinationPaths $DestinationPaths -Type 'Releases' if ($Success -eq $false) { return $false } # Old configuration still supported $Success = Start-ArtefactsBuilding -Configuration $Configuration -FullProjectPath $FullProjectPath -DestinationPaths $DestinationPaths -Type 'ReleasesUnpacked' if ($Success -eq $false) { return $false } # new configuration building multiple artefacts foreach ($Artefact in $Configuration.Steps.BuildModule.Artefacts) { $Success = Start-ArtefactsBuilding -Configuration $Configuration -FullProjectPath $FullProjectPath -DestinationPaths $DestinationPaths -ChosenArtefact $Artefact if ($Success -eq $false) { return $false } } } -ColorBefore Yellow -ColorTime Yellow -Color Yellow } # Import Modules Section, useful to check before publishing if ($Configuration.Steps.ImportModules) { $ImportSuccess = Start-ImportingModules -Configuration $Configuration -ProjectName $ProjectName if ($ImportSuccess -eq $false) { return $false } } if ($Configuration.Options.TestsAfterMerge) { $TestsSuccess = Initialize-InternalTests -Configuration $Configuration -Type 'TestsAfterMerge' if ($TestsSuccess -eq $false) { return $false } } # Publish Module Section (old configuration) if ($Configuration.Steps.PublishModule.Enabled) { $Publishing = Start-PublishingGallery -Configuration $Configuration if ($Publishing -eq $false) { return $false } } # Publish Module Section to GitHub (old configuration) if ($Configuration.Steps.PublishModule.GitHub) { $Publishing = Start-PublishingGitHub -Configuration $Configuration -ProjectName $ProjectName if ($Publishing -eq $false) { return $false } } # new configuration allowing multiple galleries foreach ($ChosenNuget in $Configuration.Steps.BuildModule.GalleryNugets) { $Success = Start-PublishingGallery -Configuration $Configuration -ChosenNuget $ChosenNuget if ($Success -eq $false) { return $false } } # new configuration allowing multiple githubs/releases foreach ($ChosenNuget in $Configuration.Steps.BuildModule.GitHubNugets) { $Success = Start-PublishingGitHub -Configuration $Configuration -ChosenNuget $ChosenNuget -ProjectName $ProjectName if ($Success -eq $false) { return $false } } if ($Configuration.Steps.BuildDocumentation) { Start-DocumentationBuilding -Configuration $Configuration -FullProjectPath $FullProjectPath -ProjectName $ProjectName } # Cleanup temp directory Write-Text "[+] Cleaning up directories created in TEMP directory" -Color Yellow $Success = Remove-Directory $FullModuleTemporaryPath if ($Success -eq $false) { return $false } $Success = Remove-Directory $FullTemporaryPath if ($Success -eq $false) { return $false } } function Start-ModuleSigning { [CmdletBinding()] param( $Configuration, $FullModuleTemporaryPath ) if ($Configuration.Steps.BuildModule.SignMerged) { Write-TextWithTime -Text 'Applying signature to files' { $registerCertificateSplat = @{ WarningAction = 'SilentlyContinue' LocalStore = 'CurrentUser' Path = $FullModuleTemporaryPath Include = @('*.ps1', '*.psd1', '*.psm1', '*.dll', '*.cat') TimeStampServer = 'http://timestamp.digicert.com' } if ($Configuration.Options.Signing) { if ($Configuration.Options.Signing.CertificatePFXBase64) { $Success = Import-ValidCertificate -CertificateAsBase64 $Configuration.Options.Signing.CertificatePFXBase64 -PfxPassword $Configuration.Options.Signing.CertificatePFXPassword if (-not $Success) { return $false } $registerCertificateSplat.Thumbprint = $Success.Thumbprint Write-Host $Success.Thumbprint } elseif ($Configuration.Options.Signing.CertificatePFXPath) { $Success = Import-ValidCertificate -FilePath $Configuration.Options.Signing.CertificatePFXPath -PfxPassword $Configuration.Options.Signing.CertificatePFXPassword if (-not $Success) { return $false } Write-Host $Success.Thumbprint $registerCertificateSplat.Thumbprint = $Success.Thumbprint } else { if ($Configuration.Options.Signing -and $Configuration.Options.Signing.Thumbprint) { $registerCertificateSplat.Thumbprint = $Configuration.Options.Signing.Thumbprint } elseif ($Configuration.Options.Signing -and $Configuration.Options.Signing.CertificateThumbprint) { $registerCertificateSplat.Thumbprint = $Configuration.Options.Signing.CertificateThumbprint } } [Array] $SignedFiles = Register-Certificate @registerCertificateSplat if ($SignedFiles.Count -eq 0) { throw "Please configure certificate for use, or disable signing." return $false } else { if ($SignedFiles[0].Thumbprint) { Write-Text -Text " [i] Multiple certificates found for signing:" foreach ($Certificate in $SignedFiles) { Write-Text " [>] Certificate $($Certificate.Thumbprint) with subject: $($Certificate.Subject)" -Color Yellow } throw "Please configure single certificate for use or disable signing." return $false } else { foreach ($File in $SignedFiles) { Write-Text " [>] File $($File.Path) with status: $($File.StatusMessage)" -Color Yellow } } } } } -PreAppend Plus } } function Start-PreparingFunctionsAndAliases { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, $FullProjectPath, $Files ) $AliasesAndFunctions = Write-TextWithTime -Text 'Preparing function and aliases names' { Get-FunctionAliasesFromFolder -FullProjectPath $FullProjectPath -Files $Files #-Folder $Configuration.Information.AliasesToExport } -PreAppend Information Write-TextWithTime -Text "Checking for duplicates in funcions and aliases" { 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 = @() } } $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-TextWithTime -Text " [-] Alias $Alias is used multiple times. Fix it!" -Color Red $FoundDuplicateAliases = $true } if ($FoundDuplicateAliases) { return $false } } 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" -PreAppend Plus -SpacesBefore ' ' $Configuration.Information.Manifest.ScriptsToProcess = $FilesEnums } } } -PreAppend Information $AliasesAndFunctions } function Start-PreparingStructure { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [System.Collections.IDictionary] $DestinationPaths, [string] $FullProjectPath, [string] $FullModuleTemporaryPath, [string] $FullTemporaryPath ) Write-TextWithTime -Text "Preparing structure" -PreAppend Information { if ($Configuration.Steps.BuildModule.DeleteBefore -eq $true) { Write-TextWithTime -Text "Deleting old module (Desktop destination) $($DestinationPaths.Desktop)" { $Success = Remove-Directory -Directory $($DestinationPaths.Desktop) -ErrorAction Stop if ($Success -eq $false) { return $false } } -PreAppend Minus -SpacesBefore " " -Color Blue -ColorError Red -ColorTime Green -ColorBefore Yellow Write-TextWithTime -Text "Deleting old module (Core destination) $($DestinationPaths.Core)" { $Success = Remove-Directory -Directory $($DestinationPaths.Core) if ($Success -eq $false) { return $false } } -PreAppend Minus -SpacesBefore " " -Color Blue -ColorError Red -ColorTime Green -ColorBefore Yellow } Set-Location -Path $FullProjectPath Write-TextWithTime -Text "Cleaning up temporary path $($FullModuleTemporaryPath)" { $Success = Remove-Directory -Directory $FullModuleTemporaryPath if ($Success -eq $false) { return $false } Add-Directory -Directory $FullModuleTemporaryPath } -PreAppend Minus -SpacesBefore " " -Color Blue -ColorError Red -ColorTime Green -ColorBefore Yellow Write-TextWithTime -Text "Cleaning up temporary path $($FullTemporaryPath)" { $Success = Remove-Directory -Directory $FullTemporaryPath if ($Success -eq $false) { return $false } Add-Directory -Directory $FullTemporaryPath } -PreAppend Minus -SpacesBefore " " -Color Blue -ColorError Red -ColorTime Green -ColorBefore Yellow } } function Start-PreparingVariables { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [string] $FullProjectPath ) Write-TextWithTime -Text "Preparing files and folders variables" -PreAppend Plus { $LinkDirectories = @() $LinkPrivatePublicFiles = @() if ($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 = 'Classes', 'Private', 'Public', 'Enums' } # This is basically converting given folder into array of variables # mostly done for internal project and testimo $DirectoriesWithArrays = $Configuration.Information.IncludeAsArray.Values if ($Configuration.Information.IncludeClasses) { $DirectoriesWithClasses = $Configuration.Information.IncludeClasses } else { $DirectoriesWithClasses = 'Classes' } if ($Configuration.Information.IncludeAll) { $DirectoriesWithAll = $Configuration.Information.IncludeAll | ForEach-Object { if ($_.EndsWith('\')) { $_ } else { "$_\" } } } else { $DirectoriesWithAll = 'Images\', 'Resources\', 'Templates\', 'Bin\', 'Lib\', 'Data\' } 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 } } foreach ($dir in $DirectoriesWithArrays) { 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 [ordered] @{ LinkDirectories = $LinkDirectories LinkFilesRoot = $LinkFilesRoot LinkPrivatePublicFiles = $LinkPrivatePublicFiles DirectoriesWithClasses = $DirectoriesWithClasses #RootFiles = $RootFiles #AllFiles = $AllFiles #Directories = $Directories Files = $Files #Exclude = $Exclude #IncludeFilesRoot = $IncludeFilesRoot DirectoriesWithPS1 = $DirectoriesWithPS1 #DirectoriesWithArrays = $DirectoriesWithArrays #DirectoriesWithAll = $DirectoriesWithAll } } } function Start-PublishingGallery { [CmdletBinding()] param( [System.Collections.IDictionary] $Configuration, [System.Collections.IDictionary] $ChosenNuget ) if ($ChosenNuget) { $Repository = if ($ChosenNuget.RepositoryName) { $ChosenNuget.RepositoryName } else { 'PSGallery' } Write-TextWithTime -Text "Publishing Module to Gallery ($Repository)" { if ($ChosenNuget.ApiKey) { $publishModuleSplat = @{ Name = $Configuration.Information.ModuleName Repository = $Repository NuGetApiKey = $ChosenNuget.ApiKey Force = $ChosenNuget.Force Verbose = $ChosenNuget.Verbose ErrorAction = 'Stop' } Publish-Module @publishModuleSplat } else { return $false } } -PreAppend Plus } elseif ($Configuration.Steps.PublishModule.Enabled) { # old way Write-TextWithTime -Text "Publishing Module to PowerShellGallery" { if ($Configuration.Options.PowerShellGallery.FromFile) { $ApiKey = Get-Content -Path $Configuration.Options.PowerShellGallery.ApiKey -ErrorAction Stop -Encoding UTF8 } else { $ApiKey = $Configuration.Options.PowerShellGallery.ApiKey } $publishModuleSplat = @{ Name = $Configuration.Information.ModuleName Repository = 'PSGallery' NuGetApiKey = $ApiKey Force = $Configuration.Steps.PublishModule.RequireForce Verbose = if ($Configuration.Steps.PublishModule.PSGalleryVerbose) { $Configuration.Steps.PublishModule.PSGalleryVerbose } else { $false } ErrorAction = 'Stop' } Publish-Module @publishModuleSplat } -PreAppend Plus } } function Start-PublishingGitHub { [cmdletBinding()] param( [System.Collections.IDictionary] $ChosenNuget, [System.Collections.IDictionary] $Configuration, [string] $ProjectName ) if ($ChosenNuget) { [Array] $ListZips = if ($ChosenNuget.Id) { foreach ($Zip in $Configuration.CurrentSettings['Artefact']) { if ($Zip.Id -eq $ChosenNuget.Id) { $ZipPath = $Zip.ZipPath if ($ZipPath -and (Test-Path -LiteralPath $ZipPath)) { $ZipPath } } } # if ($Configuration.CurrentSettings['Artefact'][$ChosenNuget.Id]) { # #$ZipName = $Configuration.CurrentSettings['Artefact'][$ChosenNuget.Id].ZipName # $ZipPath = $Configuration.CurrentSettings['Artefact'][$ChosenNuget.Id].ZipPath # } else { # $ZipPath = $null # } } else { if ($Configuration.CurrentSettings['ArtefactDefault']) { #$ZipName = $Configuration.CurrentSettings['ArtefactDefault'].ZipName $ZipPath = $Configuration.CurrentSettings['ArtefactDefault'].ZipPath if ($ZipPath -and (Test-Path -LiteralPath $ZipPath)) { $ZipPath } } else { #$ZipPath = $null } } if ($ChosenNuget.ID) { $TextToUse = "Publishing to GitHub [ID: $($ChosenNuget.Id)] ($ZipPath)" } else { $TextToUse = "Publishing to GitHub ($ZipPath)" } if ($ListZips.Count -gt 0) { Write-TextWithTime -Text $TextToUse -PreAppend Information -ColorBefore Yellow { if ($ZipPath -and (Test-Path -LiteralPath $ZipPath)) { if ($ChosenNuget.OverwriteTagName) { $ModuleName = $Configuration.Information.Manifest.ModuleName $ModuleVersion = $Configuration.Information.Manifest.ModuleVersion # if pre-release is set, we want to use it in the name if ($Configuration.CurrentSettings.PreRelease) { $ModuleVersionWithPreRelease = "$($ModuleVersion)-$($Configuration.CurrentSettings.PreRelease)" $TagModuleVersionWithPreRelease = "v$($ModuleVersionWithPreRelease)" } else { $ModuleVersionWithPreRelease = $ModuleVersion $TagModuleVersionWithPreRelease = "v$($ModuleVersion)" } $TagNameDefault = "v$($ModuleVersion)" $TagName = $ChosenNuget.OverwriteTagName $TagName = $TagName.Replace('{ModuleName}', $ModuleName) $TagName = $TagName.Replace('<ModuleName>', $ModuleName) $TagName = $TagName.Replace('{ModuleVersion}', $ModuleVersion) $TagName = $TagName.Replace('<ModuleVersion>', $ModuleVersion) $TagName = $TagName.Replace('{ModuleVersionWithPreRelease}', $ModuleVersionWithPreRelease) $TagName = $TagName.Replace('<ModuleVersionWithPreRelease>', $ModuleVersionWithPreRelease) $TagName = $TagName.Replace('{TagModuleVersionWithPreRelease}', $TagModuleVersionWithPreRelease) $TagName = $TagName.Replace('<TagModuleVersionWithPreRelease>', $TagModuleVersionWithPreRelease) $TagName = $TagName.Replace('{TagName}', $TagNameDefault) $TagName = $TagName.Replace('<TagName>', $TagNameDefault) } else { $TagName = "v$($Configuration.Information.Manifest.ModuleVersion)" } # normally we publish as prerelease if the module is prerelease edition # but since Github hides prerelease versions a bit, this is the way to overwrite this choice if ($Configuration.CurrentSettings.Prerelease) { if ($ChosenNuget.DoNotMarkAsPreRelease) { $IsPreRelease = $false } else { $IsPreRelease = $true } } else { $IsPreRelease = $false } $sendGitHubReleaseSplat = [ordered] @{ GitHubUsername = $ChosenNuget.UserName GitHubRepositoryName = if ($ChosenNuget.RepositoryName) { $ChosenNuget.RepositoryName } else { $ProjectName } GitHubAccessToken = $ChosenNuget.ApiKey TagName = $TagName AssetFilePaths = $ListZips IsPreRelease = $IsPreRelease # those don't work, requires testing #GenerateReleaseNotes = $true #MakeLatest = $true #GenerateReleaseNotes = if ($Configuration.Options.GitHub.GenerateReleaseNotes) { $true } else { $false } #MakeLatest = if ($Configuration.Options.GitHub.MakeLatest) { $true } else { $false } Verbose = if ($ChosenNuget.Verbose) { $ChosenNuget.Verbose } else { $false } } $StatusGithub = Send-GitHubRelease @sendGitHubReleaseSplat if ($StatusGithub.ReleaseCreationSucceeded -and $statusGithub.Succeeded) { $GithubColor = 'Green' $GitHubText = '+' } else { $GithubColor = 'Red' $GitHubText = '-' } Write-Text "[$GitHubText] GitHub Release Creation Status: $($StatusGithub.ReleaseCreationSucceeded)" -Color $GithubColor Write-Text "[$GitHubText] GitHub Release Succeeded: $($statusGithub.Succeeded)" -Color $GithubColor Write-Text "[$GitHubText] GitHub Release Asset Upload Succeeded: $($statusGithub.AllAssetUploadsSucceeded)" -Color $GithubColor Write-Text "[$GitHubText] GitHub Release URL: $($statusGitHub.ReleaseUrl)" -Color $GithubColor if ($statusGithub.ErrorMessage) { Write-Text "[$GitHubText] GitHub Release ErrorMessage: $($statusGithub.ErrorMessage)" -Color $GithubColor return $false } } else { Write-Text " [e] GitHub Release Creation Status: Failed" -Color Red Write-Text " [e] GitHub Release Creation Reason: $ZipPath doesn't exists. Most likely Releases option is disabled." -Color Red return $false } } } else { Write-Text -Text "[-] Publishing to GitHub failed. No ZIPs to process." -Color Red return $false } } else { # old configuration if (-not $Configuration.CurrentSettings.ArtefactZipPath -or -not (Test-Path -LiteralPath $Configuration.CurrentSettings.ArtefactZipPath)) { Write-Text -Text "[-] Publishing to GitHub failed. File $($Configuration.CurrentSettings.ArtefactZipPath) doesn't exists" -Color Red return $false } $TagName = "v$($Configuration.Information.Manifest.ModuleVersion)" $ZipPath = $Configuration.CurrentSettings.ArtefactZipPath if ($Configuration.Options.GitHub.FromFile) { $GitHubAccessToken = Get-Content -LiteralPath $Configuration.Options.GitHub.ApiKey -Encoding UTF8 } else { $GitHubAccessToken = $Configuration.Options.GitHub.ApiKey } if ($GitHubAccessToken) { if ($Configuration.Options.GitHub.RepositoryName) { $GitHubRepositoryName = $Configuration.Options.GitHub.RepositoryName } else { $GitHubRepositoryName = $ProjectName } Write-TextWithTime -Text "Publishing to GitHub ($ZipPath)" -PreAppend Information -ColorBefore Yellow { if (Test-Path -LiteralPath $ZipPath) { if ($Configuration.Steps.PublishModule.Prerelease) { $IsPreRelease = $true } else { $IsPreRelease = $false } $sendGitHubReleaseSplat = [ordered] @{ GitHubUsername = $Configuration.Options.GitHub.UserName GitHubRepositoryName = $GitHubRepositoryName GitHubAccessToken = $GitHubAccessToken TagName = $TagName AssetFilePaths = $ZipPath IsPreRelease = $IsPreRelease # those don't work, requires testing #GenerateReleaseNotes = $true #MakeLatest = $true #GenerateReleaseNotes = if ($Configuration.Options.GitHub.GenerateReleaseNotes) { $true } else { $false } #MakeLatest = if ($Configuration.Options.GitHub.MakeLatest) { $true } else { $false } Verbose = if ($Configuration.Steps.PublishModule.GitHubVerbose) { $Configuration.Steps.PublishModule.GitHubVerbose } else { $false } } $StatusGithub = Send-GitHubRelease @sendGitHubReleaseSplat if ($StatusGithub.ReleaseCreationSucceeded -and $statusGithub.Succeeded) { $GithubColor = 'Green' $GitHubText = '>' } else { $GithubColor = 'Red' $GitHubText = '-' } Write-Text " [$GitHubText] GitHub Release Creation Status: $($StatusGithub.ReleaseCreationSucceeded)" -Color $GithubColor Write-Text " [$GitHubText] GitHub Release Succeeded: $($statusGithub.Succeeded)" -Color $GithubColor Write-Text " [$GitHubText] GitHub Release Asset Upload Succeeded: $($statusGithub.AllAssetUploadsSucceeded)" -Color $GithubColor Write-Text " [$GitHubText] GitHub Release URL: $($statusGitHub.ReleaseUrl)" -Color $GithubColor if ($statusGithub.ErrorMessage) { Write-Text "[$GitHubText] GitHub Release ErrorMessage: $($statusGithub.ErrorMessage)" -Color $GithubColor return $false } } else { Write-Text " [e] GitHub Release Creation Status: Failed" -Color Red Write-Text " [e] GitHub Release Creation Reason: $ZipPath doesn't exists. Most likely Releases option is disabled." -Color Red return $false } } } } } function Step-Version { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER Module Parameter description .PARAMETER ExpectedVersion Parameter description .PARAMETER Advanced Parameter description .EXAMPLE Step-Version -Module Testimo12 -ExpectedVersion '0.1.X' Step-Version -ExpectedVersion '0.1.X' Step-Version -ExpectedVersion '0.1.5.X' Step-Version -ExpectedVersion '1.2.X' Step-Version -Module PSWriteHTML -ExpectedVersion '0.0.X' Step-Version -Module PSWriteHTML1 -ExpectedVersion '0.1.X' Step-Version -Module PSPublishModule -ExpectedVersion '0.9.X' -Advanced -LocalPSD1 "C:\Support\GitHub\PSPublishModule\PSPublishModule.psd1" .NOTES General notes #> [cmdletBinding()] param( [string] $Module, [Parameter(Mandatory)][string] $ExpectedVersion, [switch] $Advanced, [string] $LocalPSD1 ) $Version = $null $VersionCheck = [version]::TryParse($ExpectedVersion, [ref] $Version) if ($VersionCheck) { # Don't do anything, return what user wanted to get anyways @{ Version = $ExpectedVersion CurrentVersion = 'Not aquired, no auto versioning.' } } else { if ($Module) { if (-not $LocalPSD1) { try { $ModuleGallery = Find-Module -Name $Module -ErrorAction Stop -Verbose:$false -WarningAction SilentlyContinue $CurrentVersion = [version] $ModuleGallery.Version } catch { #throw "Couldn't find module $Module to asses version information. Terminating." $CurrentVersion = $null } } else { if (Test-Path -LiteralPath $LocalPSD1) { $PSD1Data = Import-PowerShellDataFile -Path $LocalPSD1 if ($PSD1Data.ModuleVersion) { try { $CurrentVersion = [version] $PSD1Data.ModuleVersion } catch { Write-Warning -Message "Couldn't parse version $($PSD1Data.ModuleVersion) from PSD1 file $LocalPSD1" $CurrentVersion = $null } } } else { Write-Warning -Message "Couldn't find local PSD1 file $LocalPSD1" $CurrentVersion = $null } } } else { $CurrentVersion = $null } $Splitted = $ExpectedVersion.Split('.') $PreparedVersion = [ordered] @{ Major = $Splitted[0] Minor = $Splitted[1] Build = $Splitted[2] Revision = $Splitted[3] } [string] $StepType = foreach ($Key in $PreparedVersion.Keys) { if ($PreparedVersion[$Key] -eq 'X') { $Key break } } if ($null -eq $CurrentVersion) { $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 CurrentVersion = $CurrentVersion } } else { $ProposedVersion } } else { throw "Couldn't properly verify version is version. Terminating." } } } function Test-AllFilePathsAndThrowErrorIfOneIsNotValid { [CmdletBinding()] param( [string[]] $filePaths ) foreach ($filePath in $filePaths) { [bool] $fileWasNotFoundAtPath = [string]::IsNullOrEmpty($filePath) -or !(Test-Path -Path $filePath -PathType Leaf) if ($fileWasNotFoundAtPath) { throw "There is no file at the specified path, '$filePath'." } } } function Test-ReparsePoint { [CmdletBinding()] param ( [string]$path ) $file = Get-Item $path -Force -ea SilentlyContinue return [bool]($file.Attributes -band [IO.FileAttributes]::ReparsePoint) } function 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, [ValidateSet('Plus', 'Minus', 'Information', 'Addition')][string] $PreAppend, [string] $Text, [switch] $Continue, [System.ConsoleColor] $Color = [System.ConsoleColor]::Cyan, [System.ConsoleColor] $ColorTime = [System.ConsoleColor]::Green, [System.ConsoleColor] $ColorError = [System.ConsoleColor]::Red, [System.ConsoleColor] $ColorBefore, [string] $SpacesBefore ) if ($PreAppend) { if ($PreAppend -eq "Information") { $TextBefore = "$SpacesBefore[i] " if (-not $ColorBefore) { $ColorBefore = [System.ConsoleColor]::Yellow } } elseif ($PreAppend -eq 'Minus') { $TextBefore = "$SpacesBefore[-] " if (-not $ColorBefore) { $ColorBefore = [System.ConsoleColor]::Red } } elseif ($PreAppend -eq 'Plus') { $TextBefore = "$SpacesBefore[+] " if (-not $ColorBefore) { $ColorBefore = [System.ConsoleColor]::Cyan } } elseif ($PreAppend -eq 'Addition') { $TextBefore = "$SpacesBefore[>] " if (-not $ColorBefore) { $ColorBefore = [System.ConsoleColor]::Yellow } } Write-Host -Object "$TextBefore" -NoNewline -ForegroundColor $ColorBefore Write-Host -Object "$Text" -ForegroundColor $Color } else { Write-Host -Object "$Text" -ForegroundColor $Color } $Time = [System.Diagnostics.Stopwatch]::StartNew() if ($null -ne $Content) { try { $InputData = & $Content if ($InputData -contains $false) { $ErrorMessage = "Failure in action above. Check output above." } else { $InputData } } catch { $ErrorMessage = $_.Exception.Message + " (File: $($_.InvocationInfo.ScriptName), Line: " + $_.InvocationInfo.ScriptLineNumber + ")" } } $TimeToExecute = $Time.Elapsed.ToString() if ($ErrorMessage) { Write-Host -Object "$SpacesBefore[e] $Text [Error: $ErrorMessage]" -ForegroundColor $ColorError if ($PreAppend) { Write-Host -Object "$($TextBefore)" -NoNewline -ForegroundColor $ColorError } Write-Host -Object "$Text [Time: $TimeToExecute]" -ForegroundColor $ColorError $Time.Stop() return $false break } else { if ($PreAppend) { Write-Host -Object "$($TextBefore)" -NoNewline -ForegroundColor $ColorBefore } Write-Host -Object "$Text [Time: $TimeToExecute]" -ForegroundColor $ColorTime } if (-not $Continue) { $Time.Stop() } } function Convert-CommandsToList { [cmdletbinding()] param( [parameter(Mandatory)][string] $ModuleName, [string[]] $CommandTypes ) $Commands = Get-Command -Module $ModuleName $CommandsOnly = $Commands | Where-Object { $_.CommandType -eq 'Function' } $List = [ordered] @{} foreach ($Command in $CommandsOnly) { if ($Command.Name.StartsWith('Get')) { $CommandType = 'Get' } elseif ($Command.Name.StartsWith('Set')) { $CommandType = 'Set' } else { $CommandType = 'Other' } if ($CommandType -ne 'Other') { $Name = $Command.Name.Replace("Get-", '').Replace("Set-", '') if (-not $List[$Name]) { $List[$Name] = [PSCustomObject] @{ #Get = $CommandType -eq 'Get' Get = if ($CommandType -eq 'Get') { $Command.Name } else { '' } #Set = $CommandType -eq 'Set' Set = if ($CommandType -eq 'Set') { $Command.Name } else { '' } } } else { $List[$Name].$CommandType = $Command.Name } } } $List.Values } 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-PortableModule { [CmdletBinding()] param( [alias('ModuleName')][string] $Name, [string] $Path = $PSScriptRoot, [switch] $Download, [switch] $Import ) if (-not $Name) { Write-Warning "Initialize-ModulePortable - Module name not given. Terminating." return } if (-not $Download -and -not $Import) { Write-Warning "Initialize-ModulePortable - Please choose Download/Import switch. Terminating." return } if ($Download) { try { if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { $null = New-Item -ItemType Directory -Path $Path -Force } Save-Module -Name $Name -LiteralPath $Path -WarningVariable WarningData -WarningAction SilentlyContinue -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message if ($WarningData) { Write-Warning "Initialize-ModulePortable - $WarningData" } Write-Warning "Initialize-ModulePortable - Error $ErrorMessage" return } } if ($Download -or $Import) { [Array] $Modules = Get-RequiredModule -Path $Path -Name $Name | Where-Object { $null -ne $_ } if ($null -ne $Modules) { [array]::Reverse($Modules) } $CleanedModules = [System.Collections.Generic.List[string]]::new() foreach ($_ in $Modules) { if ($CleanedModules -notcontains $_) { $CleanedModules.Add($_) } } $CleanedModules.Add($Name) $Items = foreach ($_ in $CleanedModules) { Get-ChildItem -LiteralPath "$Path\$_" -Filter '*.psd1' -Recurse -ErrorAction SilentlyContinue -Depth 1 } [Array] $PSD1Files = $Items.FullName } if ($Download) { $ListFiles = foreach ($PSD1 in $PSD1Files) { $PSD1.Replace("$Path", '$PSScriptRoot') } # Build File $Content = @( '$Modules = @(' foreach ($_ in $ListFiles) { " `"$_`"" } ')' "foreach (`$_ in `$Modules) {" " Import-Module `$_ -Verbose:`$false -Force" "}" ) $Content | Set-Content -Path $Path\$Name.ps1 -Force } if ($Import) { $ListFiles = foreach ($PSD1 in $PSD1Files) { $PSD1 } foreach ($_ in $ListFiles) { Import-Module $_ -Verbose:$false -Force } } } function Initialize-PortableScript { [cmdletBinding()] param( [string] $FilePath, [string] $OutputPath, [Array] $ApprovedModules ) $Output = Get-MissingFunctions -FilePath $FilePath -SummaryWithCommands -ApprovedModules $ApprovedModules $Script = Get-Content -LiteralPath $FilePath -Encoding UTF8 $FinalScript = @( $Output.Functions $Script ) $FinalScript | Set-Content -LiteralPath $OutputPath $Output } function Initialize-ProjectManager { <# .SYNOPSIS Builds VSCode Project manager config from filesystem .DESCRIPTION Builds VSCode Project manager config from filesystem .PARAMETER Path Path to where the projects are located .PARAMETER DisableSorting Disables sorting of the projects by last modified date .EXAMPLE Initialize-ProjectManager -Path "C:\Support\GitHub" .EXAMPLE Initialize-ProjectManager -Path "C:\Support\GitHub" -DisableSorting .NOTES General notes #> [cmdletBinding()] param( [parameter(Mandatory)][string] $Path, [switch] $DisableSorting ) $ProjectsPath = Get-ChildItem -LiteralPath $Path -Directory $SortedProjects = foreach ($Project in $ProjectsPath) { $AllFiles = Get-ChildItem -LiteralPath $Project.FullName -Exclude ".\.git" $NewestFile = $AllFiles | Sort-Object -Descending -Property LastWriteTime | Select-Object -First 1 [PSCustomObject] @{ name = $Project.name FullName = $Project.FullName LastWriteTime = $NewestFile.LastWriteTime } } if (-not $DisableSorting) { $SortedProjects = $SortedProjects | Sort-Object -Descending -Property LastWriteTime } $ProjectManager = foreach ($Project in $SortedProjects) { [PSCustomObject] @{ name = $Project.name rootPath = $Project.FullName paths = @() tags = @() enabled = $true } } $PathProjects = [io.path]::Combine($Env:APPDATA, "Code\User\globalStorage\alefragnani.project-manager"), [io.path]::Combine($Env:APPDATA, "Code - Insiders\User\globalStorage\alefragnani.project-manager") foreach ($Project in $PathProjects) { if (Test-Path -LiteralPath $Project) { $JsonPath = [io.path]::Combine($Project, 'projects.json') if (Test-Path -LiteralPath $JsonPath) { Get-Content -LiteralPath $JsonPath -Encoding UTF8 | Set-Content -LiteralPath "$JsonPath.backup" } $ProjectManager | ConvertTo-Json | Set-Content -LiteralPath $JsonPath } } } function Invoke-ModuleBuild { <# .SYNOPSIS Command to create new module or update existing one. It will create new module structure and everything around it, or update existing one. .DESCRIPTION Command to create new module or update existing one. It will create new module structure and everything around it, or update existing one. .PARAMETER Settings Provide settings for the module in form of scriptblock. It's using DSL to define settings for the module. .PARAMETER Path Path to the folder where new project will be created, or existing project will be updated. If not provided it will be created in one up folder from the location of build script. .PARAMETER ModuleName Provide name of the module. It's required parameter. .PARAMETER FunctionsToExportFolder Public functions folder name. Default is 'Public'. It will be used as part of PSD1 and PSM1 to export only functions from this folder. .PARAMETER AliasesToExportFolder Public aliases folder name. Default is 'Public'. It will be used as part of PSD1 and PSM1 to export only aliases from this folder. .PARAMETER Configuration Provides a way to configure module using hashtable. It's the old way of configuring module, that requires knowledge of inner workings of the module to name proper key/value pairs It's required for compatibility with older versions of the module. .PARAMETER ExcludeFromPackage Exclude files from Artefacts. Default is '.*, 'Ignore', 'Examples', 'package.json', 'Publish', 'Docs'. .PARAMETER IncludeRoot Include files in the Artefacts from root of the project. Default is '*.psm1', '*.psd1', 'License*' files. Other files will be ignored. .PARAMETER IncludePS1 Include *.ps1 files in the Artefacts from given folders. Default are 'Private', 'Public', 'Enums', 'Classes' folders. If the folder doesn't exists it will be ignored. .PARAMETER IncludeAll Include all files in the Artefacts from given folders. Default are 'Images', 'Resources', 'Templates', 'Bin', 'Lib', 'Data' folders. .PARAMETER IncludeCustomCode Parameter description .PARAMETER IncludeToArray Parameter description .PARAMETER LibrariesCore Parameter description .PARAMETER LibrariesDefault Parameter description .PARAMETER LibrariesStandard Parameter description .PARAMETER ExitCode Exit code to be returned to the caller. If not provided, it will not exit the script, but finish gracefully. Exit code 0 means success, 1 means failure. .EXAMPLE An example .NOTES General notes #> [alias('New-PrepareModule', 'Build-Module', 'Invoke-ModuleBuilder')] [CmdletBinding(DefaultParameterSetName = 'Modern')] param ( [Parameter(Position = 0, ParameterSetName = 'Modern')][scriptblock] $Settings, [parameter(ParameterSetName = 'Modern')][string] $Path, [parameter(Mandatory, ParameterSetName = 'Modern')][alias('ProjectName')][string] $ModuleName, [parameter(ParameterSetName = 'Modern')][string] $FunctionsToExportFolder = 'Public', [parameter(ParameterSetName = 'Modern')][string] $AliasesToExportFolder = 'Public', [Parameter(Mandatory, ParameterSetName = 'Configuration')][System.Collections.IDictionary] $Configuration = [ordered] @{}, [parameter(ParameterSetName = 'Modern')][string[]] $ExcludeFromPackage = @('.*', 'Ignore', 'Examples', 'package.json', 'Publish', 'Docs'), [parameter(ParameterSetName = 'Modern')][string[]] $IncludeRoot = @('*.psm1', '*.psd1', 'License*'), [parameter(ParameterSetName = 'Modern')][string[]] $IncludePS1 = @('Private', 'Public', 'Enums', 'Classes'), [parameter(ParameterSetName = 'Modern')][string[]] $IncludeAll = @('Images', 'Resources', 'Templates', 'Bin', 'Lib', 'Data'), [parameter(ParameterSetName = 'Modern')][scriptblock] $IncludeCustomCode, [parameter(ParameterSetName = 'Modern')][System.Collections.IDictionary] $IncludeToArray, [parameter(ParameterSetName = 'Modern')][string] $LibrariesCore = 'Lib\Core', [parameter(ParameterSetName = 'Modern')][string] $LibrariesDefault = 'Lib\Default', [parameter(ParameterSetName = 'Modern')][string] $LibrariesStandard = 'Lib\Standard', [parameter(ParameterSetName = 'Configuration')] [parameter(ParameterSetName = 'Modern')] [switch] $ExitCode ) if ($PsCmdlet.ParameterSetName -eq 'Configuration') { $ModuleName = $Configuration.Information.ModuleName } if ($Path) { # Path is given so we use it as is $FullProjectPath = [io.path]::Combine($Path, $ModuleName) } else { # this assumes that the script running this in Build or Publish folder (or any other folder that is 1 level below the root of the project) $PathToProject = Get-Item -LiteralPath "$($MyInvocation.PSScriptRoot)/.." $FullProjectPath = Get-Item -LiteralPath $PathToProject } Write-Host "[i] Module Build Initializing..." -ForegroundColor Yellow $GlobalTime = [System.Diagnostics.Stopwatch]::StartNew() if ($Path -and $ModuleName) { $FullProjectPath = [io.path]::Combine($Path, $ModuleName) if (-not (Test-Path -Path $Path)) { Write-Text -Text "[-] Path $Path doesn't exists. Please create it, before continuing." -Color Red if ($ExitCode) { Exit 1 } else { return } } else { $CopiedBuildModule = $false $CopiedPSD1 = $false if (Test-Path -Path $FullProjectPath) { Write-Text -Text "[i] Module $ModuleName ($FullProjectPath) already exists. Skipping inital steps" -Color DarkGray } else { Write-Text -Text "[i] Preparing module structure for $ModuleName in $Path" -Color DarkGray $Folders = 'Private', 'Public', 'Examples', 'Ignore', 'Build' Add-Directory -Directory $FullProjectPath foreach ($folder in $Folders) { $SubFolder = [io.path]::Combine($FullProjectPath, $Folder) Add-Directory -Directory $SubFolder } if (-not (Test-Path -LiteralPath "$FullProjectPath\.gitignore")) { Write-Text -Text " [+] Copying '.gitignore' file" -Color DarkGray Copy-File -Source "$PSScriptRoot\..\Data\Example-Gitignore.txt" -Destination "$FullProjectPath\.gitignore" } if (-not (Test-Path -LiteralPath "$FullProjectPath\CHANGELOG.MD")) { Write-Text -Text " [+] Copying CHANGELOG.MD file" -Color DarkGray Copy-File -Source "$PSScriptRoot\..\Data\Example-CHANGELOG.MD" -Destination "$FullProjectPath\CHANGELOG.MD" } if (-not (Test-Path -LiteralPath "$FullProjectPath\README.MD")) { Write-Text -Text " [+] Copying README.MD file" -Color DarkGray Copy-File -Source "$PSScriptRoot\..\Data\Example-README.MD" -Destination "$FullProjectPath\README.MD" } if (-not (Test-Path -LiteralPath "$FullProjectPath\License")) { Write-Text -Text " [+] Copying MIT License file" -Color DarkGray Copy-File -Source "$PSScriptRoot\..\Data\Example-LicenseMIT.txt" -Destination "$FullProjectPath\License" } if (-not (Test-Path -LiteralPath "$FullProjectPath\Build\Build-Module.ps1")) { Write-Text -Text " [+] Copying Build-Module.ps1 file" -Color DarkGray Copy-File -Source "$PSScriptRoot\..\Data\Example-ModuleBuilder.txt" -Destination "$FullProjectPath\Build\Build-Module.ps1" $CopiedBuildModule = $True } if (-not (Test-Path -LiteralPath "$FullProjectPath\$ModuleName.psm1")) { Write-Text -Text " [+] Copying Module PSM1 file." -Color DarkGray Copy-File -Source "$PSScriptRoot\..\Data\Example-ModulePSM1.txt" -Destination "$FullProjectPath\$ModuleName.psm1" } if (-not (Test-Path -LiteralPath "$FullProjectPath\$ModuleName.psd1")) { Write-Text -Text " [+] Copying Module PSD1 file." -Color DarkGray Copy-File -Source "$PSScriptRoot\..\Data\Example-ModulePSD1.txt" -Destination "$FullProjectPath\$ModuleName.psd1" $CopiedPSD1 = $True } # lets update module builder to proper module name and guid $Guid = (New-Guid).Guid if ($CopiedBuildModule) { Register-DataForInitialModule -FilePath "$FullProjectPath\Build\Build-Module.ps1" -ModuleName $ModuleName -Guid $Guid } if ($CopiedPSD1) { Register-DataForInitialModule -FilePath "$FullProjectPath\$ModuleName.psd1" -ModuleName $ModuleName -Guid $Guid } Write-Text -Text "[i] Preparing module structure for $ModuleName in $Path. Completed." -Color DarkGray } } } $ModuleOutput = New-PrepareStructure -Configuration $Configuration -Settings $Settings -PathToProject $FullProjectPath -ModuleName $ModuleName $Execute = "$($GlobalTime.Elapsed.Days) days, $($GlobalTime.Elapsed.Hours) hours, $($GlobalTime.Elapsed.Minutes) minutes, $($GlobalTime.Elapsed.Seconds) seconds, $($GlobalTime.Elapsed.Milliseconds) milliseconds" if ($ModuleOutput -notcontains $false) { Write-Host "[i] Module Build Completed " -NoNewline -ForegroundColor Green Write-Host "[Time Total: $Execute]" -ForegroundColor Green if ($ExitCode) { Exit 0 } } else { Write-Host "[i] Module Build Failed " -NoNewline -ForegroundColor Red Write-Host "[Time Total: $Execute]" -ForegroundColor Red if ($ExitCode) { Exit 1 } } } function New-ConfigurationArtefact { <# .SYNOPSIS Tells the module to create artefact of specified type .DESCRIPTION Tells the module to create artefact of specified type There can be multiple artefacts created (even of same type) At least one packed artefact is required for publishing to GitHub .PARAMETER Type There are 4 types of artefacts: - Unpacked - unpacked module (useful for testing) - Packed - packed module (as zip) - usually used for publishing to GitHub or copying somewhere - Script - script that is module in form of PS1 without PSD1 - only applicable to very simple modules - PackedScript - packed module (as zip) that is script that is module in form of PS1 without PSD1 - only applicable to very simple modules .PARAMETER ID Optional ID of the artefact. To be used by New-ConfigurationPublish cmdlet If not specified, the first packed artefact will be used for publishing to GitHub .PARAMETER Enable Enable artefact creation. By default artefact creation is disabled. .PARAMETER IncludeTagName Include tag name in artefact name. By default tag name is not included. Alternatively you can provide ArtefactName parameter to specify your own artefact name (with or without TagName) .PARAMETER Path Path where artefact will be created. Please choose a separate directory for each artefact type, as logic may be interfering one another. .PARAMETER AddRequiredModules Add required modules to artefact by copying them over. By default required modules are not added. .PARAMETER ModulesPath Path where main module or required module (if not specified otherwise in RequiredModulesPath) will be copied to. By default it will be put in the Path folder if not specified .PARAMETER RequiredModulesPath Path where required modules will be copied to. By default it will be put in the Path folder if not specified. If ModulesPath is specified, but RequiredModulesPath is not specified it will be put into ModulesPath folder. .PARAMETER CopyDirectories Provide Hashtable of directories to copy to artefact. Key is source directory, value is destination directory. .PARAMETER CopyFiles Provide Hashtable of files to copy to artefact. Key is source file, value is destination file. .PARAMETER CopyDirectoriesRelative Define if destination directories should be relative to artefact root. By default they are not. .PARAMETER CopyFilesRelative Define if destination files should be relative to artefact root. By default they are not. .PARAMETER Clear Clear artefact directory before creating artefact. By default artefact directory is not cleared. .PARAMETER ArtefactName The name of the artefact. If not specified, the default name will be used. You can use following variables that will be replaced with actual values: - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0 - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1 - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1 - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0 .EXAMPLE New-ConfigurationArtefact -Type Unpacked -Enable -Path "$PSScriptRoot\..\Artefacts\Unpacked" -RequiredModulesPath "$PSScriptRoot\..\Artefacts\Unpacked\Modules" .EXAMPLE # standard artefact, packed with tag name without any additional modules or required modules New-ConfigurationArtefact -Type Packed -Enable -Path "$PSScriptRoot\..\Artefacts\Packed" -IncludeTagName .EXAMPLE # Create artefact in form of a script. This is useful for very simple modules that should be just single PS1 file New-ConfigurationArtefact -Type Script -Enable -Path "$PSScriptRoot\..\Artefacts\Script" -IncludeTagName .EXAMPLE # Create artefact in form of a script. This is useful for very simple modules that should be just single PS1 file # But additionally pack it into zip fileĄŚż$%# New-ConfigurationArtefact -Type ScriptPacked -Enable -Path "$PSScriptRoot\..\Artefacts\ScriptPacked" -ArtefactName "Script-<ModuleName>-$((Get-Date).ToString('yyyy-MM-dd')).zip" .NOTES General notes #> [CmdletBinding()] param( [Parameter(Position = 0)][ScriptBlock] $ScriptMerge, [Parameter(Mandatory)][ValidateSet('Unpacked', 'Packed', 'Script', 'ScriptPacked')][string] $Type, [switch] $Enable, [switch] $IncludeTagName, [string] $Path, [alias('RequiredModules')][switch] $AddRequiredModules, [string] $ModulesPath, [string] $RequiredModulesPath, [System.Collections.IDictionary] $CopyDirectories, [System.Collections.IDictionary] $CopyFiles, [switch] $CopyDirectoriesRelative, [switch] $CopyFilesRelative, [switch] $Clear, [string] $ArtefactName, [string] $ID ) # if ($Type -eq 'Packed') { # $ArtefactType = 'Releases' # } else { # $ArtefactType = 'ReleasesUnpacked' # } $Artefact = [ordered ] @{ Type = $Type #$ArtefactType Configuration = [ordered] @{ Type = $Type RequiredModules = [ordered] @{ } } } if ($PSBoundParameters.ContainsKey('Enable')) { $Artefact['Configuration']['Enabled'] = $Enable # [ordered] @{ # Type = $ArtefactType # $ArtefactType = [ordered] @{ # Enabled = $Enable # } # } } if ($PSBoundParameters.ContainsKey('IncludeTagName')) { $Artefact['Configuration']['IncludeTagName'] = $IncludeTagName # [ordered] @{ # Type = $ArtefactType # $ArtefactType = [ordered] @{ # IncludeTagName = $IncludeTagName # } # } } if ($PSBoundParameters.ContainsKey('Path')) { $Artefact['Configuration']['Path'] = $Path # [ordered] @{ # Type = $ArtefactType # $ArtefactType = [ordered] @{ # Path = $Path # } # } } if ($PSBoundParameters.ContainsKey('RequiredModulesPath')) { $Artefact['Configuration']['RequiredModules']['Path'] = $RequiredModulesPath # [ordered] @{ # Type = $ArtefactType # $ArtefactType = [ordered] @{ # RequiredModules = @{ # Path = $RequiredModulesPath # } # } # } } if ($PSBoundParameters.ContainsKey('AddRequiredModules')) { $Artefact['Configuration']['RequiredModules']['Enabled'] = $true # [ordered] @{ # Type = $ArtefactType # $ArtefactType = [ordered] @{ # RequiredModules = @{ # Enabled = $true # } # } # } } if ($PSBoundParameters.ContainsKey('ModulesPath')) { $Artefact['Configuration']['RequiredModules']['ModulesPath'] = $ModulesPath # [ordered] @{ # Type = $ArtefactType # $ArtefactType = [ordered] @{ # RequiredModules = @{ # ModulesPath = $ModulesPath # } # } # } } if ($PSBoundParameters.ContainsKey('CopyDirectories')) { $Artefact['Configuration']['DirectoryOutput'] = $CopyDirectories # [ordered] @{ # Type = $ArtefactType # $ArtefactType = [ordered] @{ # DirectoryOutput = $CopyDirectories # } # } } if ($PSBoundParameters.ContainsKey('CopyDirectoriesRelative')) { $Artefact['Configuration']['DestinationDirectoriesRelative'] = $CopyDirectoriesRelative.IsPresent # [ordered] @{ # Type = $ArtefactType # $ArtefactType = [ordered] @{ # DestinationDirectoriesRelative = $CopyDirectoriesRelative.IsPresent # } # } } if ($PSBoundParameters.ContainsKey('CopyFiles')) { $Artefact['Configuration']['FilesOutput'] = $CopyFiles # [ordered] @{ # Type = $ArtefactType # $ArtefactType = [ordered] @{ # FilesOutput = $CopyFiles # } # } } if ($PSBoundParameters.ContainsKey('CopyFilesRelative')) { $Artefact['Configuration']['DestinationFilesRelative'] = $CopyFilesRelative.IsPresent # [ordered] @{ # Type = $ArtefactType # $ArtefactType = [ordered] @{ # DestinationFilesRelative = $CopyFilesRelative.IsPresent # } # } } if ($PSBoundParameters.ContainsKey('Clear')) { $Artefact['Configuration']['Clear'] = $Clear # [ordered] @{ # Type = $ArtefactType # $ArtefactType = [ordered] @{ # Clear = $Clear # } # } } if ($PSBoundParameters.ContainsKey('ArtefactName')) { $Artefact['Configuration']['ArtefactName'] = $ArtefactName # [ordered] @{ # Type = $ArtefactType # $ArtefactType = [ordered] @{ # ArtefactName = $ArtefactName # } # } } if ($PSBoundParameters.ContainsKey('ScriptMerge')) { try { $Artefact['Configuration']['ScriptMerge'] = Invoke-Formatter -ScriptDefinition $ScriptMerge.ToString() } catch { Write-Text -Text "[i] Unable to format merge script provided by user. Error: $($_.Exception.Message). Using original script." -Color Red $Artefact['Configuration']['ScriptMerge'] = $ScriptMerge.ToString() } } if ($PSBoundParameters.ContainsKey('ID')) { $Artefact['Configuration']['ID'] = $ID } $Artefact } function New-ConfigurationBuild { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER Enable Parameter description .PARAMETER DeleteTargetModuleBeforeBuild Parameter description .PARAMETER MergeModuleOnBuild Parameter description .PARAMETER MergeFunctionsFromApprovedModules Parameter description .PARAMETER SignModule Parameter description .PARAMETER DotSourceClasses Parameter description .PARAMETER DotSourceLibraries Parameter description .PARAMETER SeparateFileLibraries Parameter description .PARAMETER RefreshPSD1Only Parameter description .PARAMETER UseWildcardForFunctions Parameter description .PARAMETER LocalVersioning Parameter description .PARAMETER DoNotAttemptToFixRelativePaths Configures module builder to not replace $PSScriptRoot\..\ with $PSScriptRoot\ This is useful if you have a module that has a lot of relative paths that are required when using Private/Public folders, but for merge process those are not supposed to be there as the paths change. By default module builder will attempt to fix it. This option disables this functionality. Best practice is to use $MyInvocation.MyCommand.Module.ModuleBase or similar instead of relative paths. .PARAMETER MergeLibraryDebugging Parameter description .PARAMETER ResolveBinaryConflicts Parameter description .PARAMETER ResolveBinaryConflictsName Parameter description .PARAMETER CertificateThumbprint Parameter description .PARAMETER CertificatePFXPath Parameter description .PARAMETER CertificatePFXBase64 Parameter description .PARAMETER CertificatePFXPassword Parameter description .PARAMETER NETConfiguration Parameter description .PARAMETER NETFramework Parameter description .PARAMETER NETProjectName Parameter description .EXAMPLE An example .NOTES General notes #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] param( [switch] $Enable, [switch] $DeleteTargetModuleBeforeBuild, [switch] $MergeModuleOnBuild, [switch] $MergeFunctionsFromApprovedModules, [switch] $SignModule, [switch] $DotSourceClasses, [switch] $DotSourceLibraries, [switch] $SeparateFileLibraries, [switch] $RefreshPSD1Only, [switch] $UseWildcardForFunctions, [switch] $LocalVersioning, [switch] $DoNotAttemptToFixRelativePaths, [switch] $MergeLibraryDebugging, [switch] $ResolveBinaryConflicts, [string] $ResolveBinaryConflictsName, [string] $CertificateThumbprint, [string] $CertificatePFXPath, [string] $CertificatePFXBase64, [string] $CertificatePFXPassword, #[switch] $NETBuild, [ValidateSet('Release', 'Debug')][string] $NETConfiguration, # may need to allow user choice [string[]] $NETFramework, [string] $NETProjectName ) if ($PSBoundParameters.ContainsKey('Enable')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ Enable = $Enable.IsPresent } } } if ($PSBoundParameters.ContainsKey('DeleteTargetModuleBeforeBuild')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ DeleteBefore = $DeleteTargetModuleBeforeBuild.IsPresent } } } if ($PSBoundParameters.ContainsKey('MergeModuleOnBuild')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ Merge = $MergeModuleOnBuild.IsPresent } } } if ($PSBoundParameters.ContainsKey('MergeFunctionsFromApprovedModules')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ MergeMissing = $MergeFunctionsFromApprovedModules.IsPresent } } } if ($PSBoundParameters.ContainsKey('SignModule')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ SignMerged = $SignModule.IsPresent } } } if ($PSBoundParameters.ContainsKey('DotSourceClasses')) { # only when there are classes [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ ClassesDotSource = $DotSourceClasses.IsPresent } } } if ($PSBoundParameters.ContainsKey('DotSourceLibraries')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ LibraryDotSource = $DotSourceLibraries.IsPresent } } } if ($PSBoundParameters.ContainsKey('SeparateFileLibraries')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ LibrarySeparateFile = $SeparateFileLibraries.IsPresent } } } if ($PSBoundParameters.ContainsKey('RefreshPSD1Only')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ RefreshPSD1Only = $RefreshPSD1Only.IsPresent } } } if ($PSBoundParameters.ContainsKey('UseWildcardForFunctions')) { # Applicable only for non-merge/publish situation # It's simply to make life easier during debugging # It makes all functions/aliases exportable [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ UseWildcardForFunctions = $UseWildcardForFunctions.IsPresent } } } if ($PSBoundParameters.ContainsKey('LocalVersioning')) { # bumps version in PSD1 on every build [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ LocalVersion = $LocalVersioning.IsPresent } } } if ($PSBoundParameters.ContainsKey('DoNotAttemptToFixRelativePaths')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ DoNotAttemptToFixRelativePaths = $DoNotAttemptToFixRelativePaths.IsPresent } } } if ($PSBoundParameters.ContainsKey('MergeLibraryDebugging')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ DebugDLL = $MergeLibraryDebugging.IsPresent } } } if ($PSBoundParameters.ContainsKey('ResolveBinaryConflictsName')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ ResolveBinaryConflicts = @{ ProjectName = $ResolveBinaryConflictsName } } } } elseif ($PSBoundParameters.ContainsKey('ResolveBinaryConflicts')) { [ordered] @{ Type = 'Build' BuildModule = [ordered] @{ ResolveBinaryConflicts = $ResolveBinaryConflicts.IsPresent } } } if ($PSBoundParameters.ContainsKey('CertificateThumbprint')) { [ordered] @{ Type = 'Options' Options = [ordered] @{ Signing = [ordered] @{ CertificateThumbprint = $CertificateThumbprint } } } } elseif ($PSBoundParameters.ContainsKey('CertificatePFXPath')) { if ($PSBoundParameters.ContainsKey('CertificatePFXPassword')) { # this is added to support users direct PFX [ordered] @{ Type = 'Options' Options = [ordered] @{ Signing = [ordered] @{ CertificatePFXPath = $CertificatePFXPath CertificatePFXPassword = $CertificatePFXPassword } } } } else { throw "CertificatePFXPassword is required when using CertificatePFXPath" } } elseif ($PSBoundParameters.ContainsKey('CertificatePFXBase64')) { if ($PSBoundParameters.ContainsKey('CertificatePFXPassword')) { # this is added to support GitHub/Azure DevOps Secrets [ordered] @{ Type = 'Options' Options = [ordered] @{ Signing = [ordered] @{ CertificatePFXBase64 = $CertificatePFXBase64 CertificatePFXPassword = $CertificatePFXPassword } } } } else { throw "CertificatePFXPassword is required when using CertificatePFXBase64" } } # Build libraries configuration, this is useful if you have C# project that you want to build # so libraries are autogenerated and you can use them in your PowerShell module # $BuildLibraries = @{ # Enable = $false # build once every time nuget gets updated # Configuration = 'Release' # Framework = 'netstandard2.0', 'net472' # ProjectName = 'ImagePlayground.PowerShell' # } if ($PSBoundParameters.ContainsKey('NETConfiguration')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ Enable = $true Configuration = $NETConfiguration } } } if ($PSBoundParameters.ContainsKey('NETFramework')) { [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ Enable = $true Framework = $NETFramework } } } if ($PSBoundParameters.ContainsKey('NETProjectName')) { # this is optional as normaly it will assume same name as project name [ordered] @{ Type = 'BuildLibraries' BuildLibraries = [ordered] @{ ProjectName = $NETProjectName } } } } function New-ConfigurationCommand { [CmdletBinding()] param( [string] $ModuleName, [string[]] $CommandName ) $Configuration = [ordered] @{ Type = 'Command' Configuration = [ordered] @{ ModuleName = $ModuleName CommandName = $CommandName } } $Configuration } function New-ConfigurationDocumentation { <# .SYNOPSIS Enables or disables creation of documentation from the module using PlatyPS .DESCRIPTION Enables or disables creation of documentation from the module using PlatyPS .PARAMETER Enable Enables creation of documentation from the module. If not specified, the documentation will not be created. .PARAMETER StartClean Removes all files from the documentation folder before creating new documentation. Otherwise the `Update-MarkdownHelpModule` will be used to update the documentation. .PARAMETER UpdateWhenNew Updates the documentation right after running `New-MarkdownHelp` due to platyPS bugs. .PARAMETER Path Path to the folder where documentation will be created. .PARAMETER PathReadme Path to the readme file that will be used for the documentation. .EXAMPLE New-ConfigurationDocumentation -Enable:$false -StartClean -UpdateWhenNew -PathReadme 'Docs\Readme.md' -Path 'Docs' .EXAMPLE New-ConfigurationDocumentation -Enable -PathReadme 'Docs\Readme.md' -Path 'Docs' .NOTES General notes #> [CmdletBinding()] param( [switch] $Enable, [switch] $StartClean, [switch] $UpdateWhenNew, [Parameter(Mandatory)][string] $Path, [Parameter(Mandatory)][string] $PathReadme ) if ($Path -or $PathReadme) { $Documentation = [ordered] @{ Path = $Path PathReadme = $PathReadme } $Option = @{ Type = 'Documentation' Configuration = $Documentation } $Option } if ($Enable -or $StartClean -or $UpdateWhenNew) { $BuildDocumentation = @{ Enable = $Enable StartClean = $StartClean UpdateWhenNew = $UpdateWhenNew } $Option = @{ Type = 'BuildDocumentation' Configuration = $BuildDocumentation } $Option } } function New-ConfigurationExecute { [CmdletBinding()] param( ) } function New-ConfigurationFormat { [CmdletBinding()] param( [Parameter(Mandatory)] [validateSet( 'OnMergePSM1', 'OnMergePSD1', 'DefaultPSM1', 'DefaultPSD1' #"DefaultPublic", 'DefaultPrivate', 'DefaultOther' )][string[]]$ApplyTo, [switch] $EnableFormatting, [validateSet('None', 'Asc', 'Desc')][string] $Sort, [switch] $RemoveComments, [switch] $PlaceOpenBraceEnable, [switch] $PlaceOpenBraceOnSameLine, [switch] $PlaceOpenBraceNewLineAfter, [switch] $PlaceOpenBraceIgnoreOneLineBlock, [switch] $PlaceCloseBraceEnable, [switch] $PlaceCloseBraceNewLineAfter, [switch] $PlaceCloseBraceIgnoreOneLineBlock, [switch] $PlaceCloseBraceNoEmptyLineBefore, [switch] $UseConsistentIndentationEnable, [ValidateSet('space', 'tab')][string] $UseConsistentIndentationKind, [ValidateSet('IncreaseIndentationAfterEveryPipeline', 'NoIndentation')][string] $UseConsistentIndentationPipelineIndentation, [int] $UseConsistentIndentationIndentationSize, [switch] $UseConsistentWhitespaceEnable, [switch] $UseConsistentWhitespaceCheckInnerBrace, [switch] $UseConsistentWhitespaceCheckOpenBrace, [switch] $UseConsistentWhitespaceCheckOpenParen, [switch] $UseConsistentWhitespaceCheckOperator, [switch] $UseConsistentWhitespaceCheckPipe, [switch] $UseConsistentWhitespaceCheckSeparator, [switch] $AlignAssignmentStatementEnable, [switch] $AlignAssignmentStatementCheckHashtable, [switch] $UseCorrectCasingEnable, [ValidateSet('Minimal', 'Native')][string] $PSD1Style ) $SettingsCount = 0 $Options = [ordered] @{ Merge = [ordered] @{ #Sort = $Sort } Standard = [ordered] @{ #Sort = $Sort } } foreach ($Apply in $ApplyTo) { $Formatting = [ordered] @{} if ($PSBoundParameters.ContainsKey('RemoveComments')) { $Formatting.RemoveComments = $RemoveComments.IsPresent } $Formatting.FormatterSettings = [ordered] @{ IncludeRules = @( if ($PlaceOpenBraceEnable) { 'PSPlaceOpenBrace' } if ($PlaceCloseBraceEnable) { 'PSPlaceCloseBrace' } if ($UseConsistentIndentationEnable) { 'PSUseConsistentIndentation' } if ($UseConsistentWhitespaceEnable) { 'PSUseConsistentWhitespace' } if ($AlignAssignmentStatementEnable) { 'PSAlignAssignmentStatement' } if ($UseCorrectCasingEnable) { 'PSUseCorrectCasing' } ) Rules = [ordered] @{} } if ($PlaceOpenBraceEnable) { $Formatting.FormatterSettings.Rules.PSPlaceOpenBrace = [ordered] @{ Enable = $true OnSameLine = $PlaceOpenBraceOnSameLine.IsPresent NewLineAfter = $PlaceOpenBraceNewLineAfter.IsPresent IgnoreOneLineBlock = $PlaceOpenBraceIgnoreOneLineBlock.IsPresent } } if ($PlaceCloseBraceEnable) { $Formatting.FormatterSettings.Rules.PSPlaceCloseBrace = [ordered] @{ Enable = $true NewLineAfter = $PlaceCloseBraceNewLineAfter.IsPresent IgnoreOneLineBlock = $PlaceCloseBraceIgnoreOneLineBlock.IsPresent NoEmptyLineBefore = $PlaceCloseBraceNoEmptyLineBefore.IsPresent } } if ($UseConsistentIndentationEnable) { $Formatting.FormatterSettings.Rules.PSUseConsistentIndentation = [ordered] @{ Enable = $true Kind = $UseConsistentIndentationKind PipelineIndentation = $UseConsistentIndentationPipelineIndentation IndentationSize = $UseConsistentIndentationIndentationSize } } if ($UseConsistentWhitespaceEnable) { $Formatting.FormatterSettings.Rules.PSUseConsistentWhitespace = [ordered] @{ Enable = $true CheckInnerBrace = $UseConsistentWhitespaceCheckInnerBrace.IsPresent CheckOpenBrace = $UseConsistentWhitespaceCheckOpenBrace.IsPresent CheckOpenParen = $UseConsistentWhitespaceCheckOpenParen.IsPresent CheckOperator = $UseConsistentWhitespaceCheckOperator.IsPresent CheckPipe = $UseConsistentWhitespaceCheckPipe.IsPresent CheckSeparator = $UseConsistentWhitespaceCheckSeparator.IsPresent } } if ($AlignAssignmentStatementEnable) { $Formatting.FormatterSettings.Rules.PSAlignAssignmentStatement = [ordered] @{ Enable = $true CheckHashtable = $AlignAssignmentStatementCheckHashtable.IsPresent } } if ($UseCorrectCasingEnable) { $Formatting.FormatterSettings.Rules.PSUseCorrectCasing = [ordered] @{ Enable = $true } } Remove-EmptyValue -Hashtable $Formatting.FormatterSettings -Recursive if ($Formatting.FormatterSettings.Keys.Count -eq 0) { $null = $Formatting.Remove('FormatterSettings') } if ($Formatting.Count -gt 0 -or $EnableFormatting) { $SettingsCount++ $Formatting.Enabled = $true if ($Apply -eq 'OnMergePSM1') { $Options.Merge.FormatCodePSM1 = $Formatting } elseif ($Apply -eq 'OnMergePSD1') { $Options.Merge.FormatCodePSD1 = $Formatting } elseif ($Apply -eq 'DefaultPSM1') { $Options.Standard.FormatCodePSM1 = $Formatting } elseif ($Apply -eq 'DefaultPSD1') { $Options.Standard.FormatCodePSD1 = $Formatting } elseif ($Apply -eq 'DefaultPublic') { $Options.Standard.FormatCodePublic = $Formatting } elseif ($Apply -eq 'DefaultPrivate') { $Options.Standard.FormatCodePrivate = $Formatting } elseif ($Apply -eq 'DefaultOther') { $Options.Standard.FormatCodeOther = $Formatting } else { throw "Unknown ApplyTo: $Apply" } } if ($PSD1Style) { if ($Apply -eq 'OnMergePSD1') { $SettingsCount++ $Options['Merge']['Style'] = [ordered] @{} $Options['Merge']['Style']['PSD1'] = $PSD1Style } elseif ($Apply -eq 'DefaultPSD1') { $SettingsCount++ $Options['Standard']['Style'] = [ordered] @{} $Options['Standard']['Style']['PSD1'] = $PSD1Style } } } # Set formatting options if present if ($SettingsCount -gt 0) { $Output = [ordered] @{ Type = 'Formatting' Options = $Options } $Output } } # $Config = New-ConfigurationFormat -ApplyTo OnMergePSD1, DefaultPSD1 -PSD1Style Minimal # $Config.Options function New-ConfigurationImportModule { [CmdletBinding()] param( [switch] $ImportSelf, [switch] $ImportRequiredModules ) if ($PSBoundParameters.Keys.Contains('ImportSelf')) { $Output = [ordered] @{ Type = 'ImportModules' ImportModules = [ordered] @{ Self = $ImportSelf } } $Output } if ($PSBoundParameters.Keys.Contains('ImportRequiredModules')) { $Output = [ordered] @{ Type = 'ImportModules' ImportModules = [ordered] @{ RequiredModules = $ImportRequiredModules } } $Output } if ($VerbosePreference) { $Output = [ordered] @{ Type = 'ImportModules' ImportModules = [ordered] @{ Verbose = $true } } $Output } } function New-ConfigurationInformation { [cmdletbinding()] param( [string] $FunctionsToExportFolder, [string] $AliasesToExportFolder, [string[]] $ExcludeFromPackage, [string[]] $IncludeRoot, [string[]] $IncludePS1, [string[]] $IncludeAll, [scriptblock] $IncludeCustomCode, [System.Collections.IDictionary] $IncludeToArray, [string] $LibrariesCore, [string] $LibrariesDefault, [string] $LibrariesStandard ) $Configuration = [ordered] @{ FunctionsToExportFolder = $FunctionsToExportFolder AliasesToExportFolder = $AliasesToExportFolder ExcludeFromPackage = $ExcludeFromPackage IncludeRoot = $IncludeRoot IncludePS1 = $IncludePS1 IncludeAll = $IncludeAll IncludeCustomCode = $IncludeCustomCode IncludeToArray = $IncludeToArray LibrariesCore = $LibrariesCore LibrariesDefault = $LibrariesDefault LibrariesStandard = $LibrariesStandard } Remove-EmptyValue -Hashtable $Configuration $Option = @{ Type = 'Information' Configuration = $Configuration } $Option } function New-ConfigurationManifest { [CmdletBinding()] param( [Parameter(Mandatory)][string] $ModuleVersion, [ValidateSet('Desktop', 'Core')][string[]] $CompatiblePSEditions = @('Desktop', 'Core'), [Parameter(Mandatory)][string] $GUID, [Parameter(Mandatory)][string] $Author, [string] $CompanyName, [string] $Copyright, [string] $Description, [string] $PowerShellVersion = '5.1', [string[]] $Tags, [string] $IconUri, [string] $ProjectUri, [string] $DotNetFrameworkVersion, [string] $LicenseUri, [alias('PrereleaseTag')][string] $Prerelease ) $Manifest = [ordered] @{ ModuleVersion = $ModuleVersion CompatiblePSEditions = @($CompatiblePSEditions) GUID = $GUID Author = $Author CompanyName = $CompanyName Copyright = $Copyright Description = $Description PowerShellVersion = $PowerShellVersion Tags = $Tags IconUri = $IconUri ProjectUri = $ProjectUri DotNetFrameworkVersion = $DotNetFrameworkVersion LicenseUri = $LicenseUri Prerelease = $Prerelease } Remove-EmptyValue -Hashtable $Manifest $Option = @{ Type = 'Manifest' Configuration = $Manifest } $Option } function New-ConfigurationModule { <# .SYNOPSIS Provides a way to configure Required Modules or External Modules that will be used in the project. .DESCRIPTION Provides a way to configure Required Modules or External Modules that will be used in the project. .PARAMETER Type Choose between RequiredModule, ExternalModule and ApprovedModule, where RequiredModule is the default. .PARAMETER Name Name of PowerShell module that you want your module to depend on. .PARAMETER Version Version of PowerShell module that you want your module to depend on. If you don't specify a version, any version of the module is acceptable. You can also use word 'Latest' to specify that you want to use the latest version of the module, and the module will be pickup up latest version available on the system. .PARAMETER RequiredVersion RequiredVersion of PowerShell module that you want your module to depend on. This forces the module to require this specific version. When using Version, the module will be picked up if it's equal or higher than the version specified. When using RequiredVersion, the module will be picked up only if it's equal to the version specified. .PARAMETER Guid Guid of PowerShell module that you want your module to depend on. If you don't specify a Guid, any Guid of the module is acceptable, but it is recommended to specify it. Alternatively you can use word 'Auto' to specify that you want to use the Guid of the module, and the module GUID .EXAMPLE # Add standard module dependencies (directly, but can be used with loop as well) New-ConfigurationModule -Type RequiredModule -Name 'platyPS' -Guid 'Auto' -Version 'Latest' New-ConfigurationModule -Type RequiredModule -Name 'powershellget' -Guid 'Auto' -Version 'Latest' New-ConfigurationModule -Type RequiredModule -Name 'PSScriptAnalyzer' -Guid 'Auto' -Version 'Latest' .EXAMPLE # Add external module dependencies, using loop for simplicity foreach ($Module in @('Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Archive', 'Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Security')) { New-ConfigurationModule -Type ExternalModule -Name $Module } .EXAMPLE # Add approved modules, that can be used as a dependency, but only when specific function from those modules is used # And on that time only that function and dependant functions will be copied over # Keep in mind it has it's limits when "copying" functions such as it should not depend on DLLs or other external files New-ConfigurationModule -Type ApprovedModule -Name 'PSSharedGoods', 'PSWriteColor', 'Connectimo', 'PSUnifi', 'PSWebToolbox', 'PSMyPassword' .NOTES General notes #> [CmdletBinding()] param( [validateset('RequiredModule', 'ExternalModule', 'ApprovedModule')] $Type = 'RequiredModule', [Parameter(Mandatory)][string[]] $Name, [string] $Version, [string] $RequiredVersion, [string] $Guid ) foreach ($N in $Name) { if ($Type -eq 'ApprovedModule') { # Approved modules are simplified, as they don't have any other options $Configuration = $N } else { $ModuleInformation = [ordered] @{ ModuleName = $N ModuleVersion = $Version RequiredVersion = $RequiredVersion Guid = $Guid } if ($Version -and $RequiredVersion) { throw 'You cannot use both Version and RequiredVersion at the same time for the same module. Please choose one or the other (New-ConfigurationModule) ' } Remove-EmptyValue -Hashtable $ModuleInformation if ($ModuleInformation.Count -eq 0) { return } elseif ($ModuleInformation.Count -eq 1 -and $ModuleInformation.Contains('ModuleName')) { $Configuration = $N } else { $Configuration = $ModuleInformation } } $Option = @{ Type = $Type Configuration = $Configuration } $Option } } Register-ArgumentCompleter -CommandName New-ConfigurationModule -ParameterName Version -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) 'Auto', 'Latest' | Where-Object { $_ -like "*$wordToComplete*" } } Register-ArgumentCompleter -CommandName New-ConfigurationModule -ParameterName Guid -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) 'Auto', 'Latest' | Where-Object { $_ -like "*$wordToComplete*" } } function New-ConfigurationModuleSkip { <# .SYNOPSIS Provides a way to ignore certain commands or modules during build process and continue module building on errors. .DESCRIPTION Provides a way to ignore certain commands or modules during build process and continue module building on errors. During build if a build module can't find require module or command it will fail the build process to prevent incomplete module from being created. This option allows to skip certain modules or commands and continue building the module. This is useful for commands we know are not available on all systems, or we get them different way. .PARAMETER IgnoreModuleName Ignore module name or names. If the module is not available on the system it will be ignored and build process will continue. .PARAMETER IgnoreFunctionName Ignore function name or names. If the function is not available in the module it will be ignored and build process will continue. .PARAMETER Force This switch will force build process to continue even if the module or command is not available (aka you know what you are doing) .EXAMPLE New-ConfigurationModuleSkip -IgnoreFunctionName 'Invoke-Formatter', 'Find-Module' -IgnoreModuleName 'platyPS' .NOTES General notes #> [CmdletBinding()] param( [string[]] $IgnoreModuleName, [string[]] $IgnoreFunctionName, [switch] $Force ) $Configuration = [ordered] @{ Type = 'ModuleSkip' Configuration = [ordered] @{ IgnoreModuleName = $IgnoreModuleName IgnoreFunctionName = $IgnoreFunctionName Force = $Force } } Remove-EmptyValue -Hashtable $Configuration.Configuration $Configuration } function New-ConfigurationPublish { <# .SYNOPSIS Provide a way to configure publishing to PowerShell Gallery or GitHub .DESCRIPTION Provide a way to configure publishing to PowerShell Gallery or GitHub You can configure publishing to both at the same time You can publish to multiple PowerShellGalleries at the same time as well You can have multiple GitHub configurations at the same time as well .PARAMETER Type Choose between PowerShellGallery and GitHub .PARAMETER FilePath API Key to be used for publishing to GitHub or PowerShell Gallery in clear text in file .PARAMETER UserName When used for GitHub this parameter is required to know to which repository to publish. This parameter is not used for PSGallery publishing .PARAMETER RepositoryName When used for PowerShellGallery publishing this parameter provides a way to overwrite default PowerShellGallery and publish to a different repository When not used, the default PSGallery will be used. When used for GitHub publishing this parameter provides a way to overwrite default repository name and publish to a different repository When not used, the default repository name will be used, that matches the module name .PARAMETER ApiKey API Key to be used for publishing to GitHub or PowerShell Gallery in clear text .PARAMETER Enabled Enable publishing to GitHub or PowerShell Gallery .PARAMETER PreReleaseTag Allow to publish to GitHub as pre-release. By default it will be published as release .PARAMETER OverwriteTagName Allow to overwrite tag name when publishing to GitHub. By default "v<ModuleVersion>" will be used i.e v1.0.0 You can use following variables that will be replaced with actual values: - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0 - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1 - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1 - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0 .PARAMETER DoNotMarkAsPreRelease Allow to publish to GitHub as release even if pre-release tag is set on the module version. By default it will be published as pre-release if pre-release tag is set. This setting prevents it. .PARAMETER Force Allow to publish lower version of module on PowerShell Gallery. By default it will fail if module with higher version already exists. .PARAMETER ID Optional ID of the artefact. If not specified, the default packed artefact will be used. If no packed artefact is specified, the first packed artefact will be used (if enabled) If no packed artefact is enabled, the publishing will fail .EXAMPLE New-ConfigurationPublish -Type PowerShellGallery -FilePath 'C:\Support\Important\PowerShellGalleryAPI.txt' -Enabled:$true .EXAMPLE New-ConfigurationPublish -Type GitHub -FilePath 'C:\Support\Important\GitHubAPI.txt' -UserName 'EvotecIT' -Enabled:$true -ID 'ToGitHub' .NOTES General notes #> [CmdletBinding()] param( [Parameter(Mandatory, ParameterSetName = 'ApiKey')] [Parameter(Mandatory, ParameterSetName = 'ApiFromFile')] [ValidateSet('PowerShellGallery', 'GitHub')][string] $Type, [Parameter(Mandatory, ParameterSetName = 'ApiFromFile')][string] $FilePath, [Parameter(Mandatory, ParameterSetName = 'ApiKey')][string] $ApiKey, [Parameter(ParameterSetName = 'ApiKey')] [Parameter(ParameterSetName = 'ApiFromFile')] [string] $UserName, [Parameter(ParameterSetName = 'ApiKey')] [Parameter(ParameterSetName = 'ApiFromFile')] [string] $RepositoryName, [Parameter(ParameterSetName = 'ApiKey')] [Parameter(ParameterSetName = 'ApiFromFile')] [switch] $Enabled, # [Parameter(ParameterSetName = 'ApiKey')] # [Parameter(ParameterSetName = 'ApiFromFile')] # [string] $PreReleaseTag, [Parameter(ParameterSetName = 'ApiKey')] [Parameter(ParameterSetName = 'ApiFromFile')] [string] $OverwriteTagName, [Parameter(ParameterSetName = 'ApiKey')] [Parameter(ParameterSetName = 'ApiFromFile')] [switch] $Force, [Parameter(ParameterSetName = 'ApiKey')] [Parameter(ParameterSetName = 'ApiFromFile')] [string] $ID, [Parameter(ParameterSetName = 'ApiKey')] [Parameter(ParameterSetName = 'ApiFromFile')] [switch] $DoNotMarkAsPreRelease ) if ($FilePath) { $ApiKeyToUse = Get-Content -Path $FilePath -ErrorAction Stop -Encoding UTF8 } else { $ApiKeyToUse = $ApiKey } if ($Type -eq 'PowerShellGallery') { $TypeToUse = 'GalleryNuget' } elseif ($Type -eq 'GitHub') { $TypeToUse = 'GitHubNuget' if (-not $UserName) { throw 'UserName is required for GitHub. Please fix New-ConfigurationPublish and provide UserName' } } else { return } $Settings = [ordered] @{ Type = $TypeToUse Configuration = [ordered] @{ Type = $Type ApiKey = $ApiKeyToUse ID = $ID Enabled = $Enabled UserName = $UserName RepositoryName = $RepositoryName Force = $Force.IsPresent OverwriteTagName = $OverwriteTagName DoNotMarkAsPreRelease = $DoNotMarkAsPreRelease.IsPresent Verbose = $VerbosePreference } } Remove-EmptyValue -Hashtable $Settings -Recursive 2 $Settings } function New-ConfigurationTest { [CmdletBinding()] param( #[Parameter(Mandatory)][ValidateSet('BeforeMerge', 'AfterMerge')][string[]] $When, [Parameter(Mandatory)][string] $TestsPath, [switch] $Enable, [switch] $Force ) if ($Enable) { # lets temporary set it here only, not sure if it's worth before merge $When = 'AfterMerge' foreach ($W in $When) { $Configuration = [ordered] @{ Type = "Tests$W" Configuration = [ordered] @{ When = $W TestsPath = $TestsPath Force = $Force.ispresent } } Remove-EmptyValue -Hashtable $Configuration.Configuration $Configuration } } } function Register-Certificate { [cmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, ParameterSetName = 'PFX')][string] $CertificatePFX, [Parameter(Mandatory, ParameterSetName = 'Store')][ValidateSet('LocalMachine', 'CurrentUser')][string] $LocalStore, [alias('CertificateThumbprint')][Parameter(ParameterSetName = 'Store')][string] $Thumbprint, [Parameter(Mandatory)][string] $Path, [string] $TimeStampServer = 'http://timestamp.digicert.com', [ValidateSet('All', '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 -Message "Register-Certificate - No certificates found in store." return } elseif ($Cert.Count -eq 1) { $Certificate = $Cert } else { if ($Thumbprint) { $Certificate = $Cert | Where-Object { $_.Thumbprint -eq $Thumbprint } if (-not $Certificate) { Write-Warning -Message "Register-Certificate - No certificates found by that thumbprint" return } } else { $CodeError = "Get-ChildItem -Path Cert:\$LocalStore\My -CodeSigningCert" Write-Warning -Message "Register-Certificate - More than one certificate found in store. Provide Thumbprint for expected certificate" Write-Warning -Message "Register-Certificate - Use: $CodeError" $Cert return } } } elseif ($PSBoundParameters.Keys -contains 'CertificatePFX') { if (Test-Path -LiteralPath $CertificatePFX) { $Certificate = Get-PfxCertificate -FilePath $CertificatePFX if (-not $Certificate) { Write-Warning -Message "Register-Certificate - No certificates found for PFX" return } } } if ($Certificate -and $Path) { if (Test-Path -LiteralPath $Path) { Get-ChildItem -Path $Path -Filter * -Include $Include -Recurse -ErrorAction SilentlyContinue | Where-Object { ($_ | Get-AuthenticodeSignature).Status -eq 'NotSigned' } | Set-AuthenticodeSignature -Certificate $Certificate -TimestampServer $TimeStampServer -IncludeChain $IncludeChain -HashAlgorithm Sha256 } } } 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 Send-GitHubRelease { <# .SYNOPSIS Creates a new Release for the given GitHub repository. .DESCRIPTION Uses the GitHub API to create a new Release for a given repository. Allows you to specify all of the Release properties, such as the Tag, Name, Assets, and if it's a Draft or Prerelease or not. .PARAMETER GitHubUsername The username that the GitHub repository exists under. e.g. For the repository https://github.com/deadlydog/New-GitHubRelease, the username is 'deadlydog'. .PARAMETER GitHubRepositoryName The name of the repository to create the Release for. e.g. For the repository https://github.com/deadlydog/New-GitHubRelease, the repository name is 'New-GitHubRelease'. .PARAMETER GitHubAccessToken The Access Token to use as credentials for GitHub. Access tokens can be generated at https://github.com/settings/tokens. The access token will need to have the repo/public_repo permission on it for it to be allowed to create a new Release. .PARAMETER TagName The name of the tag to create at the Commitish. .PARAMETER ReleaseName The name to use for the new release. If blank, the TagName will be used. .PARAMETER ReleaseNotes The text describing the contents of the release. .PARAMETER AssetFilePaths The full paths of the files to include in the release. .PARAMETER Commitish Specifies the commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Unused if the Git tag already exists. Default: the repository's default branch (usually master). .PARAMETER IsDraft True to create a draft (unpublished) release, false to create a published one. Default: false .PARAMETER IsPreRelease True to identify the release as a prerelease. false to identify the release as a full release. Default: false .OUTPUTS A hash table with the following properties is returned: Succeeded = $true if the Release was created successfully and all assets were uploaded to it, $false if some part of the process failed. ReleaseCreationSucceeded = $true if the Release was created successfully (does not include asset uploads), $false if the Release was not created. AllAssetUploadsSucceeded = $true if all assets were uploaded to the Release successfully, $false if one of them failed, $null if there were no assets to upload. ReleaseUrl = The URL of the new Release that was created. ErrorMessage = A message describing what went wrong in the case that Succeeded is $false. .EXAMPLE # Import the module dynamically from the PowerShell Gallery. Use CurrentUser scope to avoid having to run as admin. Import-Module -Name New-GitHubRelease -Scope CurrentUser # Specify the parameters required to create the release. Do it as a hash table for easier readability. $newGitHubReleaseParameters = @{ GitHubUsername = 'deadlydog' GitHubRepositoryName = 'New-GitHubRelease' GitHubAccessToken = 'SomeLongHexidecimalString' ReleaseName = "New-GitHubRelease v1.0.0" TagName = "v1.0.0" ReleaseNotes = "This release contains the following changes: ..." AssetFilePaths = @('C:\MyProject\Installer.exe','C:\MyProject\Documentation.md') IsPreRelease = $false IsDraft = $true # Set to true when testing so we don't publish a real release (visible to everyone) by accident. } # Try to create the Release on GitHub and save the results. $result = New-GitHubRelease @newGitHubReleaseParameters # Provide some feedback to the user based on the results. if ($result.Succeeded -eq $true) { Write-Output "Release published successfully! View it at $($result.ReleaseUrl)" } elseif ($result.ReleaseCreationSucceeded -eq $false) { Write-Error "The release was not created. Error message is: $($result.ErrorMessage)" } elseif ($result.AllAssetUploadsSucceeded -eq $false) { Write-Error "The release was created, but not all of the assets were uploaded to it. View it at $($result.ReleaseUrl). Error message is: $($result.ErrorMessage)" } Attempt to create a new Release on GitHub, and provide feedback to the user indicating if it succeeded or not. .LINK Project home: https://github.com/deadlydog/New-GitHubRelease .NOTES Name: New-GitHubRelease Author: Daniel Schroeder (originally based on the script at https://github.com/majkinetor/au/blob/master/scripts/Github-CreateRelease.ps1) GitHub Release API Documentation: https://developer.github.com/v3/repos/releases/#create-a-release Version: 1.0.2 #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = "The username the repository is under (e.g. deadlydog).")] [string] $GitHubUsername, [Parameter(Mandatory = $true, HelpMessage = "The repository name to create the release in (e.g. Invoke-MsBuild).")] [string] $GitHubRepositoryName, [Parameter(Mandatory = $true, HelpMessage = "The Acess Token to use as credentials for GitHub.")] [string] $GitHubAccessToken, [Parameter(Mandatory = $true, HelpMessage = "The name of the tag to create at the the Commitish.")] [string] $TagName, [Parameter(Mandatory = $false, HelpMessage = "The name of the release. If blank, the TagName will be used.")] [string] $ReleaseName, [Parameter(Mandatory = $false, HelpMessage = "Text describing the contents of the tag.")] [string] $ReleaseNotes, [Parameter(Mandatory = $false, HelpMessage = "The full paths of the files to include in the release.")] [string[]] $AssetFilePaths, [Parameter(Mandatory = $false, HelpMessage = "Specifies the commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Unused if the Git tag already exists. Default: the repository's default branch (usually master).")] [string] $Commitish, [Parameter(Mandatory = $false, HelpMessage = "True to create a draft (unpublished) release, false to create a published one. Default: false")] [bool] $IsDraft = $false, [Parameter(Mandatory = $false, HelpMessage = "True to identify the release as a prerelease. false to identify the release as a full release. Default: false")] [bool] $IsPreRelease = $false #[switch] $GenerateReleaseNotes, #[switch] $MakeLatest ) BEGIN { # 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 [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls [string] $NewLine = [Environment]::NewLine if ([string]::IsNullOrEmpty($ReleaseName)) { $ReleaseName = $TagName } # 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 = ($null -eq $AssetFilePaths) -or ($AssetFilePaths.Count -le 0) if ($thereAreNoAssetsToIncludeInTheRelease) { $result.AllAssetUploadsSucceeded = $null } $authHeader = [ordered] @{ Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($GitHubAccessToken + ":x-oauth-basic")) } $releaseData = [ordered] @{ tag_name = $TagName target_commitish = $Commitish name = $ReleaseName body = $ReleaseNotes draft = $IsDraft prerelease = $IsPreRelease #generate_release_notes = $GenerateReleaseNotes.IsPresent #make_latest = $MakeLatest.IsPresent } $createReleaseWebRequestParameters = [ordered] @{ Uri = "https://api.github.com/repos/$GitHubUsername/$GitHubRepositoryName/releases" Method = 'POST' Headers = $authHeader ContentType = 'application/vnd.github+json' Body = (ConvertTo-Json $releaseData -Compress) } try { Write-Verbose "Sending web request to create the new Release..." $createReleaseWebRequestResults = Invoke-RestMethodAndThrowDescriptiveErrorOnFailure -requestParametersHashTable $createReleaseWebRequestParameters } catch { $result.ReleaseCreationSucceeded = $false $result.ErrorMessage = $_.Exception.Message return $result } $result.ReleaseCreationSucceeded = $true $result.ReleaseUrl = $createReleaseWebRequestResults.html_url if ($thereAreNoAssetsToIncludeInTheRelease) { $result.Succeeded = $true return $result } # 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 Test-BasicModule { [cmdletBinding()] param( [string] $Path, [string] $Type ) if ($Type -contains 'Encoding') { Get-ChildItem -LiteralPath $Path -Recurse -Filter '*.ps1' | Get-Encoding } } 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 } } if ($PSVersionTable.PSEdition -eq 'Desktop' -and (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full").Release -lt 379893) { Write-Warning "This module requires .NET Framework 4.5.2 or later."; return } # Export functions and aliases as required Export-ModuleMember -Function @('Convert-CommandsToList', 'Get-MissingFunctions', 'Initialize-PortableModule', 'Initialize-PortableScript', 'Initialize-ProjectManager', 'Invoke-ModuleBuild', 'New-ConfigurationArtefact', 'New-ConfigurationBuild', 'New-ConfigurationCommand', 'New-ConfigurationDocumentation', 'New-ConfigurationExecute', 'New-ConfigurationFormat', 'New-ConfigurationImportModule', 'New-ConfigurationInformation', 'New-ConfigurationManifest', 'New-ConfigurationModule', 'New-ConfigurationModuleSkip', 'New-ConfigurationPublish', 'New-ConfigurationTest', 'Register-Certificate', 'Remove-Comments', 'Send-GitHubRelease', 'Test-BasicModule', 'Test-ScriptFile', 'Test-ScriptModule') -Alias @('Build-Module', 'Invoke-ModuleBuilder', 'New-PrepareModule') # SIG # Begin signature block # MIItrwYJKoZIhvcNAQcCoIItoDCCLZwCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB2HlkhHqgHGDqK # c5Q3yMqtuBONlCMJkDQfdXscfYFGMqCCJrIwggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG # SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw # aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK # EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm # dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu # d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD # eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1 # XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld # QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS # YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm # M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT # QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx # fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD # VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq # hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4 # XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ # aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg # X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk # apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL # FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy # 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u # KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54 # zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8 # 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8 # aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w # ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT # MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1 # c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG # SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS # g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9 # /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn # HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0 # VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f # sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj # gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0 # QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv # mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T # /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk # 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r # mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E # FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n # P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG # CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu # Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln # aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v # Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV # HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB # AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp # wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl # zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ # cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe # Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j # Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh # IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6 # OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw # N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR # 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2 # VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGsDCCBJigAwIBAgIQ # CK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEV # MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t # MSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAw # MDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT # aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEF # AAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+k # jmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9 # NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9 # URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegY # E2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS # 4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJa # wv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+w # c86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eR # Gv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF2 # 3r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCK # ZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhEC # AwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2 # O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P # MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcB # AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr # BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1 # c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln # aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAH # BgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6 # mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/ # SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzY # gBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9 # kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ # 9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAew # Q3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5Lm # Tl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HA # SIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xr # y7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhR # ILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFu # v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGwDCCBKigAwIBAgIQDE1pckuU+jwqSj0p # B4A9WjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5 # NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIyMDkyMTAwMDAwMFoXDTMzMTEy # MTIzNTk1OVowRjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSQwIgYD # VQQDExtEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMiAtIDIwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQDP7KUmOsap8mu7jcENmtuh6BSFdDMaJqzQHFUeHjZt # vJJVDGH0nQl3PRWWCC9rZKT9BoMW15GSOBwxApb7crGXOlWvM+xhiummKNuQY1y9 # iVPgOi2Mh0KuJqTku3h4uXoW4VbGwLpkU7sqFudQSLuIaQyIxvG+4C99O7HKU41A # gx7ny3JJKB5MgB6FVueF7fJhvKo6B332q27lZt3iXPUv7Y3UTZWEaOOAy2p50dIQ # kUYp6z4m8rSMzUy5Zsi7qlA4DeWMlF0ZWr/1e0BubxaompyVR4aFeT4MXmaMGgok # vpyq0py2909ueMQoP6McD1AGN7oI2TWmtR7aeFgdOej4TJEQln5N4d3CraV++C0b # H+wrRhijGfY59/XBT3EuiQMRoku7mL/6T+R7Nu8GRORV/zbq5Xwx5/PCUsTmFnta # fqUlc9vAapkhLWPlWfVNL5AfJ7fSqxTlOGaHUQhr+1NDOdBk+lbP4PQK5hRtZHi7 # mP2Uw3Mh8y/CLiDXgazT8QfU4b3ZXUtuMZQpi+ZBpGWUwFjl5S4pkKa3YWT62SBs # GFFguqaBDwklU/G/O+mrBw5qBzliGcnWhX8T2Y15z2LF7OF7ucxnEweawXjtxojI # sG4yeccLWYONxu71LHx7jstkifGxxLjnU15fVdJ9GSlZA076XepFcxyEftfO4tQ6 # dwIDAQABo4IBizCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwFgYD # VR0lAQH/BAwwCgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZI # AYb9bAcBMB8GA1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCPnshvMB0GA1UdDgQW # BBRiit7QYfyPMRTtlwvNPSqUFN9SnDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8v # Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hBMjU2 # VGltZVN0YW1waW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCBgDAkBggrBgEFBQcw # AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUFBzAChkxodHRwOi8v # Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hB # MjU2VGltZVN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQBVqioa80bz # eFc3MPx140/WhSPx/PmVOZsl5vdyipjDd9Rk/BX7NsJJUSx4iGNVCUY5APxp1Mqb # KfujP8DJAJsTHbCYidx48s18hc1Tna9i4mFmoxQqRYdKmEIrUPwbtZ4IMAn65C3X # CYl5+QnmiM59G7hqopvBU2AJ6KO4ndetHxy47JhB8PYOgPvk/9+dEKfrALpfSo8a # OlK06r8JSRU1NlmaD1TSsht/fl4JrXZUinRtytIFZyt26/+YsiaVOBmIRBTlClmi # a+ciPkQh0j8cwJvtfEiy2JIMkU88ZpSvXQJT657inuTTH4YBZJwAwuladHUNPeF5 # iL8cAZfJGSOA1zZaX5YWsWMMxkZAO85dNdRZPkOaGK7DycvD+5sTX2q1x+DzBcNZ # 3ydiK95ByVO5/zQQZ/YmMph7/lxClIGUgp2sCovGSxVK05iQRWAzgOAj3vgDpPZF # R+XOuANCR+hBNnF3rf2i6Jd0Ti7aHh2MWsgemtXC8MYiqE+bvdgcmlHEL5r2X6cn # l7qWLoVXwGDneFZ/au/ClZpLEQLIgpzJGgV8unG1TnqZbPTontRamMifv427GFxD # 9dAq6OJi7ngE273R+1sKqHB+8JeEeOMIA11HLGOoJTiXAdI/Otrl5fbmm9x+LMz/ # F0xNAKLY1gEOuIvu5uByVYksJxlh9ncBjDCCB18wggVHoAMCAQICEAfCUnQoFKLW # q/4k6hfl3S4wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMCVVMxFzAVBgNVBAoT # DkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENv # ZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMTAeFw0yMzA0MTYwMDAw # MDBaFw0yNjA3MDYyMzU5NTlaMGcxCzAJBgNVBAYTAlBMMRIwEAYDVQQHDAlNaWtv # xYLDs3cxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlzIEVWT1RFQzEhMB8GA1UE # AwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIICIjANBgkqhkiG9w0BAQEFAAOC # Ag8AMIICCgKCAgEAlJoHlzELSGimkpCr2wLfBhWSdcsDh/EsMZU7rODHMq1plTq0 # QVUUAPAKRfRWnqG8JpGcb5MUExSxypvvJJ8KJhFLJXGvAqkjiNGMBC7+RME1RIdA # vw2nob8aOrZJjTxff0j9Sgt3NJdbzvjO73TVRikCEK4cauxBtInswWTgIrpDXRlV # 0WDi5+O1d6i+T8Bv6LtmpSf74nyA2nfNahW/kJFIdNiaNuEjI1nSg8rXazF4tNt+ # QjeEa1vvII30Sfnyio4DCJm7nHgrIvSL9Wuum1HPWpwHpjm0+JheVP8kAYALgKN/ # o1QfMIlHfO5FEDtMyQhfL6tmK1Ts/DiZjF/IICLBBFGdwmSg9IVXN3Zu3FkgMPPx # TcxjT5QGiMc11/ang9BIGgi0ZCLQN7d3kFviAF8kv/WZ56RVKA70BmyvkOP2z9Im # /fFy30KcVRkbtHAldDYO+wyJERfiMkdT3MFQKvjs1VN7ynqNub/657YlwpgsYluK # B2DtvHkkP3iAHJ4ovt7igzWayNeT+1cQ65FCHOhbYkrzocHNwM2PrxH4r1JBSkas # L0kq+Hwq65JO89kHu9mcJcNhA0VR8stH1FRjvUDLoehN0cJyS/eoqdGpXJoSgARq # CKkltOZ13QlG5F5oTwk0+Z2kA7mdVJAF22T0oSo2z8M3Vz9m/CPZ0PPVUoECAwEA # AaOCAgMwggH/MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1Ud # DgQWBBR68WolWbgyccRJNeWy6DLhSOdt9zAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0l # BAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDovL2NybDMu # ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2 # U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFD # QTEuY3JsMD4GA1UdIAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUHAgEWG2h0dHA6 # Ly93d3cuZGlnaWNlcnQuY29tL0NQUzCBlAYIKwYBBQUHAQEEgYcwgYQwJAYIKwYB # BQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBcBggrBgEFBQcwAoZQaHR0 # cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNp # Z25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcnQwCQYDVR0TBAIwADANBgkqhkiG # 9w0BAQsFAAOCAgEAtxHh11D4aXt9Stgy+Nx34eqpLwR8kdUZQ/ZVSJJXEQkedGR8 # 6FrOhAZUxcqIb5KXJVQrkXUFt97Uur7SjzrnKQw7+MLAPus5CWCPHx6Lluk6mtVu # O2Eq3OQDkoSHCffjaTWyjRood3aEpXIqNplCgl+SP2a8yQZEKSdJGIWv6VEk9gmx # Nya6CX9r0FhlIiPidy3YjzR5oTtZfs2kJEsb9HFQxEzH0BmSikVREmehYOtW9HY7 # 0EseddDHW8bSjI70t2bQMrap0B5NYqT/kYPjOZRR60pFJZ6Rmvn957kIcQ2+zfRP # IVFXr8QC06xYn4PM4bJVUR+fw3/wsZTClwu6Kd9PwMkLDkMR1tbjcd7RtQzIIs6c # AWrK8YesGu4mgPi6dO6tSPdni4a2G7cN8QtrzSBnTHTe53e+sjCI3WJwJ+69/MML # WidymA9EE5e+xAfLv+XArN0oWXQ3coOCuzaCZfIhB626raKABzjC4iaYi9ovWJ/J # EDAev0OkTDtyFDy7snAfaOgzYsEB3+ibeaFuz9PZOTccQRJpLMcDW5mbzUOuWZ93 # sVACqhvsd9RIM+SGeFP4z80WpRJRCKUtK4K1YPEfKRDoXfeZhM6eVhEShcl4Xupw # em0mB7/HJSwFdIjJLt9PK6X4zIkJKktyy831CeTh6rSikDTC8c/c9fOVArMxggZT # MIIGTwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5j # LjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNB # NDA5NiBTSEEzODQgMjAyMSBDQTECEAfCUnQoFKLWq/4k6hfl3S4wDQYJYIZIAWUD # BAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMx # DAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkq # hkiG9w0BCQQxIgQgomUnAO5mQt2aRA3K6PU0EV0F08d1IofGaunyIc13Eu4wDQYJ # KoZIhvcNAQEBBQAEggIASGjtaPQ6R0ctcKYkiwYfWiAAi6Kltvgs7pQQRmykgxLj # VaFZwHRuSsjIxZgXiHswVFKrRPHeGmrES/HA+PTccgze/Epwe3VWLZSq7mm4iWCw # 3BVuf3wZ1baJHAvNhXqUUQWI/QzYCnmM2/I7OV1caNTQJc/xfOST+dlw3+0iBuBc # 0WXfc69qJdGI+HEMX3tOjZOb+WVztpbYqa++tYASa/XNEyG/iXRvLD823Co6H1TD # rhb2u8YRN3WzNa1AHA+7YcdOr0G8cMVwGLa4AnLbl6x/x8eoD2mJ2A9VUh2tjXKu # mEWPZuZA/5+2YWod8A22GEdrS64NvU63zQrYDzwExjFXVfjNGknbzx0Z16iddqvn # jXUbg9+mHNvgTVGhmJ1S+vIa98ShQBQaYi9BqKx1yuoTY+joAM7YMmHDF6Zf1wma # JGJxhPWX+Oc7F33lB3B/hx5aqCRwm8dk+d6Oc0fQd+YPDubUhp/t9BT9gwINdmFV # dj0/UVz1/rObEpxY9GuHKmXfhehO7AXHCz/z1URvul66tQPh+eo9fhcSuwtSCB+Z # sEMoYLtD9kKVuA4hmKfr9Ggd37hNd0K+hENKt6P29C0OyfpSNBO1vh+aYQmCIRUv # dHVmuwVo+aURbcapDEgJ0XSZ1IUAs9DuwkzydA5Ms6R9gPFZ4KmtpRYyalczQsCh # ggMgMIIDHAYJKoZIhvcNAQkGMYIDDTCCAwkCAQEwdzBjMQswCQYDVQQGEwJVUzEX # MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0 # ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBAhAMTWlyS5T6PCpK # PSkHgD1aMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEH # ATAcBgkqhkiG9w0BCQUxDxcNMjMwNzI0MjA0MjI2WjAvBgkqhkiG9w0BCQQxIgQg # HuFdjga14+D/rtELFbIleG+Eou3DXq3bAPiD8Y9q8pkwDQYJKoZIhvcNAQEBBQAE # ggIAiK+2s6oGjUXOs9nJfFDPkm65SJvqKMLSMNeDBYUwfwnl8GVxVTOm66B0P4Dt # 4H9Vir3cONZJHHezC6rEfzG0Md5FQlHpCOpwlk3OswsvArnp7jXNEFob1+v+MMjN # JAQEh4yCmzGaBdKCj7P+3ARI+aE68lKLFK1jp/fKxZQrBa+JP+zpFe24sAXN3K8z # 1Hq+CNhgSbn4wKkTJ6ZPPZLbJ7/VyWuSW0XIjc56ykL0XUHcVYNlD/l3C6WI1Q2y # MGreV7UxPa24ujK/Rc8WoY8VPSA3OyMUR/skI+bysHMZkt6Zmphm8nv3wEEyF+nO # M6L8CoazqGFtT6BdgC/LahNF1lJ0Pi/HbGpugVPya6OatV9/aasiL0uWS1xnfvJa # aSvv2RgnzX3F9coFrB47mbfREtKJAwmgtfBNXcfWpyglu+cZs77Mq1QMQstdVQ79 # cQCUvKVDtwbhkrMxvNc7TRNNqAc1+eDKIJGLuUx8QVxNcK0H5qNcj/4kF9gRrPbx # 2+5jEjpV5IzDRquAyr7LeD9jWrBT/Oz7jiXz0vBkRIiI0t3Tr2IZdi2Dp0kN+4k0 # upLpaSofBM/HrJ+QSoArEo8OF02EOrhX0MVyJGNCE619L35cAtjqctltIRw+0G9B # jpR2401ZhVlEPqT6RPzMTlmjyEVxWy0yp99aJwPwdmBVvLw= # SIG # End signature block |