PSFramework.NuGet.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PSFPowerShellDataFile -Path "$($script:ModuleRoot)\PSFramework.NuGet.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName PSFramework.NuGet.Import.DoDotSource -Fallback $false if ($PSFramework_NuGet_dotsourcemodule) { $script:doDotSource = $true } <# Note on Resolve-Path: All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. This is important when testing for paths. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = Get-PSFConfigValue -FullName PSFramework.NuGet.Import.IndividualFiles -Fallback $false if ($PSFramework_NuGet_importIndividualFiles) { $importIndividualFiles = $true } if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] Param ( [string] $Path ) $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath if ($doDotSource) { . $resolvedPath } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) { . Import-ModuleFile -Path $path } # Import all internal functions foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) { . Import-ModuleFile -Path $path } # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code <# This file loads the strings documents from the respective language folders. This allows localizing messages and errors. Load psd1 language files for each language you wish to support. Partial translations are acceptable - when missing a current language message, it will fallback to English or another available language. #> Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'PSFramework.NuGet' -Language 'en-US' function New-PublishResult { <# .SYNOPSIS Creates a new publish result object, provided as result of Save-PSFModule. .DESCRIPTION Creates a new publish result object, provided as result of Save-PSFModule. .PARAMETER ComputerName The computer the module was deployed to. .PARAMETER Module The module that was deployed. .PARAMETER Version The version of the module that was deployed. .PARAMETER Success Whether the deployment action succeeded. Even if there is a message - which usually means something went wrong - success is possible. For example, when a cleanup step failed, but the intended action worked. .PARAMETER Message A message added to the result. Usually describes what went wrong - fully or partially. Some messages may be included with a success - when the actual goal was met, but something less important went wrong anyway. .PARAMETER Path The path deployed to. When deploying to a remote computer, this will include the local path from the perspective of the remote computer. .EXAMPLE PS C:\> New-PublishResult -ComputerName server1 -Module PSFramework -Version 1.12.346 -Success $true -Path 'C:\Program Files\WindowsPowerShell\Modules' Creates a report of how PSFramework in version 1.12.346 was successfully deployed to the default modules folder on server1 #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [string] $ComputerName, [string] $Module, [string] $Version, [bool] $Success, [string] $Message, [string] $Path ) [PSCustomObject]@{ PSTypeName = 'PSFramework.NuGet.PublishResult' Computername = $ComputerName Module = $Module Version = $Version Success = $Success Message = $Message Path = $Path } } function Publish-StagingModule { <# .SYNOPSIS Dispatches module publishing commands. .DESCRIPTION Dispatches module publishing commands. This command takes the path to where the locally cached modules are held before copying them to their target location, then ensures they are sent there. It differentiates between local deployments and remote deployments, all remote deployments being performed in parallel. .PARAMETER Path The path to where the modules lie that need to be deployed. .PARAMETER TargetPath The targeting information that determines where the modules get published. Contrary to the name, this is not a string but expects the output from Resolve-RemotePath. The object includes the paths (plural) and the session information needed for remote deployments. .PARAMETER ThrottleLimit Up to how many computers to deploy the modules to in parallel. Defaults to: 5 Default can be configured under the 'PSFramework.NuGet.Remoting.Throttling' setting. .PARAMETER Force Redeploy a module that already exists in the target path. By default it will skip modules that do already exist in the target path. .PARAMETER Cmdlet The $PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user. .EXAMPLE PS C:\> Publish-StagingModule -Path $stagingDirectory -TargetPath $targets -Force:$Force -Cmdlet $PSCmdlet Deploys all modules under $stagingDirectory to the target paths/computers in $targets. Will overwrite existing modules if $Force is $true. #> [CmdletBinding()] param ( [string] $Path, $TargetPath, [ValidateRange(1, [int]::MaxValue)] [int] $ThrottleLimit = (Get-PSFConfigValue -FullName 'PSFramework.NuGet.Remoting.Throttling'), [switch] $Force, $Cmdlet ) process { $localPaths = @($TargetPath).Where{ -not $_.Session }[0] $remotePaths = @($TargetPath).Where{ $_.Session } if ($localPaths) { Publish-StagingModuleLocal -Path $Path -TargetPath $localPaths -Force:$Force -Cmdlet $Cmdlet } if ($remotePaths) { Publish-StagingModuleRemote -Path $Path -TargetPath $remotePaths -ThrottleLimit $ThrottleLimit -Force:$Force } } } function Publish-StagingModuleLocal { <# .SYNOPSIS Deploys modules to a local path. .DESCRIPTION Deploys modules to a local path. .PARAMETER Path The path from where modules are copied. .PARAMETER TargetPath The destination path information where to deploy the modules. Not a string, but the return objects from Resolve-RemotePath (which contrary to its name is also capable of resolving local paths) .PARAMETER Force Redeploy a module that already exists in the target path. By default it will skip modules that do already exist in the target path. .PARAMETER Cmdlet The $PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user. .EXAMPLE PS C:\> Publish-StagingModuleLocal -Path $stagingDirectory -TargetPath $targets -Force:$Force -Cmdlet $PSCmdlet Deploys all modules under $stagingDirectory to the target paths/computers in $targets. Will overwrite existing modules if $Force is $true. #> [CmdletBinding()] param ( [string] $Path, $TargetPath, [switch] $Force, $Cmdlet ) $msgParam = @{ PSCmdlet = $Cmdlet } $publishCommon = @{ ComputerName = $env:COMPUTERNAME } $oldSuffix = "old_$(Get-Random -Minimum 100 -Maximum 999)" $killIt = $ErrorActionPreference -eq 'Stop' foreach ($module in Get-ChildItem -Path $Path) { foreach ($version in Get-ChildItem -Path $module.FullName) { foreach ($destination in $TargetPath.Results) { if (-not $destination.Exists) { continue } $publishCommon.Path = $destination.Path $publishCommon.Module = $module.Name $publishCommon.Version = $version.Name $testPath = Join-Path -Path $destination.Path -ChildPath "$($module.Name)/$($version.Name)/$($module.DirectoryName).psd1" $alreadyExists = Test-Path -Path $testPath if ($alreadyExists -and -not $Force) { Write-PSFMessage @msgParam -String 'Publish-StagingModule.Skipping.AlreadyExists' -StringValues $module.Name, $version.Name, $destination.Path continue } $targetVersionRoot = Join-Path -Path $destination.Path -ChildPath $module.Name $targetVersionDirectory = Join-Path -Path $destination.Path -ChildPath "$($module.Name)/$($version.Name)" # Rename old version if ($alreadyExists) { Invoke-PSFProtectedCommand -ActionString 'Publish-StagingModule.Deploying.RenameOld' -ActionStringValues $module.Name, $version.Name -Target $TargetPath -ScriptBlock { Rename-Item -LiteralPath $targetVersionDirectory -NewName "$($version.Name)_$oldSuffix" -Force -ErrorAction Stop } -PSCmdlet $Cmdlet -EnableException $killIt -Continue -ErrorEvent { $result = New-PublishResult @publishCommon -Success $false -Message "Failed to rename old version: $_" $PSCmdlet.WriteObject($result, $true) } } # Deploy New Version Invoke-PSFProtectedCommand -ActionString 'Publish-StagingModule.Deploying.Local' -ActionStringValues $module.Name, $version.Name, $destination.Path -Target $TargetPath -ScriptBlock { if (-not (Test-Path $targetVersionRoot)) { $null = New-Item -Path $destination.Path -Name $module.Name -ItemType Directory -Force } Copy-Item -Path $version.FullName -Destination $targetVersionRoot -Recurse -Force } -PSCmdlet $Cmdlet -EnableException $killIt -Continue -ErrorEvent { # Rollback to old version in case of deployment error if ($alreadyExists) { Remove-Item -Path $targetVersionDirectory -Force -ErrorAction SilentlyContinue Rename-Item -LiteralPath "$($targetVersionDirectory)_$oldSuffix" -NewName $version.Name -Force -ErrorAction Continue # Don't interfere with the formal error handling, but show extra error if applicable } $result = New-PublishResult @publishCommon -Success $false -Message "Failed to deploy version: $_" $PSCmdlet.WriteObject($result, $true) } # Remove old version if ($alreadyExists) { Invoke-PSFProtectedCommand -ActionString 'Publish-StagingModule.Deploying.DeleteOld' -ActionStringValues $module.Name, $version.Name -Target $TargetPath -ScriptBlock { Remove-Item -LiteralPath "$($targetVersionDirectory)_$oldSuffix" -Force -ErrorAction Stop -Recurse } -PSCmdlet $Cmdlet -EnableException $false -Continue -ErrorEvent { $result = New-PublishResult @publishCommon -Success $true -Message "Failed to cleanup previous version: $_" $PSCmdlet.WriteObject($result, $true) } } New-PublishResult @publishCommon -Success $true } } } } function Publish-StagingModuleRemote { <# .SYNOPSIS Deploys modules to a remote path. .DESCRIPTION Deploys modules to a remote path. This happens in parallel using psframework Runspace Workflows. .PARAMETER Path The path from where modules are copied. .PARAMETER TargetPath The destination path information where to deploy the modules. Not a string, but the return objects from Resolve-RemotePath. This object also includes the PSSession objects needed to execute the transfer. .PARAMETER ThrottleLimit Up to how many computers to deploy the modules to in parallel. Defaults to: 5 Default can be configured under the 'PSFramework.NuGet.Remoting.Throttling' setting. .PARAMETER Force Redeploy a module that already exists in the target path. By default it will skip modules that do already exist in the target path. .EXAMPLE PS C:\> Publish-StagingModuleRemote -Path $stagingDirectory -TargetPath $targets -Force:$Force -Cmdlet $PSCmdlet Deploys all modules under $stagingDirectory to the target paths/computers in $targets. Will overwrite existing modules if $Force is $true. #> [CmdletBinding()] param ( [string] $Path, $TargetPath, [ValidateRange(1, [int]::MaxValue)] [int] $ThrottleLimit = 5, [switch] $Force ) begin { #region Worker Code $code = { param ( $TargetPath ) <# Inherited Variables: - $Path - Where the modules to publish lie - $Force - Whether to overwrite/redeploy modules that already exist in that path in that version #> $PSDefaultParameterValues['Write-PSFMessage:ModuleName'] = 'PSFramework.NuGet' $PSDefaultParameterValues['Write-PSFMessage:FunctionName'] = 'Publish-StagingModule' #region Functions function Get-GlobalFailResult { [CmdletBinding()] param ( [string] $ComputerName, [string] $Path, [System.Management.Automation.ErrorRecord] $ErrorRecord ) foreach ($module in Get-ChildItem -Path $Path) { foreach ($version in Get-ChildItem -Path $module.FullName) { New-PublishResult -ComputerName $ComputerName -Module $module.Name -Version $version.Name -Success $false -Path 'n/a' -Message $ErrorRecord } } } #endregion Functions trap { Write-PSFMessage -Level Error -String 'Publish-StagingModule.Error.General' -StringValues $TargetPath.ComputerName -ErrorRecord $_ #region Cleanup Staging Directory if ($stagingDirectory) { $null = Invoke-SessionCommand @sessionCommon -Code { param ($Path) Remove-Item -Path $Path -Recurse -Force -ErrorAction Ignore } -ArgumentList $stagingDirectory } #endregion Cleanup Staging Directory Get-GlobalFailResult -Path $Path -ComputerName $TargetPath.ComputerName -ErrorRecord $_ $__PSF_Workflow.Data.Failed[$TargetPath.ComputerName] = $true $__PSF_Workflow.Data.Completed[$TargetPath.ComputerName] = $true $null = $__PSF_Workflow.Data.InProgress.TryRemove($TargetPath.ComputerName, [ref]$null) return } $publishCommon = @{ ComputerName = $TargetPath.ComputerName } $sessionCommon = @{ Session = $TargetPath.Session.Session } $oldSuffix = "old_$(Get-Random -Minimum 100 -Maximum 999)" $anyFailed = $false #region Prepare Staging Directory # This allows us to minimize the cutover time, when replacing an existing module $stagingResult = Invoke-SessionCommand @sessionCommon -Code { $tempDir = $env:TEMP if (-not $tempDir) { $localAppData = $env:LOCALAPPDATA if (-not $localAppData -and -not $IsLinux -and -not $IsMacOS) { $localAppData = [Environment]::GetFolderPath("LocalApplicationData") } if (-not $localAppData -and $Env:XDG_CONFIG_HOME) { $localAppData = $Env:XDG_CONFIG_HOME } if (-not $localAppData) { $localAppData = Join-Path -Path $HOME -ChildPath '.config' } $tempDir = Join-Path -Path $localAppData -ChildPath 'Temp' } if (-not (Test-Path -Path $tempDir)) { New-Item -Path $tempDir -ItemType Directory -Force -ErrorAction Stop } $stagingPath = Join-Path -Path $tempDir -ChildPath "PsfGet-$(Get-Random)" (New-Item -Path $stagingPath -ItemType Directory -Force -ErrorAction Stop).FullName } if (-not $stagingResult.Success) { Write-PSFMessage -Level Warning -String 'Publish-StagingModule.Remote.Error.TempStagingSetup' -StringValues $TargetPath.ComputerName, $stagingResult.Error -Tag error, temp, setup throw $stagingResult.Error } $stagingDirectory = $stagingResult.Data #endregion Prepare Staging Directory #region Send Modules foreach ($module in Get-ChildItem -Path $Path) { foreach ($version in Get-ChildItem -Path $module.FullName) { foreach ($destination in $TargetPath.Results) { if (-not $destination.Exists) { continue } #region Verify Existence $publishCommon.Path = $destination.Path $publishCommon.Module = $module.Name $publishCommon.Version = $version.Name $testPath = Join-Path -Path $destination.Path -ChildPath "$($module.Name)/$($version.Name)/$($module.Name).psd1" $alreadyExists = Invoke-Command -Session $TargetPath.Session.Session -ScriptBlock { param ($TestPath) Test-Path -Path $TestPath } -ArgumentList $testPath if ($alreadyExists -and -not $Force) { Write-PSFMessage -String 'Publish-StagingModule.Remote.Skipping.AlreadyExists' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name -Target ("$($module.Name) ($($version.Name))") New-PublishResult @publishCommon -Success $true -Message 'Module already deployed' continue } #endregion Verify Existence $targetStagingRoot = Join-Path -Path $stagingDirectory -ChildPath $module.Name $targetStagingVersionDirectory = Join-Path -Path $targetStagingRoot -ChildPath $version.Name $targetVersionRoot = Join-Path -Path $destination.Path -ChildPath $module.Name $targetVersionDirectory = Join-Path -Path $targetVersionRoot -ChildPath $version.Name #region Send Module to Staging Write-PSFMessage -String 'Publish-StagingModule.Remote.DeployStaging' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $targetStagingRoot -Target ("$($module.Name) ($($version.Name))") $createResult = Invoke-SessionCommand @sessionCommon -Code { param ($ModuleRoot) New-Item -Path $ModuleRoot -ItemType Directory -Force -ErrorAction Stop } -ArgumentList $targetStagingRoot if (-not $createResult.Success) { Write-PSFMessage -String 'Publish-StagingModule.Remote.DeployStaging.FailedDirectory' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $targetStagingRoot, $createResult.Error -Target ("$($module.Name) ($($version.Name))") New-PublishResult @publishCommon -Success $false -Message "Failed to create staging module folder $targetStagingRoot on $($TargetPath.ComputerName): $($createResult.Error)" $anyFailed = $true continue } try { Copy-Item -LiteralPath $version.FullName -Destination $targetStagingRoot -Recurse -Force -ToSession $TargetPath.Session.Session -ErrorAction Stop } catch { Write-PSFMessage -String 'Publish-StagingModule.Remote.DeployStaging.FailedCopy' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $targetStagingRoot -Target ("$($module.Name) ($($version.Name))") -ErrorRecord $_ New-PublishResult @publishCommon -Success $false -Message "Failed to copy module to folder $targetStagingRoot on $($TargetPath.ComputerName): $_" $anyFailed = $true continue } #endregion Send Module to Staging #region Rename old version if ($alreadyExists) { Write-PSFMessage -String 'Publish-StagingModule.Remote.Deploying.RenameOld' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $testPath -Target ("$($module.Name) ($($version.Name))") $renameResult = Invoke-SessionCommand @sessionCommon -Code { param ($Path, $NewName) Rename-Item -LiteralPath $Path -NewName $NewName -ErrorAction Stop -Force } -ArgumentList $targetVersionDirectory, "$($version.Name)_$oldSuffix" if ($renameResult.Success) { Write-PSFMessage -String 'Publish-StagingModule.Remote.Deploying.RenameOld.Success' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $testPath -Target ("$($module.Name) ($($version.Name))") } else { Write-PSFMessage -Level Warning -String 'Publish-StagingModule.Remote.Deploying.RenameOld.NoSuccess' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $testPath, $renameResult.Error -Target ("$($module.Name) ($($version.Name))") $anyFailed = $true New-PublishResult @publishCommon -Success $false -Message "Failed to rename old version: $($renameResult.Error)" continue } } #endregion Rename old version #region Deploy New Version Write-PSFMessage -String 'Publish-StagingModule.Deploying.Remote' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $targetVersionRoot -Target ("$($module.Name) ($($version.Name))") $deployResult = Invoke-SessionCommand @sessionCommon -Code { param ($Path, $Destination) if (-not (Test-Path -Path $Destination)) { $null = New-Item -Path $Destination -ItemType Directory -Force -ErrorAction Stop } Move-Item -Path $Path -Destination $Destination -Force -ErrorAction Stop } -ArgumentList $targetStagingVersionDirectory, $targetVersionRoot if (-not $deployResult.Success) { Write-PSFMessage -Level Warning -String 'Publish-StagingModule.Deploying.Remote.Failed' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $targetVersionRoot, $deployResult.Error -Target ("$($module.Name) ($($version.Name))") $anyFailed = $true if (-not $alreadyExists) { New-PublishResult @publishCommon -Success $false -Message "Failed to deploy version: $($deployResult.Error)" continue } $rollbackResult = Invoke-SessionCommand @sessionCommon -Code { param ($Path, $TempName) $parent, $name = Split-Path -Path $Path if (Test-Path -Path $Path) { Remove-Item -Path $Path -Recurse -Force -ErrorAction Stop } $original = Join-Path -Path $parent -ChildPath $TempName Rename-Item -Path $original -NewName $name -Force -ErrorAction Stop } -ArgumentList $targetVersionDirectory, "$($version.Name)_$oldSuffix" if ($rollbackResult.Success) { New-PublishResult @publishCommon -Success $false -Message "Failed to re-deploy version, rollback was successful: $($deployResult.Error)" } else { New-PublishResult @publishCommon -Success $false -Message "Failed to re-deploy version, rollback failed: $($deployResult.Error)" } continue } #endregion Deploy New Version #region Remove Old Version if ($alreadyExists) { $cleanupResult = Invoke-SessionCommand @sessionCommon -Code { param ($Path) Remove-Item -LiteralPath $Path -Force -ErrorAction Stop -Recurse } -ArgumentList "$($targetVersionDirectory)_$oldSuffix" if (-not $cleanupResult.Success) { New-PublishResult @publishCommon -Success $true -Message "Failed to cleanup previous version: $($cleanupResult.Error)" continue } } #endregion Remove Old Version New-PublishResult @publishCommon -Success $true } } } #endregion Send Modules #region Cleanup Staging Directory $null = Invoke-SessionCommand @sessionCommon -Code { param ($Path) Remove-Item -Path $Path -Recurse -Force -ErrorAction Ignore } -ArgumentList $stagingDirectory #endregion Cleanup Staging Directory $__PSF_Workflow.Data.Completed[$TargetPath.ComputerName] = $true if ($anyFailed) { $__PSF_Workflow.Data.Failed[$TargetPath.ComputerName] = $true } else { $__PSF_Workflow.Data.Success[$TargetPath.ComputerName] = $true } $null = $__PSF_Workflow.Data.InProgress.TryRemove($TargetPath.ComputerName, [ref]$null) } #endregion Worker Code # Limit Worker Count to Target Count if ($TargetPath.Count -lt $ThrottleLimit) { $ThrottleLimit = $TargetPath.Count } $workflow = New-PSFRunspaceWorkflow -Name PublishModule -Force $null = $workflow | Add-PSFRunspaceWorker -Name Publisher -InQueue Input -OutQueue Results -ScriptBlock $code -CloseOutQueue -Count $ThrottleLimit -Functions @{ 'New-PublishResult' = [scriptblock]::Create((Get-Command New-PublishResult).Definition) 'Invoke-SessionCommand' = [scriptblock]::Create((Get-Command Invoke-SessionCommand).Definition) } -Variables @{ Path = $Path Force = $Force } $workflow | Write-PSFRunspaceQueue -Name Input -BulkValues $TargetPath -Close # Add Tracking for Progress Information $workflow.Data['InProgress'] = [System.Collections.Concurrent.ConcurrentDictionary[string, string]]::new() $workflow.Data['Failed'] = [System.Collections.Concurrent.ConcurrentDictionary[string, string]]::new() $workflow.Data['Success'] = [System.Collections.Concurrent.ConcurrentDictionary[string, string]]::new() $workflow.Data['Completed'] = [System.Collections.Concurrent.ConcurrentDictionary[string, string]]::new() $progressId = Get-Random } process { try { $workflow | Start-PSFRunspaceWorkflow Write-Progress -Id $progressId -Activity 'Deploying Modules' while ($workflow.Data.Completed.Count -lt $TargetPath.Count) { Start-Sleep -Seconds 1 $status = 'In Progress: {0} | Failed: {1} | Succeeded: {2} | Completed: {3}' -f $workflow.Data.InProgress.Count, $workflow.Data.Failed.Count, $workflow.Data.Success.Count, $workflow.Data.Completed.Count $percent = ($workflow.Data.Completed.Count / $TargetPath.Count * 100) -as [int] if ($percent -gt 100) { $percent = 100 } Write-Progress -Id $progressId -Activity 'Deploying Modules' -Status $status -PercentComplete $percent } $workflow | Wait-PSFRunspaceWorkflow -Queue Results -Closed } finally { # Ensure finally executes without interruption, lest an impatient admin leads to leftover state Disable-PSFConsoleInterrupt $workflow | Stop-PSFRunspaceWorkflow $results = $workflow | Read-PSFRunspaceQueue -Name Results -All | Write-Output # Needs to bextra enumerated if multiple results happen in a single worker $workflow | Remove-PSFRunspaceWorkflow Enable-PSFConsoleInterrupt $results } } } function Read-VersionString { <# .SYNOPSIS Parses a Version String to work for PSGet V2 & V3 .DESCRIPTION Parses a Version String to work for PSGet V2 & V3 Supported Syntax: <Prefix><Version><Connector><Version><Suffix> Prefix: "[" (-ge) or "(" (-gt) or nothing (-ge) Version: A valid version of 2-4 elements or nothing Connector: A "," or a "-" Suffix: "]" (-le) or ")" (-lt) or nothing (-le) .PARAMETER Version The Version string to parse. .PARAMETER Cmdlet The $PSCmdlet variable of the caller. As this is an internal utility command, this allows it to terminate in the context of the calling command and remain invisible to the user. .EXAMPLE PS C:\> Read-VersionString -Version '[1.0.0,2.0.0)' -Cmdlet $PSCmdlet Resolves to a version object with a minimum version of 1.0.0 and less than 2.0.0. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Version, $Cmdlet = $PSCmdlet ) process { $result = [PSCustomObject]@{ V3String = '' Required = '' Minimum = '' Maximum = '' Prerelease = $false } # Plain Version if ($Version -as [version]) { $result.V3String = $Version $result.Required = $Version return $result } # Plain Version with Prerelease Tag if ($Version -match '^\d+(\.\d+){1,3}-\D') { $result.V3String = $Version -replace '-\D.*$' $result.Required = $Version -replace '-\D.*$' $result.Prerelease = $true return $result } <# Must match <Prefix><Version><Connector><Version><Suffix> Prefix: "[" (-ge) or "(" (-gt) or nothing (-ge) Version: A valid version of 2-4 elements or nothing Connector: A "," or a "-" Suffix: "]" (-le) or ")" (-lt) or nothing (-le) #> if ($Version -notmatch '^(\[|\(){0,1}(\d+(\.\d+){1,3}){0,1}(-|,)(\d+(\.\d+){1,3}){0,1}(\]|\)){0,1}$') { Stop-PSFFunction -String 'Read-VersionString.Error.BadFormat' -StringValues $Version -EnableException $true -Cmdlet $Cmdlet -Category InvalidArgument } $startGT = $Version -match '^\(' $endGT = $Version -match '\)$' $lower, $higher = $Version -replace '\[|\]|\(|\)' -split ',|-' $v3Start = '[' if ($startGT) { $v3Start = '(' } $v3End = ']' if ($endGT) { $v3End = ')' } $result.V3String = "$($v3Start)$($lower),$($higher)$($v3End)" if ($lower) { $result.Minimum = $lower -as [version] if ($startGT) { $parts = $lower -split '\.' $parts[-1] = 1 + $parts[-1] $result.Minimum = $parts -join '.' } } if ($higher) { if ($higher -match '^0(\.0){1,3}$') { Stop-PSFFunction -String 'Read-VersionString.Error.BadFormat.ZeroUpperBound' -StringValues $Version -EnableException $true -Cmdlet $Cmdlet -Category InvalidArgument } $result.Maximum = $higher -as [version] if ($endGT) { $parts = $higher -split '\.' $index = $parts.Count - 1 do { if (0 -lt $parts[$index]) { $parts[$index] = -1 + $parts[$index] break } $index-- } until ($index -lt 0) if ($index -lt ($parts.Count - 1)) { foreach ($position in ($index + 1)..($parts.Count - 1)) { $parts[$position] = 999 } } $result.Maximum = $parts -join '.' } } $result } } function Resolve-ModuleScopePath { <# .SYNOPSIS Resolves the paths associated with the selected scope. .DESCRIPTION Resolves the paths associated with the selected scope. Returns separate results per computer, to account for differentiated, dynamic scope-path resolution. .PARAMETER Scope The scope to resolve the paths for. Defaults to "CurrentUser" on local deployments. Defaults to "AllUsers" on remote deployments. .PARAMETER ManagedSession Managed remoting sessions (if any). Use New-ManagedSession to establish these. .PARAMETER PathHandling Whether all specified paths must exist on a target computer, or whether a single finding counts as success. Defaults to: All .PARAMETER TargetHandling How the command should handle unsuccessful computer targets: All unsuccessful checks lead to a non-terminating exception. However, depending on this parameter, a forced terminating exception might be thrown: - "All": Even a single unsuccessful computer leads to terminal errors. - "Any": If no target was successful, terminate - "None": Never terminate Defaults to: None .PARAMETER Cmdlet The $PSCmdlet variable of the caller. As this is an internal utility command, this allows it to terminate in the context of the calling command and remain invisible to the user. .EXAMPLE PS C:\> Resolve-ModuleScopePath -Scope AllUsers -ManagedSession $managedSessions -TargetHandling Any -PathHandling Any -Cmdlet $PSCmdlet Resolves the path to use for the "AllUsers" scope for each computer in $managedSessions - or the local computer if none. If the scope resolves to multiple paths, any single existing one will consider the respective computer as successul. If any computer at all resolved successfully, the command will return and allow the caller to continue. Otherwise it will end the calling command with a terminating exception. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] [CmdletBinding()] param ( [AllowEmptyString()] [string] $Scope, [AllowNull()] $ManagedSession, [ValidateSet('All', 'Any')] [string] $PathHandling = 'All', [ValidateSet('All', 'Any', 'None')] [string] $TargetHandling = 'None', $Cmdlet = $PSCmdlet ) begin { #region Code $code = { param ( $Data, $PathHandling ) $result = [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME Scope = $Data.Name Path = $Data.Path Results = @() ExistsAll = $false ExistsAny = $false Success = $false Error = $null SessionID = $global:__PsfSessionId Session = $null } #region Calculate Target paths $targetPaths = $Data.Path if ($Data.ScriptBlock) { $pathCalculator = [ScriptBlock]::Create($Data.ScriptBlock.ToString()) if (Get-Module PSFramework) { try { $targetPaths = ([PsfScriptBlock]$pathCalculator).InvokeGlobal() } catch { $result.Error = $_ return $result } } else { try { $targetPaths = & $pathCalculator } catch { $result.Error = $_ return $result } } } #endregion Calculate Target paths $pathResults = foreach ($path in $targetPaths) { if (-not $path) { continue } try { $resolvedPaths = Resolve-Path -Path $path -ErrorAction Stop } catch { [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME Path = $path Exists = $false } continue } foreach ($resolvedPath in $resolvedPaths) { [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME Path = $resolvedPath Exists = $true } } } $result.Results = $pathResults $result.ExistsAll = @($pathResults).Where{ -not $_.Exists }.Count -lt 1 $result.ExistsAny = @($pathResults).Where{ $_.Exists }.Count -gt 0 if ($PathHandling -eq 'All') { $result.Success = $result.ExistsAll } else { $result.Success = $result.ExistsAny } if (-not $result.Success) { $message = "[$env:COMPUTERNAME] Path not found: $(@($pathResults).Where{ -not $_.Exists }.ForEach{ "'$($_.Path)'" } -join ', ')" $result.Error = [System.Management.Automation.ErrorRecord]::new( [System.Exception]::new($message), 'PathNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, @(@($pathResults).Where{ -not $_.Exists }.ForEach{ "'$($_.Path)'" }) ) } $result } #endregion Code $killIt = $ErrorActionPreference -eq 'Stop' if (-not $Scope) { $Scope = 'AllUsers' if (-not $ManagedSession) { $Scope = 'CurrentUser' } } $scopeObject = $script:moduleScopes[$Scope] if (-not $scopeObject) { Stop-PSFFunction -String 'Resolve-ModuleScopePath.Error.ScopeNotFound' -StringValues $Scope, ((Get-PSFModuleScope).Name -join ', ') -Cmdlet $Cmdlet -EnableException $killIt return } } process { if (Test-PSFFunctionInterrupt) { return } #region Collect Test-Results if (-not $ManagedSession) { $testResult = & $code $scopeObject, $PathHandling } else { $failed = $null $testResult = Invoke-PSFCommand -ComputerName $ManagedSession.Session -ScriptBlock $code -ArgumentList $scopeObject, $PathHandling -ErrorAction SilentlyContinue -ErrorVariable failed $failedResults = foreach ($failedTarget in $failed) { [PSCustomObject]@{ ComputerName = $failedTarget.TargetObject Scope = $scopeObject.Name Path = $scopeObject.Path Results = @() ExistsAll = $null ExistsAny = $null Success = $false Error = $failedTarget SessionID = $null Session = $null } } $testResult = @($testResult) + @($failedResults) | Remove-PSFNull } #endregion Collect Test-Results #region Evaluate Success foreach ($result in $testResult) { if ($result.SessionID) { $result.Session = @($ManagedSession).Where{ $_.ID -eq $result.SessionID }[0] } [PSFramework.Object.ObjectHost]::AddScriptMethod($result, 'ToString', { '{0}: {1}' -f $this.ComputerName, ($this.Path -join ' | ') }) if ($result.Success) { continue } if (-not $result.Results) { Write-PSFMessage -String 'Resolve-ModuleScopePath.Error.UnReached' -StringValues $result.ComputerName, ($result.Path -join ' | ') -Tag fail, connect -Target $result } else { Write-PSFMessage -String 'Resolve-ModuleScopePath.Error.NotFound' -StringValues $result.ComputerName, (@($result.Results).Where{ -not $_.Exists }.Path -join ' | ') -Tag fail, notfound -Target $result } $Cmdlet.WriteError($result.Error) } if ($TargetHandling -eq 'All' -and @($testResult).Where{ -not $_.Success }.Count -gt 0) { Stop-PSFFunction -String 'Resolve-ModuleScopePath.Fail.NotAll' -StringValues (@($testResult).Where{-not $_.Success }.ComputerName -join ' | ') -EnableException $true -Cmdlet $Cmdlet } if ($TargetHandling -eq 'Any' -and @($testResult).Where{ $_.Success }.Count -eq 0) { Stop-PSFFunction -String 'Resolve-ModuleScopePath.Fail.NotAny' -StringValues ($testResult.ComputerName -join ' | ') -EnableException $true -Cmdlet $Cmdlet } #endregion Evaluate Success $testResult } } function Resolve-ModuleTarget { <# .SYNOPSIS Resolves the search criteria for modules to save or install. .DESCRIPTION Resolves the search criteria for modules to save or install. For each specified module, it will return a result including the parameters Save-Module and Save-PSResource will need. .PARAMETER InputObject A module object to retrieve. Can be the output of Get-Module, Find-Module, Find-PSResource or Find-PSFModule. .PARAMETER Name The name of the module to resolve. .PARAMETER Version The version condition for the module. Supports a fairly flexible syntax. Examples: - 2.0.0 # Exactly v2.0.0 - 2.1.0-RC2 # Preview "RC2" of exactly version 2.1.0 - 2.0.0-2.4.5 # Any version at least 2.0.0 and at most 2.4.5 - [2.0,3.0) # At least 2.0 but less than 3.0 - [2.0-3.0) # At least 2.0 but less than 3.0 .PARAMETER Prerelease Include Prerelease versions. Redundant if asking for a specific version with a specific prerelease suffix. .PARAMETER Cmdlet The $PSCmdlet variable of the caller. As this is an internal utility command, this allows it to terminate in the context of the calling command and remain invisible to the user. .EXAMPLE PS C:\> Resolve-ModuleTarget -InputObject $InputObject -Cmdlet $PSCmdlet Resolves the object as a module target. In case of error, the terminating error will happen within the scope of the caller. #> [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, ParameterSetName = 'ByObject')] [object[]] $InputObject, [Parameter(ParameterSetName = 'ByName')] [string[]] $Name, [Parameter(ParameterSetName = 'ByName')] [AllowEmptyString()] [string] $Version, [Parameter(ParameterSetName = 'ByName')] [switch] $Prerelease, $Cmdlet = $PSCmdlet ) begin { function New-ModuleTarget { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( $Object, [string] $Name, [AllowEmptyString()] [string] $Version, [switch] $Prerelease, $Cmdlet ) $v2Param = @{ } $v3Param = @{ } $actualName = $Name $versionString = '' if ($Object) { $v3Param.InputObject = $Object $v2Param.Name = $Object.Name $v2Param.RequiredVersion = $Object.AdditionalMetadata.NormalizedVersion $versionString = $Object.AdditionalMetadata.NormalizedVersion $actualName = $Object.Name # V3 if ($Object.IsPrerelease) { $v2Param.AllowPrerelease = $true } # V2 if ($Object.AdditionalMetadata.IsPrerelease) { $v2Param.AllowPrerelease = $true } # Get-Module if ($Object -is [System.Management.Automation.PSModuleInfo]) { $versionString = $Object.Version $v2Param.RequiredVersion = $Object.Version if ($Object.PrivateData.PSData.Prerelease) { $v2Param.AllowPrerelease = $true $v2Param.RequiredVersion = '{0}-{1}' -f $Object.Version, $Object.PrivateData.PSData.Prerelease } } } else { $v2Param.Name = $Name $v3Param.Name = $Name if ($Prerelease) { $v2Param.AllowPrerelease = $true $v3Param.Prerelease = $true } if ($Version) { $versionData = Read-VersionString -Version $Version -Cmdlet $Cmdlet $v3Param.Version = $versionData.V3String if ($versionData.Required) { $v2Param.RequiredVersion = $versionData.Required } else { if ($versionData.Minimum) { $v2Param.MinimumVersion = $versionData.Minimum } if ($versionData.Maximum) { $v2Param.MaximumVersion = $versionData.Maximum } } if ($versionData.Prerelease) { $v2Param.AllowPrerelease = $true $v3Param.Prerelease = $true } } } [PSCustomObject]@{ PSTypeName = 'PSFramework.NuGet.ModuleTarget' Name = $actualName TargetName = "$actualName ($versionString)" Version = $versionString V2Param = $v2Param V3Param = $v3Param } } } process { foreach ($object in $InputObject) { # Case 1: Find-PSFModule if ($object.PSObject.TypeNames -contains 'PSFramework.NuGet.ModuleInfo') { New-ModuleTarget -Object $object.Object -Cmdlet $Cmdlet } # Case 2: Find-Module # Case 3: Find-PSResource # Case 4: Get-Module else { New-ModuleTarget -Object $object -Cmdlet $Cmdlet } } foreach ($nameEntry in $Name) { New-ModuleTarget -Name $nameEntry -Version $Version -Prerelease:$Prerelease -Cmdlet $Cmdlet } } } function Resolve-RemotePath { <# .SYNOPSIS Test for target paths on remote computers. .DESCRIPTION Test for target paths on remote computers. Has differentiated error handling (see description on TargetHandling or examples), in order to ensure proper tracking of all parallely processed targets. .PARAMETER Path The paths to check. .PARAMETER ComputerName The computers to check the paths on. Supports established PSSession objects. .PARAMETER ManagedSession Managed Remoting Sessions to associate with the paths resolved. Used later to bulk-process the paths in parallel. .PARAMETER PathHandling Whether all specified paths must exist on a target computer, or whether a single finding counts as success. Defaults to: All .PARAMETER TargetHandling How the command should handle unsuccessful computer targets: All unsuccessful checks lead to a non-terminating exception. However, depending on this parameter, a forced terminating exception might be thrown: - "All": Even a single unsuccessful computer leads to terminal errors. - "Any": If no target was successful, terminate - "None": Never terminate Defaults to: None .PARAMETER Cmdlet The $PSCmdlet variable of the caller. As this is an internal utility command, this allows it to terminate in the context of the calling command and remain invisible to the user. .EXAMPLE PS C:\> Resolve-RemotePath -Path C:\Temp -ComputerName $computers Checks for C:\Temp on all computers in $computers Will not generate any terminating errors. .EXAMPLE PS C:\> Resolve-RemotePath -Path C:\Temp -ComputerName $computers -TargetHandling All Checks for C:\Temp on all computers in $computers If even a single computer cannot be reached or does not have the path, this will terminate the command. .EXAMPLE PS C:\> Resolve-RemotePath -Path C:\Temp, C:\Tmp -ComputerName $computers -TargetHandling All -PathHandling Any Checks for C:\Temp or C:\Tmp on all computers in $computers Each computer is considered successful, if one of the two paths exist on it. If even a single computer is not successful - has neither path or cannot be reached - this command will terminate. .EXAMPLE PS C:\> Resolve-RemotePath -Path C:\Temp, C:\Tmp -ComputerName $computers -TargetHandling Any -PathHandling Any -ErrorAction SilentlyContinue Checks for C:\Temp or C:\Tmp on all computers in $computers Each computer is considered successful, if one of the two paths exist on it. This command will continue unbothered, so long as at least one computer is successful. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string[]] $Path, [AllowEmptyCollection()] [AllowNull()] [PSFComputer[]] $ComputerName, $ManagedSession, [ValidateSet('All', 'Any')] [string] $PathHandling = 'All', [ValidateSet('All', 'Any', 'None')] [string] $TargetHandling = 'None', $Cmdlet = $PSCmdlet ) begin { #region Implementing Code $code = { param ($Data) $pathResults = foreach ($path in $Data.Path) { try { $resolvedPaths = Resolve-Path -Path $path -ErrorAction Stop } catch { [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME Path = $path Exists = $false } continue } foreach ($resolvedPath in $resolvedPaths) { [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME Path = $resolvedPath Exists = $true } } } $result = [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME Path = $Data.Path Results = $pathResults ExistsAll = @($pathResults).Where{ -not $_.Exists }.Count -lt 1 ExistsAny = @($pathResults).Where{ $_.Exists }.Count -gt 0 Success = $null Error = $null SessionID = $global:__PsfSessionId Session = $null } if ($Data.PathHandling -eq 'All') { $result.Success = $result.ExistsAll } else { $result.Success = $result.ExistsAny } if (-not $result.Success) { $message = "[$env:COMPUTERNAME] Path not found: $(@($pathResults).Where{ -not $_.Exists }.ForEach{ "'$($_.Path)'" } -join ', ')" $result.Error = [System.Management.Automation.ErrorRecord]::new( [System.Exception]::new($message), 'PathNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, @(@($pathResults).Where{ -not $_.Exists }.ForEach{ "'$($_.Path)'" }) ) } $result } #endregion Implementing Code # Passing a single array-argument as a hashtable is more reliable $data = @{ Path = $Path; PathHandling = $PathHandling } } process { #region Collect Test-Results if (-not $ComputerName) { $testResult = & $code $data } else { $failed = $null $testResult = Invoke-PSFCommand -ComputerName $ComputerName -ScriptBlock $code -ArgumentList $data -ErrorAction SilentlyContinue -ErrorVariable failed $failedResults = foreach ($failedTarget in $failed) { [PSCustomObject]@{ ComputerName = $failedTarget.TargetObject Path = $Path Results = @() ExistsAll = $null ExistsAny = $null Success = $false Error = $failedTarget SessionID = $null Session = $null } } $testResult = @($testResult) + @($failedResults) | Remove-PSFNull } #endregion Collect Test-Results foreach ($result in $testResult) { if ($result.SessionID) { $result.Session = @($ManagedSession).Where{ $_.ID -eq $result.SessionID }[0] } [PSFramework.Object.ObjectHost]::AddScriptMethod($result, 'ToString', { '{0}: {1}' -f $this.ComputerName, ($this.Path -join ' | ') }) if ($result.Success) { continue } if (-not $result.Results) { Write-PSFMessage -String 'Resolve-RemotePath.Error.UnReached' -StringValues $result.ComputerName, ($Path -join ' | ') -Tag fail, connect -Target $result } else { Write-PSFMessage -String 'Resolve-RemotePath.Error.NotFound' -StringValues $result.ComputerName, (@($result.Results).Where{ -not $_.Exists }.Path -join ' | ') -Tag fail, notfound -Target $result } $Cmdlet.WriteError($result.Error) } if ($TargetHandling -eq 'All' -and @($testResult).Where{ -not $_.Success }.Count -gt 0) { Stop-PSFFunction -String 'Resolve-RemotePath.Fail.NotAll' -StringValues (@($testResult).Where{-not $_.Success }.ComputerName -join ' | '), ($Path -join ' | ') -EnableException $true -Cmdlet $Cmdlet } if ($TargetHandling -eq 'Any' -and @($testResult).Where{ $_.Success }.Count -eq 0) { Stop-PSFFunction -String 'Resolve-RemotePath.Fail.NotAny' -StringValues ($testResult.ComputerName -join ' | '), ($Path -join ' | ') -EnableException $true -Cmdlet $Cmdlet } $testResult } } function Resolve-Repository { <# .SYNOPSIS Resolves the PowerShell Repository to use, including their order. .DESCRIPTION Resolves the PowerShell Repository to use, including their order. This differs from Get-PSFRepository by throwing a terminating exception in case no repository was found. .PARAMETER Name Names of the Repositories to lookup. Can be multiple, can use wildcards. .PARAMETER Type Whether to return PSGet V2, V3 or all repositories. Defaults to: "All" .PARAMETER Cmdlet The $PSCmdlet variable of the caller. As this is an internal utility command, this allows it to terminate in the context of the calling command and remain invisible to the user. .EXAMPLE PS C:\> Resolve-Repository -Name PSGallery, Contoso -Cmdlet $PSCmdlet Returns all repositories instances named PSGallery or Contoso, whether registered in V2 or V3 .EXAMPLE Ps C:\> Resolve-Repository -Name PSGallery -Type V3 -Cmdlet $PSCmdlet Returns the PSGet V3 instance of the PSGallery repository. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string[]] $Name, [ValidateSet('All', 'V2', 'V3')] [string] $Type = 'All', $Cmdlet = $PSCmdlet ) process { $repos = Get-PSFRepository -Name $Name -Type $Type if (-not $repos) { Stop-PSFFunction -String 'Resolve-Repository.Error.NoRepo' -StringValues ($Name -join ', '), $Type -EnableException $true -Cmdlet $Cmdlet -Category ObjectNotFound } $repos } } function Save-StagingModule { <# .SYNOPSIS Downloads modules from a repository into a specified, local path. .DESCRIPTION Downloads modules from a repository into a specified, local path. This is used internally by Save-PSFModule to cache modules to deploy in one central location that is computer-local. .PARAMETER InstallData The specifics of the module to download. The result of the Resolve-ModuleTarget command, it contains V2/V3 specific targeting information. .PARAMETER Path The path where to save them to. .PARAMETER Repositories The repositories to contact. Must be repository objects as returned by Get-PSFRepository. Repository priority will be adhered. .PARAMETER Credential The Credentials to use for accessing the repositories. .PARAMETER SkipDependency Do not include any dependencies. Works with PowerShellGet V1/V2 as well. .PARAMETER AuthenticodeCheck Whether modules must be correctly signed by a trusted source. Uses "Get-PSFModuleSignature" for validation. Defaults to: $false Default can be configured under the 'PSFramework.NuGet.Install.AuthenticodeSignature.Check' setting. .PARAMETER TrustRepository Whether we should trust the repository installed from and NOT ask users for confirmation. .PARAMETER Cmdlet The $PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user. .EXAMPLE PS C:\> Save-StagingModule -InstallData $installData -Path $tempDirectory -Repositories $repositories -Cmdlet $PSCmdlet -Credential $Credential -SkipDependency:$SkipDependency -AuthenticodeCheck:$AuthenticodeCheck Downloads modules from a repository into a specified, local path. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] param ( [object[]] $InstallData, [string] $Path, [object[]] $Repositories, [AllowNull()] [PSCredential] $Credential, [switch] $SkipDependency, [switch] $AuthenticodeCheck, [switch] $TrustRepository, $Cmdlet = $PSCmdlet ) begin { #region Implementing Functions function Save-StagingModuleV2 { [CmdletBinding()] param ( $Repository, $Item, [string] $Path, [AllowNull()] [PSCredential] $Credential, [switch] $SkipDependency, [switch] $AuthenticodeCheck ) Write-PSFMessage -String 'Save-StagingModule.SavingV2.Start' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item $callSpecifics = @{ AcceptLicense = $true ErrorAction = 'Stop' Repository = $Repository.Name } if ($Credential) { $callSpecifics.Credential = $Credential } if ($Repository.Credential) { $callSpecifics.Credential = $Repository.Credential } $result = [PSCustomObject]@{ Success = $false Error = $null ModuleName = $Item.Name ModuleVersion = $item.Version RepositoryName = $Repository.Name RepositoryType = $Repository.Type } $tempDirectory = New-PSFTempDirectory -Name StagingSub -ModuleName PSFramework.NuGet $param = $Item.v2Param # 1) Save to temp folder try { Save-Module @param -Path $tempDirectory @callSpecifics } catch { Write-PSFMessage -String 'Save-StagingModule.SavingV2.Error.Download' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item -Tag fail, save -ErrorRecord $_ $result.Error = $_ Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet Write-PSFMessage -String 'Save-StagingModule.SavingV2.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item return $result } # 2) Remove redundant modules if ($SkipDependency) { # V2 Does not support saving without its dependencies coming along, so we cleanup in pre-staging try { Get-ChildItem -Path $tempDirectory | Where-Object Name -NE $Item.Name | Remove-Item -Force -Recurse -ErrorAction Stop } catch { Write-PSFMessage -String 'Save-StagingModule.SavingV2.Error.DependencyCleanup' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item -Tag fail, cleanup -ErrorRecord $_ $result.Error = $_ Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet Write-PSFMessage -String 'Save-StagingModule.SavingV2.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item return $result } } # 3) Verify Signature if ($AuthenticodeCheck) { $signatures = foreach ($moduleBase in Get-ChildItem -Path $tempDirectory) { Get-PSFModuleSignature -Path (Get-Item -Path "$moduleBase\*").FullName } foreach ($signature in $signatures) { Write-PSFMessage -String 'Save-StagingModule.SavingV2.SignatureCheck' -StringValues $signature.Name, $signature.Version, $signature.IsSigned -Target $signature } if ($unsigned = @($signatures).Where{ -not $_.IsSigned }) { $result.Error = [System.Management.Automation.ErrorRecord]::new( [System.Exception]::new("Modules are not signed by a trusted code signer: $($unsigned.Name -join ', ')"), 'NotTrusted', [System.Management.Automation.ErrorCategory]::SecurityError, $unsigned ) Write-PSFMessage -String 'Save-StagingModule.SavingV2.Error.Unsigned' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item -Tag fail, signed -ErrorRecord $result.Error Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet Write-PSFMessage -String 'Save-StagingModule.SavingV2.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item return $result } } # 4) Move to Staging try { Get-ChildItem -Path $tempDirectory | Copy-Item -Destination $Path -Recurse -Force -ErrorAction Stop } catch { Write-PSFMessage -String 'Save-StagingModule.SavingV2.Error.Transfer' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item -Tag fail, save -ErrorRecord $_ $result.Error = $_ Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet Write-PSFMessage -String 'Save-StagingModule.SavingV2.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item return $result } Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet $result.Success = $true Write-PSFMessage -String 'Save-StagingModule.SavingV2.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item $result } function Save-StagingModuleV3 { [CmdletBinding()] param ( $Repository, $Item, [string] $Path, [AllowNull()] [PSCredential] $Credential, [switch] $SkipDependency, [switch] $AuthenticodeCheck, [switch] $TrustRepository ) Write-PSFMessage -String 'Save-StagingModule.SavingV3.Start' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item $callSpecifics = @{ ErrorAction = 'Stop' Repository = $Repository.Name } if ((Get-Command Save-PSResource).Parameters.Keys -contains 'AcceptLicense') { $callSpecifics.AcceptLicense = $true } if ($Credential) { $callSpecifics.Credential = $Credential } if ($Repository.Credential) { $callSpecifics.Credential = $Repository.Credential } if ($SkipDependency) { $callSpecifics.SkipDependencyCheck = $true } $result = [PSCustomObject]@{ Success = $false Error = $null ModuleName = $Item.Name ModuleVersion = $item.Version RepositoryName = $Repository.Name RepositoryType = $Repository.Type } $tempDirectory = New-PSFTempDirectory -Name StagingSub -ModuleName PSFramework.NuGet $param = $Item.v3Param # 1) Save to temp folder try { Save-PSResource @param -Path $tempDirectory @callSpecifics } catch { Write-PSFMessage -String 'Save-StagingModule.SavingV3.Error.Download' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item -Tag fail, save -ErrorRecord $_ $result.Error = $_ Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet Write-PSFMessage -String 'Save-StagingModule.SavingV3.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item return $result } # 2) Verify Signature if ($AuthenticodeCheck) { $signatures = foreach ($moduleBase in Get-ChildItem -Path $tempDirectory) { Get-PSFModuleSignature -Path (Get-Item -Path "$moduleBase\*").FullName } foreach ($signature in $signatures) { Write-PSFMessage -String 'Save-StagingModule.SavingV3.SignatureCheck' -StringValues $signature.Name, $signature.Version, $signature.IsSigned -Target $signature } if ($unsigned = @($signatures).Where{ -not $_.IsSigned }) { $result.Error = [System.Management.Automation.ErrorRecord]::new( [System.Exception]::new("Modules are not signed by a trusted code signer: $($unsigned.Name -join ', ')"), 'NotTrusted', [System.Management.Automation.ErrorCategory]::SecurityError, $unsigned ) Write-PSFMessage -String 'Save-StagingModule.SavingV3.Error.Unsigned' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item -Tag fail, signed -ErrorRecord $result.Error Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet Write-PSFMessage -String 'Save-StagingModule.SavingV3.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item return $result } } # 3) Move to Staging try { Get-ChildItem -Path $tempDirectory | Copy-Item -Destination $Path -Recurse -Force -ErrorAction Stop } catch { Write-PSFMessage -String 'Save-StagingModule.SavingV3.Error.Transfer' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item -Tag fail, save -ErrorRecord $_ $result.Error = $_ Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet Write-PSFMessage -String 'Save-StagingModule.SavingV3.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item return $result } Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet $result.Success = $true Write-PSFMessage -String 'Save-StagingModule.SavingV3.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item $result } #endregion Implementing Functions $common = @{ SkipDependency = $SkipDependency AuthenticodeCheck = $AuthenticodeCheck Path = $Path Credential = $Credential } } process { :item foreach ($installItem in $InstallData) { $saveResults = foreach ($repository in $Repositories | Set-PSFObjectOrder -Property Priority, '>Type') { $saveResult = switch ($repository.Type) { V2 { Save-StagingModuleV2 -Repository $repository -Item $installItem @common } V3 { Save-StagingModuleV3 -Repository $repository -Item $installItem -TrustRepository:$TrustRepository @common } default { Stop-PSFFunction -String 'Save-StagingModule.Error.UnknownRepoType' -StringValues $repository.Type, $repository.Name -Target $repository -Cmdlet $Cmdlet -EnableException $true } } if ($saveResult.Success) { continue item } $saveResult } # Only reached if no repository was successful foreach ($result in $saveResults) { $Cmdlet.WriteError($result.Error) } Stop-PSFFunction -String 'Save-StagingModule.Error.SaveFailed' -StringValues $installItem.Name, $installItem.Version, (@($repository).ForEach{ '{0} ({1})' -f $_.Name, $_.Type } -join ', ') -Target $installItem -Cmdlet $Cmdlet -EnableException $true } } } function Assert-V2Publishing { <# .SYNOPSIS Ensures users are warned when trying to publish using GetV2 on a system possibly not configured as such. .DESCRIPTION Ensures users are warned when trying to publish using GetV2 on a system possibly not configured as such. Warning only shown once per session. .PARAMETER Cmdlet The PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user. .EXAMPLE ps C:\> Assert-V2Publishing -Cmdlet $PSCmdlet Ensures users are warned when trying to publish using GetV2 on a system possibly not configured as such. #> [CmdletBinding()] param ( $Cmdlet = $PSCmdlet ) process { if ($script:psget.v2CanPublish) { return } Write-PSFMessage -Level Warning -String 'Assert-V2Publishing.CannotPublish' -PSCmdlet $Cmdlet -Once GetV2Publish } } function Copy-Module { <# .SYNOPSIS Copies the content of a module to a staging path and returns information about the module copied. .DESCRIPTION Copies the content of a module to a staging path and returns information about the module copied. This is intended to simplify the pre-publish preparation steps and help avoid modifying the actual sources by accident. .PARAMETER Path Path where the module files are. .PARAMETER Destination Destination Path to copy to. .PARAMETER Cmdlet The PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user. .PARAMETER Continue In case of error, call continue unless ErrorAction is set to Stop. Simplifies error handling in non-terminating situations. .PARAMETER ContinueLabel When used together with "-Contionue", it allowd you to specify the label/name of the loop to continue with. .EXAMPLE PS C:\> Copy-Module -Path $sourceModule -Destination $workingDirectory -Cmdlet $PSCmdlet -Continue Creates a copy of $sourceModule in $workingDirectory #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $true)] [string] $Destination, $Cmdlet = $PSCmdlet, [switch] $Continue, [string] $ContinueLabel ) begin { $killIt = $ErrorActionPreference -eq 'Stop' $stopCommon = @{ Cmdlet = $Cmdlet EnableException = $killIt } if ($Continue) { $stopCommon.Continue = $true } if ($ContinueLabel) { $stopCommon.ContinueLabel = $ContinueLabel } } process { $sourceDirectoryPath = $Path if ($Path -like '*.psd1') { $sourceDirectoryPath = Split-Path -Path $Path } $moduleName = Split-Path -Path $sourceDirectoryPath -Leaf if ($moduleName -match '^\d+(\.\d+){1,3}$') { $moduleName = Split-Path -Path (Split-Path -Path $sourceDirectoryPath) -Leaf } #region Validation $manifestPath = Join-Path -Path $sourceDirectoryPath -ChildPath "$moduleName.psd1" if (-not (Test-Path -Path $manifestPath)) { Stop-PSFFunction -String 'Copy-Module.Error.ManifestNotFound' -StringValues $Path -Target $Path @stopCommon -Category ObjectNotFound return } $tokens = $null $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($manifestPath, [ref]$tokens, [ref]$errors) if ($errors) { Stop-PSFFunction -String 'Copy-Module.Error.ManifestSyntaxError' -StringValues $manifestPath -Target $Path @stopCommon -Category ObjectNotFound return } #endregion Validation #region Deploy to Staging try { $null = New-Item -Path $Destination -Name $moduleName -ItemType Directory -ErrorAction Stop } catch { Stop-PSFFunction -String 'Copy-Module.Error.StagingFolderFailed' -StringValues $Path -Target $Path @stopCommon -ErrorRecord $_ return } $destinationPath = Join-Path -Path $Destination -ChildPath $moduleName try { Copy-Item -Path "$($sourceDirectoryPath.Trim('\/'))\*" -Destination $destinationPath -Recurse -Force -ErrorAction Stop } catch { Stop-PSFFunction -String 'Copy-Module.Error.StagingFolderCopy' -StringValues $Path -Target $Path @stopCommon -ErrorRecord $_ return } #endregion Deploy to Staging $hashtableAst = $ast.EndBlock.Statements[0].PipelineElements[0].Expression [PSCustomObject]@{ Name = $moduleName Path = $destinationPath ManifestPath = Join-Path -Path $destinationPath -ChildPath "$moduleName.psd1" SourcePath = $sourceDirectoryPath Author = @($hashtableAst.KeyValuePairs).Where{ $_.Item1.Value -eq 'Author' }.Item2.PipelineElements.Expression.Value Version = @($hashtableAst.KeyValuePairs).Where{ $_.Item1.Value -eq 'ModuleVersion' }.Item2.PipelineElements.Expression.Value Description = @($hashtableAst.KeyValuePairs).Where{ $_.Item1.Value -eq 'Description' }.Item2.PipelineElements.Expression.Value RequiredModules = Read-ManifestDependency -Path $manifestPath } } } function New-DummyModule { <# .SYNOPSIS Creates an empty dummy module. .DESCRIPTION Creates an empty dummy module. This is used for publishing Resource Modules, the purpose of which are the files later copied into it and not its nature as a module. .PARAMETER Path Where to create the dummy module. .PARAMETER Name The name of the module to assign. .PARAMETER Version What version should the module have? Defaults to: 1.0.0 .PARAMETER Description A description to include in the dummy module. Defaults to a pointless placeholder. .PARAMETER Author Who is the author? Defaults to the current user's username. .PARAMETER RequiredModules Any dependencies to include. Uses the default module-spec syntax. .PARAMETER Cmdlet The PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user. .EXAMPLE PS C:\> New-DummyModule -Path $stagingDirectory -Name $Name -Version $Version -RequiredModules $RequiredModules -Description $Description -Author $Author -Cmdlet $PSCmdlet Creates a new dummy module in $stagingDirectory #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $true)] [string] $Name, [string] $Version = '1.0.0', [string] $Description = '<Dummy Description>', [AllowEmptyString()] [string] $Author, [object[]] $RequiredModules ) process { $param = @{ Path = Join-Path -Path $Path -ChildPath "$Name.psd1" RootModule = "$Name.psm1" ModuleVersion = $Version Description = $Description } if ($Author) { $param.Author = $Author } if ($RequiredModules) { $param.RequiredModules = $RequiredModules } New-ModuleManifest @param $null = New-Item -Path $Path -Name "$Name.psm1" -ItemType File } } function Publish-ModuleToPath { <# .SYNOPSIS Publishes a module to a specific target path- .DESCRIPTION Publishes a module to a specific target path- This will create an appropriate .nupkg file in the target location. Dependencies will not be considered when publishing like this. The module cannot already exist in the target path. .PARAMETER Module The module to publish. Expects a module information object as returned by Copy-Module. .PARAMETER Path The path to publish to. .PARAMETER ForceV3 Force publishing via PSResourceGet. By default it uses the latest version the module detected as available. This is primarily used for internal testing of the command without a module context. .PARAMETER Cmdlet The PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Publish-ModuleToPath -Module $moduleData -Path $DestinationPath -Cmdlet $PSCmdlet Publishes the module to the provided $DestinationPath. #> [CmdletBinding(SupportsShouldProcess = $true)] param ( $Module, [string] $Path, [switch] $ForceV3, $Cmdlet = $PSCmdlet ) begin { $killIt = $ErrorActionPreference -eq 'Stop' $useV3 = $script:psget.V3 -or $ForceV3 if (-not $useV3) { Assert-V2Publishing -Cmdlet $Cmdlet } $stagingDirectory = New-PSFTempDirectory -ModuleName PSFramework.NuGet -Name Publish.StagingLocalCopy } process { #region Verify Existing Module in Repository $fileName = '{0}.{1}.nupkg' -f $Module.Name, $Module.Version $destinationFile = Join-Path -Path $Path -ChildPath $fileName if (Test-Path -Path $destinationFile) { Stop-PSFFunction -String 'Publish-ModuleToPath.Error.AlreadyPublished' -StringValues $Module.Name, $Module.Version, $Path -EnableException $killIt -Category InvalidOperation return } #endregion Verify Existing Module in Repository $repoName = "PSF_Temp_$(Get-Random)" #region V3 if ($useV3) { try { Register-PSResourceRepository -Name $repoName -Uri $stagingDirectory -Trusted Publish-PSResource -Path $Module.Path -Repository $repoName -SkipDependenciesCheck } catch { Stop-PSFFunction -String 'Publish-ModuleToPath.Error.FailedToStaging.V3' -StringValues $module.Name, $module.Version -Cmdlet $Cmdlet -ErrorRecord $_ -EnableException $killIt return } finally { Unregister-PSResourceRepository -Name $repoName } } #endregion V3 #region V2 else { try { Register-PSRepository -Name $repoName -SourceLocation $stagingDirectory -PublishLocation $stagingDirectory -InstallationPolicy Trusted Disable-ModuleCommand -Name 'Get-ModuleDependencies' -ModuleName 'PowerShellGet' Publish-Module -Path $Module.Path -Repository $repoName } catch { Stop-PSFFunction -String 'Publish-ModuleToPath.Error.FailedToStaging.V2' -StringValues $module.Name, $module.Version -Cmdlet $Cmdlet -ErrorRecord $_ -EnableException $killIt return } finally { Enable-ModuleCommand -Name 'Get-ModuleDependencies' -ModuleName 'PowerShellGet' Unregister-PSRepository -Name $repoName } } #endregion V2 #region Copy New Package $sourcePath = Join-Path -Path $stagingDirectory -ChildPath $fileName Invoke-PSFProtectedCommand -ActionString 'Publish-ModuleToPath.Publishing' -ActionStringValues $module.Name, $module.Version -Target $Path -ScriptBlock { Copy-Item -Path $sourcePath -Destination $Path -Force -ErrorAction Stop -Confirm:$false } -PSCmdlet $Cmdlet -EnableException $killIt #endregion Copy New Package } end { Remove-PSFTempItem -ModuleName PSFramework.NuGet -Name Publish.StagingLocalCopy } } function Publish-ModuleV2 { <# .SYNOPSIS Publishes a PowerShell module using PowerShellGet V2. .DESCRIPTION Publishes a PowerShell module using PowerShellGet V2. .PARAMETER Module The module to publish. Expects an module information object as returned by Copy-Module. .PARAMETER Repository The repository to publish to. Expects a repository object as returned by Get-PSFRepository. .PARAMETER ApiKey The ApiKey for authenticating the request. Generally needed when publishing to the PowerShell gallery. .PARAMETER Credential The credentials to use for authenticating the request. Generally needed when publishing to internal repositories. .PARAMETER SkipDependenciesCheck Do not check for required modules, do not validate the module manifest. By default, it will check, whether all required modules are already published to the repository. However, it also - usually - requires all modules to be locally available when publishing. With this parameter set, that is no longer an issue. .PARAMETER Continue In case of error, call continue unless ErrorAction is set to Stop. Simplifies error handling in non-terminating situations. .PARAMETER ContinueLabel When used together with "-Contionue", it allowd you to specify the label/name of the loop to continue with. .PARAMETER Cmdlet The PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Publish-ModuleV2 -Module $module -Repository $repo -SkipDependenciesCheck -ApiKey $key Publishes the module provided in $module to the repository $repo, authenticating the request with the Api key $key. It will not validate any dependencies as it does so. #> [CmdletBinding(SupportsShouldProcess = $true)] param ( $Module, $Repository, [string] $ApiKey, [PSCredential] $Credential, [switch] $SkipDependenciesCheck, [switch] $Continue, [string] $ContinueLabel, $Cmdlet = $PSCmdlet ) process { $killIt = $ErrorActionPreference -eq 'Stop' $commonPublish = @{ Repository = $Repository.Name Confirm = $false } if ($Repository.Credential) { $commonPublish.Credential = $Credential } if ($Credential) { $commonPublish.Credential = $Credential } if ($ApiKey) { $commonPublish.NuGetApiKey = $ApiKey } if ($SkipDependenciesCheck) { Disable-ModuleCommand -Name 'Get-ModuleDependencies' -ModuleName 'PowerShellGet' $customReturn = Get-Module $Module.Path -ListAvailable Disable-ModuleCommand -Name 'Microsoft.PowerShell.Core\Test-ModuleManifest' -ModuleName 'PowerShellGet' -Return $customReturn } try { Invoke-PSFProtectedCommand -ActionString 'Publish-ModuleV2.Publish' -ActionStringValues $Module.Name, $Module.Version, $Repository.Name -ScriptBlock { Publish-Module @commonPublish -Path $Module.Path -ErrorAction Stop } -Target "$($Module.Name) ($($Module.Version))" -PSCmdlet $Cmdlet -EnableException $killIt -Continue:$Continue -ContinueLabel $ContinueLabel } finally { if ($SkipDependenciesCheck) { Enable-ModuleCommand -Name 'Get-ModuleDependencies' -ModuleName 'PowerShellGet' Enable-ModuleCommand -Name 'Microsoft.PowerShell.Core\Test-ModuleManifest' -ModuleName 'PowerShellGet' } } } } function Publish-ModuleV3 { <# .SYNOPSIS Publishes a PowerShell module using PowerShellGet V3. .DESCRIPTION Publishes a PowerShell module using PowerShellGet V3. .PARAMETER Module The module to publish. Expects an module information object as returned by Copy-Module. .PARAMETER Repository The repository to publish to. Expects a repository object as returned by Get-PSFRepository. .PARAMETER ApiKey The ApiKey for authenticating the request. Generally needed when publishing to the PowerShell gallery. .PARAMETER Credential The credentials to use for authenticating the request. Generally needed when publishing to internal repositories. .PARAMETER SkipDependenciesCheck Do not check for required modules, do not validate the module manifest. By default, it will check, whether all required modules are already published to the repository. However, it also - usually - requires all modules to be locally available when publishing. With this parameter set, that is no longer an issue. .PARAMETER Continue In case of error, call continue unless ErrorAction is set to Stop. Simplifies error handling in non-terminating situations. .PARAMETER ContinueLabel When used together with "-Contionue", it allowd you to specify the label/name of the loop to continue with. .PARAMETER Cmdlet The PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Publish-ModuleV2 -Module $module -Repository $repo -SkipDependenciesCheck -ApiKey $key Publishes the module provided in $module to the repository $repo, authenticating the request with the Api key $key. It will not validate any dependencies as it does so. #> [CmdletBinding(SupportsShouldProcess = $true)] param ( $Module, $Repository, [string] $ApiKey, [PSCredential] $Credential, [switch] $SkipDependenciesCheck, [switch] $Continue, [string] $ContinueLabel, $Cmdlet = $PSCmdlet ) process { $killIt = $ErrorActionPreference -eq 'Stop' # Ensure to now overwrite a local file if ($Repository.Uri -like 'file:*') { $targetPath = $Repository.Uri -replace '^file:///' -replace 'file:' $targetFile = Join-Path -Path $targetPath -ChildPath "$($Module.Name).$($Module.Version).nupkg" if (Test-Path -path $targetFile) { Stop-PSFFunction -String 'Publish-ModuleV3.Error.AlreadyPublished' -StringValues $Module.Name, $Module.Version, $Repository.Name -Cmdlet $Cmdlet -EnableException $killIt -Continue:$Continue -ContinueLabel $ContinueLabel -Target "$($Module.Name) ($($Module.Version))" return } } $commonPublish = @{ Repository = $Repository.Name Confirm = $false } if ($Repository.Credential) { $commonPublish.Credential = $Credential } if ($Credential) { $commonPublish.Credential = $Credential } if ($ApiKey) { $commonPublish.ApiKey = $ApiKey } if ($SkipDependenciesCheck) { $commonPublish.SkipDependenciesCheck = $SkipDependenciesCheck # Parity with V2 - Disabling the dependency check will also prevent Manifest Validation there $commonPublish.SkipModuleManifestValidate = $true } Invoke-PSFProtectedCommand -ActionString 'Publish-ModuleV3.Publish' -ActionStringValues $Module.Name, $Module.Version, $Repository.Name -ScriptBlock { Publish-PSResource @commonPublish -Path $Module.Path -ErrorAction Stop } -Target "$($Module.Name) ($($Module.Version))" -PSCmdlet $Cmdlet -EnableException $killIt -Continue:$Continue -ContinueLabel $ContinueLabel } } function Read-ManifestDependency { <# .SYNOPSIS Reads the RequiredModules from a manifest, using AST. .DESCRIPTION Reads the RequiredModules from a manifest, using AST. Will return a list of objects with the following properties: - Name: Name of the required module - Version: Version of the required module. Will return 0.0.0 if no version is required. - Exact: Whether the version constraint means EXACTLY this version, rather than AT LEAST this version. .PARAMETER Path Path to the manifest to read. .EXAMPLE PS C:\> Read-ManifestDependency -Path C:\Code\MyModule\MyModule.psd1 Returns the modules required by the MyModule module. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Path ) process { $tokens = $null $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors) $requirements = foreach ($requirement in @($ast.EndBlock.Statements[0].PipelineElements[0].Expression.KeyValuePairs).Where{ $_.Item1.Value -eq 'RequiredModules' }.Item2.PipelineElements.Expression.SubExpression.Statements) { $actualRequirement = $requirement.PipelineElements[0].Expression switch ($actualRequirement.GetType().Name) { 'HashtableAst' { [PSCustomObject]@{ Name = $actualRequirement.KeyValuePairs.Where{ $_.Item1.Value -eq 'ModuleName' }.Item2.PipelineElements.Expression.Value Version = $actualRequirement.KeyValuePairs.Where{ $_.Item1.Value -match 'Version$' }.Item2.PipelineElements.Expression.Value -as [version] Exact = $actualRequirement.KeyValuePairs.Item1.Value -contains 'RequiredVersion' } } 'StringConstantExpressionAst' { [PSCustomObject]@{ Name = $actualRequirement.Value Version = '0.0.0' -as [version] Exact = $false } } default { throw "Unexpected Module Dependency AST in $Path : $($actualRequirement.GetType().Name)" } } } foreach ($requirement in $requirements) { if ($requirement.Exact -and -not $requirement.Version) { Write-PSFMessage -Level Warning -String 'Read-ManifestDependency.Warning.VersionError' -StringValues $Path, $requirement.Name } if (-not $requirement.Version) { $requirement.Version = '0.0.0' -as [version] } $requirement } } } function Disable-ModuleCommand { <# .SYNOPSIS Disables a specific command in a specific module. .DESCRIPTION Disables a specific command in a specific module. This hides the command with an alias pointing to a mostly empty function that cares not about the parameters provided. Use "Enable-ModuleCommand" to revert the changes applied. .PARAMETER Name The name of the command to hide. .PARAMETER ModuleName The module the command to hide is from .PARAMETER Return The object the command should return when called. By default, nothing is returned. .EXAMPLE PS C:\> Disable-ModuleCommand -Name 'Get-ModuleDependencies' -ModuleName 'PowerShellGet' Prevents the command Get-ModuleDependencies from PowerShellGet from returning anything. .EXAMPLE PS C:\> Disable-ModuleCommand -Name 'Microsoft.PowerShell.Core\Test-ModuleManifest' -ModuleName 'PowerShellGet' -Return $customReturn Prevents the command Microsoft.PowerShell.Core\Test-ModuleManifest from doing its usual job. Instead it will statically return the value in $customReturn. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string] $ModuleName, $Return ) process { if ($PSBoundParameters.Keys -contains 'Return') { $script:ModuleCommandReturns[$Name] = $Return } Import-Module $ModuleName -Verbose:$False & (Get-Module $ModuleName) { function script:psfFunctionOverride { $calledAs = $MyInvocation.InvocationName $returns = & (Get-Module PSFramework.NuGet) { $script:ModuleCommandReturns } if ($returns.Keys -contains $calledAs) { $returns[$calledAs] } } Set-Alias -Name $args[0] -Value psfFunctionOverride -Scope Script } $Name } } function Enable-ModuleCommand { <# .SYNOPSIS Re-Enables a command that was previously disabled. .DESCRIPTION Re-Enables a command that was previously disabled. Use Disable-ModuleCommand to disable/override a command. .PARAMETER Name Name of the command to restore. .PARAMETER ModuleName Name of the module the command is from. .EXAMPLE PS C:\> Enable-ModuleCommand -Name 'Get-ModuleDependencies' -ModuleName 'PowerShellGet' Enables the command Get-ModuleDependencies from the module PowerShellGet #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string] $ModuleName ) process { Import-Module $ModuleName -Verbose:$False $module = Get-Module -Name $ModuleName $internal = [PSFramework.Utility.UtilityHost]::GetPrivateProperty('Internal', $module.SessionState) $mscope = [PSFramework.Utility.UtilityHost]::GetPrivateProperty('ModuleScope', $internal) [PSFramework.Utility.UtilityHost]::InvokePrivateMethod("RemoveAlias", $mscope, @($Name, $true)) } } function Resolve-AkaMsLink { <# .SYNOPSIS Resolves an aka.ms shortcut link to its full address. .DESCRIPTION Resolves an aka.ms shortcut link to its full address. This is done by sending the web request against it while limiting the redirect count to 1, then reading the error. .PARAMETER Name The full link or shorthand to resolve. Can take any of the following notations: + https://aka.ms/psgetv3 + aka.ms/psgetv3 + psgetv3 .EXAMPLE PS C:\> Resolve-AkaMsLink -Name psgetv3 Returns the Url https://aka.ms/psgetv3 points to. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string] $Name ) process { if ($Name -notmatch 'aka\.ms') { $Name = 'https://aka.ms/{0}' -f $Name.TrimStart("/") } if ($Name -notmatch '^https://') { $Name = 'https://{0}' -f $Name.TrimStart("/") } try { $null = Invoke-WebRequest -Uri $Name -MaximumRedirection 1 -ErrorAction Stop } catch { # Not doing a version check, since exact cut-over version between behaviors unknown # PS 5.1 if ($_.TargetObject.Address.AbsoluteUri) { $_.TargetObject.Address.AbsoluteUri } # PS ?+ else { $_.TargetObject.RequestUri.AbsoluteUri } } } } function Invoke-SessionCommand { <# .SYNOPSIS Executes a command in an already provided session and returns the results in a consistent manner. .DESCRIPTION Executes a command in an already provided session and returns the results in a consistent manner. This simplifies error handling, especially ErrorAction for errors that happen remotely. This command will never throw an error - it will always only return an object with three properties: + Success (bool): Whether the operation succeeded. + Error (ErrorRecord): If it failed, the error record. Will be deserialized, if it was not a remoting error. + Data (object): Any return values the scriptblock generated. .PARAMETER Session The session to invoke the command in. .PARAMETER Code The Code to execute .PARAMETER ArgumentList The arguments to pass to the code .EXAMPLE PS C:\> Invoke-SessionCommand -Session $session -Code { Remove-Item -Path C:\Temp\* -Force -Recurse -ErrorAction stop } Tries to delete all items under C:\Temp in the remote session. Successful or not, it will always return a return object, reporting the details. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Management.Automation.Runspaces.PSSession] $Session, [Parameter(Mandatory = $true)] [scriptblock] $Code, [object[]] $ArgumentList ) process { $data = @{ Code = $Code ArgumentList = $ArgumentList } $scriptblock = { param ($Data) try { # Local Execution, but with Invoke-Command so that the arguments properly enumerate $result = Invoke-Command ([scriptblock]::Create($Data.Code.ToString())) -ArgumentList $Data.ArgumentList [PSCustomObject]@{ Success = $true Error = $null Data = $result } } catch { [PSCustomObject]@{ Success = $false Error = $_ Data = $null } } } try { Invoke-Command -Session $Session -ScriptBlock $scriptblock -ArgumentList $data -ErrorAction Stop } catch { [PSCustomObject]@{ Success = $false Error = $_ Data = $null } } } } function New-ManagedSession { <# .SYNOPSIS Creates new remoting sessions where needed and flags them all with an ID that is shared, both locally and remotely. .DESCRIPTION Creates new remoting sessions where needed and flags them all with an ID that is shared, both locally and remotely. This allows easily mapping arguments when parallel invocation makes argument separation difficult. Note: While a nifty feature in general, this has been superseded in Save-PSFModule, what it originally was developed for. The command still provides useful convenience of standardizing input when provided mixed input types, as sessions outside of the PSFramework management are needed for now, but its original intent is no longer critical. .PARAMETER ComputerName The computers to deploy the modules to. Accepts both names or established PSRemoting sessions. .PARAMETER Credential Credentials to use for remoting connections (if present). .PARAMETER ConfigurationName The name of the PSSessionConfiguration to use for the remoting connection. Changing this allows you to execute remote code in PowerShell 7 if configured on the other side. This setting can be updated via configuration, using the 'PSFramework.NuGet.Remoting.DefaultConfiguration' setting. .PARAMETER Type What kind of session to create. + Temporary: Should be deleted after use. + Persistent: Should be kept around. Computer targets that are already established PSSessions will be flagged as "External" instead. .PARAMETER Cmdlet The $PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> New-ManagedSession -ComputerName $ComputerName -Credential $RemotingCredential -Cmdlet $PSCmdlet -Type Temporary Establishes sessions to all targets in $ComputerName if needed, using the credentials in $RemotingCredential (if any). The newly-established sessions will be considered temporary and should be purged before the task is done. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [AllowNull()] [PSFComputer[]] $ComputerName, [PSCredential] $Credential, [string] $ConfigurationName = (Get-PSFConfigValue -FullName 'PSFramework.NuGet.Remoting.DefaultConfiguration'), [Parameter(Mandatory = $true)] [ValidateSet('Persistent', 'Temporary')] [string] $Type, $Cmdlet, [switch] $EnableException ) begin { $param = @{ } if ($ConfigurationName -ne 'Microsoft.PowerShell') { $param.ConfigurationName = $ConfigurationName } if ($Credential) { $param.Credential = $Credential } $killIt = $EnableException -or $ErrorActionPreference -eq 'Stop' } process { if (-not $ComputerName) { return } #region Collect Sessions $sessionHash = @{ } @($ComputerName).Where{ $_.Type -eq 'PSSession' }.ForEach{ if ($_.InputObject.State -ne 'Opened') { Stop-PSFFunction -String 'New-ManagedSession.Error.BrokenSession' -StringValues "$_" -FunctionName 'New-ManagedSession' -ModuleName 'PSFramework.NuGet' -Cmdlet $Cmdlet -EnableException $killIt return } $sessionHash["$($_.InputObject.InstanceId)"] = [PSCustomObject]@{ PSTypeName = 'PSFramework.NuGet.ManagedSession' Type = 'Extern' ComputerName = $_.InputObject.Computername Session = $_.InputObject ID = $null } } $nonSessions = @($ComputerName).Where{ $_.Type -ne 'PSSession' } if ($nonSessions) { $pssessions = New-PSSession -ComputerName $nonSessions @param -ErrorAction SilentlyContinue -ErrorVariable failedConnections foreach ($fail in $failedConnections) { Write-PSFMessage -Level Warning -String 'New-ManagedSession.Error.Connect' -StringValues $fail.TargetObject -ErrorRecord $fail -Target $fail.TargetObject -PSCmdlet $Cmdlet -EnableException $killIt } @($pssessions).ForEach{ $sessionHash["$($_.InstanceId)"] = [PSCustomObject]@{ PSTypeName = 'PSFramework.NuGet.ManagedSession' Type = $Type ComputerName = $_.Computername Session = $_ ID = $null } } } #endregion Collect Sessions if ($sessionHash.Count -eq 0) { return } #region Identify Sessions $identifiers = Invoke-Command -Session $sessionHash.Values.Session -ScriptBlock { if (-not $global:__PsfSessionId) { $global:__PsfSessionId = "$([Guid]::NewGuid())" } [PSCustomObject]@{ ID = $global:__PsfSessionId } } @($identifiers).ForEach{ $sessionHash["$($_.RunspaceId)"].ID = $_.ID } #endregion Identify Sessions $sessionHash.Values } } function Find-PSFModule { <# .SYNOPSIS Search for modules in PowerShell repositories. .DESCRIPTION Search for modules in PowerShell repositories. .PARAMETER Name Name(s) of the module(s) to look for. .PARAMETER Repository The repositories to search in. .PARAMETER Tag Tags to search by. .PARAMETER Credential Credentials to use to access repositories. .PARAMETER AllowPrerelease Whether to include modules flagged as "Prerelease" as part of the results .PARAMETER IncludeDependencies Whether to also list all required dependencies. .PARAMETER Version Version constrains for the module to search. Will use the latest version available within the limits. Examples: - "1.0.0": EXACTLY this one version - "1.0.0-1.999.999": Any version between the two limits (including the limit values) - "[1.0.0-2.0.0)": Any version greater or equal to 1.0.0 but less than 2.0.0 - "2.3.0-": Any version greater or equal to 2.3.0. Supported Syntax: <Prefix><Version><Connector><Version><Suffix> Prefix: "[" (-ge) or "(" (-gt) or nothing (-ge) Version: A valid version of 2-4 elements or nothing Connector: A "," or a "-" Suffix: "]" (-le) or ")" (-lt) or nothing (-le) .PARAMETER AllVersions Whether all versions available should be returned together .PARAMETER Type What kind of repository to search in. + All: (default) Use all, irrespective of type + V2: Only search classic repositories, as would be returned by Get-PSRepository + V3: Only search modern repositories, as would be returned by Get-PSResourceRepository .EXAMPLE PS C:\> Find-PSFModule -Name PSFramework Search all configured repositories for the module "PSFramework" #> [CmdletBinding(DefaultParameterSetName = 'default')] Param ( [Parameter(Position = 0)] [string[]] $Name, [PsfArgumentCompleter('PSFramework.NuGet.Repository')] [Parameter(Position = 1)] [string[]] $Repository, [string[]] $Tag, [PSCredential] $Credential, [switch] $AllowPrerelease, [switch] $IncludeDependencies, [Parameter(ParameterSetName = 'Version')] [string] $Version, [Parameter(ParameterSetName = 'AllVersions')] [switch] $AllVersions, [ValidateSet('All', 'V2', 'V3')] [string] $Type = 'All' ) begin { #region Functions function ConvertFrom-ModuleInfo { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] $InputObject ) process { if ($null -eq $InputObject) { return } $type = 'V2' if ($InputObject.GetType().Name -eq 'PSResourceInfo') { $type = 'V3' } [PSCustomObject]@{ PSTypeName = 'PSFramework.NuGet.ModuleInfo' Name = $InputObject.Name Version = $InputObject.Version Type = $type Repository = $InputObject.Repository Author = $InputObject.Author Commands = $InputObject.Includes.Command Object = $InputObject } } } #endregion Functions $useVersionFilter = $Version -or $AllVersions if ($Version) { $convertedVersion = Read-VersionString -Version $Version -Cmdlet $PSCmdlet $versionFilter = $convertedVersion.V3String } if ($PSCmdlet.ParameterSetName -eq 'AllVersions') { $versionFilter = '*' } $param = $PSBoundParameters | ConvertTo-PSFHashtable -Include Name, Repository, Tag, Credential, IncludeDependencies } process { #region V2 if ($script:psget.V2 -and $Type -in 'All', 'V2') { $paramClone = $param.Clone() $paramClone += $PSBoundParameters | ConvertTo-PSFHashtable -Include AllVersions, AllowPrerelease if ($Version) { if ($convertedVersion.Required) { $paramClone.RequiredVersion = $convertedVersion.Required } if ($convertedVersion.Minimum) { $paramClone.MinimumVersion = $convertedVersion.Minimum } if ($convertedVersion.Maximum) { $paramClone.MaximumVersion = $convertedVersion.Maximum } } $execute = $true if ($paramClone.Repository) { $paramClone.Repository = $paramClone.Repository | Where-Object { $_ -match '\*' -or $_ -in (Get-PSFRepository -Type V2).Name } $execute = $paramClone.Repository -as [bool] } if ($execute) { Find-Module @paramClone | ConvertFrom-ModuleInfo } } #endregion V2 #region V3 if ($script:psget.V3 -and $Type -in 'All', 'V3') { $paramClone = $param.Clone() $paramClone += $PSBoundParameters | ConvertTo-PSFHashtable -Include AllowPrerelease -Remap @{ AllowPrerelease = 'Prerelease' } if ($useVersionFilter) { $paramClone.Version = $versionFilter } $paramClone.Type = 'Module' $execute = $true if ($paramClone.Repository) { $paramClone.Repository = $paramClone.Repository | Where-Object { $_ -match '\*' -or $_ -in (Get-PSFRepository -Type V3).Name } $execute = $paramClone.Repository -as [bool] } if ($execute) { Find-PSResource @paramClone | ConvertFrom-ModuleInfo } } #endregion V3 } } function Get-PSFModuleScope { <# .SYNOPSIS Lists the registered module scopes. .DESCRIPTION Lists the registered module scopes. These are used as presets with Install-PSFModule's '-Scope' parameter. Use Register-PSFModuleScope to add additional scopes. .PARAMETER Name The name of the scope to filter by. Defaults to '*' .EXAMPLE PS C:\> Get-PSFModuleScope Lists all registered module scopes. #> [CmdletBinding()] param ( [PsfArgumentCompleter('PSFramework.NuGet.ModuleScope')] [string] $Name = '*' ) process { ($script:moduleScopes.Values) | Where-Object Name -Like $Name } } function Install-PSFModule { <# .SYNOPSIS Installs PowerShell modules from a PowerShell repository. .DESCRIPTION Installs PowerShell modules from a PowerShell repository. They can be installed locally or to remote computers. .PARAMETER Name Name of the module to install. .PARAMETER Version Version constrains for the module to install. Will use the latest version available within the limits. Examples: - "1.0.0": EXACTLY this one version - "1.0.0-1.999.999": Any version between the two limits (including the limit values) - "[1.0.0-2.0.0)": Any version greater or equal to 1.0.0 but less than 2.0.0 - "2.3.0-": Any version greater or equal to 2.3.0. Supported Syntax: <Prefix><Version><Connector><Version><Suffix> Prefix: "[" (-ge) or "(" (-gt) or nothing (-ge) Version: A valid version of 2-4 elements or nothing Connector: A "," or a "-" Suffix: "]" (-le) or ")" (-lt) or nothing (-le) .PARAMETER Prerelease Whether to include prerelease versions in the potential results. .PARAMETER Scope Where to install the module to. Use Register-PSFModuleScope to add additional scopes to the list of options. Scopes can either use a static path or dynamic code to calculate - per computer - where to install the module. If not specified, it will default to: - CurrentUser - for local installation (irrespective of whether the console is run "As Administrator" or not.) - AllUsers - for remote installations when using the -ComputerName parameter. .PARAMETER ComputerName The computers to deploy the modules to. Accepts both names or established PSRemoting sessions. All transfer happens via PowerShell Remoting. If you provide names, by default this module will connect to the "Microsoft.PowerShell" configuration name. To change that name, use the 'PSFramework.NuGet.Remoting.DefaultConfiguration' configuration setting. .PARAMETER SkipDependency Do not include any dependencies. Works with PowerShellGet V1/V2 as well. .PARAMETER AuthenticodeCheck Whether modules must be correctly signed by a trusted source. Uses "Get-PSFModuleSignature" for validation. Defaults to: $false Default can be configured under the 'PSFramework.NuGet.Install.AuthenticodeSignature.Check' setting. .PARAMETER Force Redeploy a module that already exists in the target path. By default it will skip modules that do already exist in the target path. .PARAMETER Credential The credentials to use for connecting to the Repository (NOT the remote computers). .PARAMETER RemotingCredential The credentials to use for connecting to remote computers we want to deploy modules to via remoting. These will NOT be used for repository access. .PARAMETER ThrottleLimit Up to how many computers to deploy the modules to in parallel. Defaults to: 5 Default can be configured under the 'PSFramework.NuGet.Remoting.Throttling' setting. .PARAMETER Repository Repositories to install from. Respects the priority order of repositories. See Get-PSFRepository for available repositories (and their priority). Lower numbers are installed from first. .PARAMETER TrustRepository Whether we should trust the repository installed from and NOT ask users for confirmation. .PARAMETER Type What type of repository to download from. V2 uses classic Save-Module. V3 uses Save-PSResource. Availability depends on the installed PSGet module versions and configured repositories. Use Install-PSFPowerShellGet to deploy the latest versions of the package modules. Only the version on the local computer matters, even when deploying to remote computers. .PARAMETER InputObject The module to install. Takes the output of Get-Module, Find-Module, Find-PSResource and Find-PSFModule, to specify the exact version and name of the module. Even when providing a locally available version, the module will still be downloaded from the repositories chosen. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Install-PSFModule -Name EntraAuth Installs the EntraAuth module locally for the CurrentUser. .EXAMPLE PS C:\> Install-PSFModule -Name ADMF -ComputerName AdminHost1, AdminHost2 Installs the ADMF module (and all of its dependencies) for All Users on the computers AdminHost1 and AdminHost2 .EXAMPLE PS C:\> Install-PSFModule -Name string, PoshRSJob -ComputerName $sshSessions -Scope ScriptModules Installs the String and PoshRSJob module to all computers with an established session in $sshSessions. The modules will be installed to the "ScriptModules" scope - something that must have first been registered using the Register-PSFModuleScope command. #> [CmdletBinding(PositionalBinding = $false, DefaultParameterSetName = 'ByName', SupportsShouldProcess = $true)] Param ( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'ByName')] [string[]] $Name, [Parameter(ParameterSetName = 'ByName')] [string] $Version, [Parameter(ParameterSetName = 'ByName')] [switch] $Prerelease, [PsfValidateSet(TabCompletion = 'PSFramework.NuGet.ModuleScope')] [PsfArgumentCompleter('PSFramework.NuGet.ModuleScope')] [string] $Scope, [PSFComputer[]] $ComputerName, [switch] $SkipDependency, [switch] $AuthenticodeCheck = (Get-PSFConfigValue -FullName 'PSFramework.NuGet.Install.AuthenticodeSignature.Check'), [switch] $Force, [PSCredential] $Credential, [PSCredential] $RemotingCredential, [ValidateRange(1, [int]::MaxValue)] [int] $ThrottleLimit = (Get-PSFConfigValue -FullName 'PSFramework.NuGet.Remoting.Throttling'), [PsfArgumentCompleter('PSFramework.NuGet.Repository')] [string[]] $Repository = ((Get-PSFrepository).Name | Sort-Object -Unique), [switch] $TrustRepository, [ValidateSet('All', 'V2', 'V3')] [string] $Type = 'All', [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ByObject')] [object[]] $InputObject ) begin { $killIt = $ErrorActionPreference -eq 'Stop' $cleanedUp = $false # Resolution only happens to early detect impossible parameterization. Will be called again in Save-PSFModule. $null = Resolve-Repository -Name $Repository -Type $Type -Cmdlet $PSCmdlet # Terminates if no repositories found $managedSessions = New-ManagedSession -ComputerName $ComputerName -Credential $RemotingCredential -Cmdlet $PSCmdlet -Type Temporary if ($ComputerName -and -not $managedSessions) { Stop-PSFFunction -String 'Install-PSFModule.Error.NoComputerValid' -StringValues ($ComputerName -join ', ') -EnableException $killIt -Cmdlet $PSCmdlet return } $resolvedPaths = Resolve-ModuleScopePath -Scope $Scope -ManagedSession $managedSessions -TargetHandling Any -PathHandling Any -Cmdlet $PSCmdlet # Errors for bad paths, terminates if no path # Used to declare variable in the current scope, to prevent variable lookup snafus when det $command = $null $saveParam = $PSBoundParameters | ConvertTo-PSFHashtable -ReferenceCommand Save-PSFModule -Exclude ComputerName, RemotingCredential $saveParam.Path = '<placeholder>' # Meet Parameterset requirements } process { if (Test-PSFFunctionInterrupt) { return } $stopParam = @{ StringValues = $Name -join ', '} if ($InputObject) { $names = foreach ($item in $InputObject) { if ($item -is [string]) { $item } elseif ($item.ModuleName) { $item.ModuleName } } $stopParam = @{ StringValues = $names -join ', '} } #region Start Nested Save-PSFModule if (-not $command) { $command = { Save-PSFModule @saveParam -PathInternal $resolvedPaths -Cmdlet $PSCmdlet -ErrorAction $ErrorActionPreference }.GetSteppablePipeline() try { $command.Begin((-not $Name)) } catch { if (-not $cleanedUp -and $managedSessions) { $managedSessions | Where-Object Type -EQ 'Temporary' | ForEach-Object Session | Remove-PSSession } $cleanedUp = $true Stop-PSFFunction -String 'Install-PSFModule.Error.Setup' @stopParam -ErrorRecord $_ -EnableException $killIt -Cmdlet $PSCmdlet return } } #endregion Start Nested Save-PSFModule #region Execute Process try { if ($Name) { $command.Process() } else { $command.Process($InputObject) } } catch { if (-not $cleanedUp -and $managedSessions) { $managedSessions | Where-Object Type -EQ 'Temporary' | ForEach-Object Session | Remove-PSSession } $cleanedUp = $true Stop-PSFFunction -String 'Install-PSFModule.Error.Installation' @stopParam -ErrorRecord $_ -EnableException $killIt -Cmdlet $PSCmdlet return } #endregion Execute Process } end { if (-not $cleanedUp -and $managedSessions) { $managedSessions | Where-Object Type -EQ 'Temporary' | ForEach-Object Session | Remove-PSSession } if (Test-PSFFunctionInterrupt) { return } $null = $command.End() } } function Publish-PSFModule { <# .SYNOPSIS Publish a PowerShell module. .DESCRIPTION Publish a PowerShell module. Allows publishing to either nuget repositories or as .nupkg file to disk. .PARAMETER Path The path to the module to publish. Either the directory or the psd1 file. .PARAMETER Repository The repository to publish to. .PARAMETER Type What kind of repository to publish to. - All (default): All types of repositories are eligible. - V2: Only repositories from the old PowerShellGet are eligible. - V3: Only repositories from the new PSResourceGet are eligible. If multiple repositories of the same name are found, the one at the highest version among them is chosen. .PARAMETER Credential The credentials to use to authenticate to the Nuget service. Mostly used for internal repository servers. .PARAMETER ApiKey The ApiKey to use to authenticate to the Nuget service. Mostly used for publishing to the PSGallery. .PARAMETER SkipDependenciesCheck Do not validate dependencies or the module manifest. This removes the need to have the dependencies installed when publishing using PSGet v2 .PARAMETER DestinationPath Rather than publish to a repository, place the finished .nupgk file in this path. Use when doing the final publish step outside of PowerShell code. .PARAMETER Tags Tags to add to the module. .PARAMETER LicenseUri The LicenseUri for the module. Mostly used as metadata for the PSGallery. .PARAMETER IconUri The Icon Uri for the module. Mostly used as metadata for the PSGallery. .PARAMETER ProjectUri The Link to the project - frequently the Github repository hosting your module. Mostly used as metadata for the PSGallery. .PARAMETER ReleaseNotes The release notes of your module - or at least the link to them. Mostly used as metadata for the PSGallery. .PARAMETER Prerelease The prerelease tag to include. This flags the module as "Prerelease", hiding it from regular Find-PSFModule / Install-PSFModule use. Use to provide test versions that only affect those in the know. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Publish-PSFModule -Path C:\code\MyModule -Repository PSGallery -ApiKey $key Publishes the module "MyModule" to the PSGallery. .EXAMPLE PS C:\> Publish-PSFModule -Path C:\code\MyModule -Repository AzDevOps -Credential $cred -SkipDependenciesCheck Publishes the module "MyModule" to the repository "AzDevOps". It will not check for any dependencies and use the credentials stored in $cred to authenticate the request. .EXAMPLE PS C:\> Publish-PSFModule -Path C:\code\MyModule -Repository AzDevOps -SkipDependenciesCheck Publishes the module "MyModule" to the repository "AzDevOps". It will not check for any dependencies. If there are any credentials assigned to the repository (Use Set-PSFRepository to assign), those will be used to authenticate the request. Otherwise it will try default windows authentication (Which may well work, if the repository is hosted by an on-prem Azure DevOps Server in an Active Directory environment). .EXAMPLE PS C:\> Publish-PSFModule -Path C:\code\MyModule -DestinationPath \\contoso.com\it\packages Wraps the module "MyModule" into a .nupkg file and copies that to '\\contoso.com\it\packages' #> [CmdletBinding(DefaultParameterSetName = 'ToRepository', SupportsShouldProcess = $true)] Param ( [Parameter(Mandatory = $true)] [PsfPath] $Path, [Parameter(Mandatory = $true, ParameterSetName = 'ToRepository')] [PsfValidateSet(TabCompletion = 'PSFramework.NuGet.Repository')] [PsfArgumentCompleter('PSFramework.NuGet.Repository')] [string[]] $Repository, [Parameter(ParameterSetName = 'ToRepository')] [ValidateSet('All', 'V2', 'V3')] [string] $Type = 'All', [Parameter(ParameterSetName = 'ToRepository')] [PSCredential] $Credential, [Parameter(ParameterSetName = 'ToRepository')] [string] $ApiKey, [Parameter(ParameterSetName = 'ToRepository')] [switch] $SkipDependenciesCheck, [Parameter(Mandatory = $true, ParameterSetName = 'ToPath')] [PsfDirectory] $DestinationPath, [string[]] $Tags, [string] $LicenseUri, [string] $IconUri, [string] $ProjectUri, [string] $ReleaseNotes, [string] $Prerelease ) begin { #region Setup $killIt = $ErrorActionPreference -eq 'Stop' if ($Repository) { # Resolve Repositories Search-PSFPowerShellGet $repositories = Resolve-Repository -Name $Repository -Type $Type -Cmdlet $PSCmdlet | Group-Object Name | ForEach-Object { @($_.Group | Sort-Object Type -Descending)[0] } } # Create Temp Directories $workingDirectory = New-PSFTempDirectory -ModuleName PSFramework.NuGet -Name Publish.Work $commonPublish = @{ Cmdlet = $PSCmdlet Continue = $true ContinueLabel = 'repo' } if ($ApiKey) { $commonPublish.ApiKey = $ApiKey } if ($Credential) { $commonPublish.Credential = $Credential } if ($SkipDependenciesCheck) { $commonPublish.SkipDependenciesCheck = $SkipDependenciesCheck } #endregion Setup } process { try { foreach ($sourceModule in $Path) { # Update Metadata per Parameter $moduleData = Copy-Module -Path $sourceModule -Destination $workingDirectory -Cmdlet $PSCmdlet -Continue Update-PSFModuleManifest -Path $moduleData.ManifestPath -Tags $Tags -LicenseUri $LicenseUri -IconUri $IconUri -ProjectUri $ProjectUri -ReleaseNotes $ReleaseNotes -Prerelease $Prerelease -Cmdlet $PSCmdlet -Continue # Case 1: Publish to Destination Path if ($DestinationPath) { Publish-ModuleToPath -Module $moduleData -Path $DestinationPath -Cmdlet $PSCmdlet continue } # Case 2: Publish to Repository :repo foreach ($repositoryObject in $repositories) { switch ($repositoryObject.Type) { V2 { Publish-ModuleV2 @commonPublish -Module $moduleData -Repository $repositoryObject } V3 { Publish-ModuleV3 @commonPublish -Module $moduleData -Repository $repositoryObject } default { Stop-PSFFunction -String 'Publish-PSFModule.Error.UnexpectedRepositoryType' -StringValues $repositoryObject.Name, $repositoryObject.Type -Continue -Cmdlet $PSCmdlet -EnableException $killIt } } } } } finally { # Cleanup Temp Directory Remove-PSFTempItem -ModuleName PSFramework.NuGet -Name Publish.* } } } function Register-PSFModuleScope { <# .SYNOPSIS Provide a scope you can install modules to. .DESCRIPTION Provide a scope you can install modules to. Those are used by Install-PFModule to pick what path to install to. .PARAMETER Name Name of the scope. Must be unique, otherwise it will overwrite an existing scope. .PARAMETER Path Path where modules should be stored. .PARAMETER Mode Specifying a mode will add the path provided to the PSModulePath variable for this session. - Append: Adds the path as the last option, making it the last location PowerShell will look for modules. - Prepend: Adds the path as the first option, making it take precedence over all other module paths. .PARAMETER ScriptBlock Logic determining, where modules should be stored. This scriptblock will not receive any parameters. Used to dynamically determine the path, may be executed against remote computers, when installing to remote computers. Keep in mind that dependencies may not be available. .PARAMETER Description A description to add to the module scope registered. Purely for documentation purposes. .PARAMETER Persist Remember the configured scope. For the current user, even when starting a new console, this scope will still exist. This will NOT remember the "Mode" parameter - configure your PSModulePath environment evariable separately, if desired. Not compatible with a ScriptBlock-based setting. .EXAMPLE PS C:\> Register-PSFModuleScope -Name WinPSAllUsers -Path 'C:\Program Files\WindowsPowerShell\Modules' Registers the module-scope "WinPSAllusers" with the default path for Modules in Windows PowerShell. This would allow installing modules for Windows PowerShell from PowerShell 7. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true, ParameterSetName = 'Path')] [string] $Path, [Parameter(ParameterSetName = 'Path')] [ValidateSet('Append', 'Prepend')] [string] $Mode, [Parameter(Mandatory = $true, ParameterSetName = 'Scriptblock')] [PsfValidateLanguageMode('FullLanguage')] [scriptblock] $ScriptBlock, [string] $Description, [Parameter(ParameterSetName = 'Path')] [switch] $Persist ) process { $typeMap = @{ $true = 'Dynamic' $false = 'Static' } $script:moduleScopes[$Name] = [PSCustomObject]@{ PSTypeName = 'PSFramework.NuGet.ModulePath' Name = $Name Type = $typeMap[($ScriptBlock -as [bool])] Path = $Path ScriptBlock = $ScriptBlock Description = $Description } if ($Persist) { Set-PSFConfig -Module 'PSFramework.NuGet' -Name "ModuleScopes.$Name.Path" -Value $Path -PassThru | Register-PSFConfig if ($Description) { Set-PSFConfig -Module 'PSFramework.NuGet' -Name "ModuleScopes.$Name.Description" -Value $Description -PassThru | Register-PSFConfig } } if (-not $Mode) { return } $envPaths = $env:PSModulePath -split ';' if ($Path -in $envPaths) { return } switch ($Mode) { 'Append' { $envPaths = @($envPaths) + $Path } 'Prepend' { $envPaths = @($Path) + $envPaths } } $env:PSModulePath = $envPaths -join ';' } } function Save-PSFModule { <# .SYNOPSIS Downloads modules to a specified path. .DESCRIPTION Downloads modules to a specified path. Supports flexible repository resolution, modern versioning and deployment to remote systems. When specifying remote computers, all file transfer is performed via PSRemoting only. ErrorAction is only honored for local deployments. .PARAMETER Name Name of the module to download. .PARAMETER Version Version constrains for the module to save. Will use the latest version available within the limits. Examples: - "1.0.0": EXACTLY this one version - "1.0.0-1.999.999": Any version between the two limits (including the limit values) - "[1.0.0-2.0.0)": Any version greater or equal to 1.0.0 but less than 2.0.0 - "2.3.0-": Any version greater or equal to 2.3.0. Supported Syntax: <Prefix><Version><Connector><Version><Suffix> Prefix: "[" (-ge) or "(" (-gt) or nothing (-ge) Version: A valid version of 2-4 elements or nothing Connector: A "," or a "-" Suffix: "]" (-le) or ")" (-lt) or nothing (-le) .PARAMETER Prerelease Whether to include prerelease versions in the potential results. .PARAMETER Path Where to store the modules. If used together with the -ComputerName parameter, this is considered a local path from within the context of a remoting session to that computer, If you want to deploy a module to "\\server1\C$\Scripts\Modules" provide "C:\Scripts\Modules" as -Path, with "-ComputerName server1". Unless you actually WANT to deploy without remoting but with SMB (in which case do not provide a -ComputerName) See examples for less confusion :) .PARAMETER ComputerName The computers to deploy the modules to. Accepts both names or established PSRemoting sessions. The -Path parameter will be considered as a local path from within a remoting session. If you want to deploy a module to "\\ComputerName\C$\Scripts\Modules" provide "C:\Scripts\Modules" as -Path. See examples for less confusion :) If you provide names, by default this module will connect to the "Microsoft.PowerShell" configuration name. To change that name, use the 'PSFramework.NuGet.Remoting.DefaultConfiguration' configuration setting. .PARAMETER SkipDependency Do not include any dependencies. Works with PowerShellGet V1/V2 as well. .PARAMETER AuthenticodeCheck Whether modules must be correctly signed by a trusted source. Uses "Get-PSFModuleSignature" for validation. Defaults to: $false Default can be configured under the 'PSFramework.NuGet.Install.AuthenticodeSignature.Check' setting. .PARAMETER Force Redeploy a module that already exists in the target path. By default it will skip modules that do already exist in the target path. .PARAMETER Credential The credentials to use for connecting to the Repository (NOT the remote computers). .PARAMETER RemotingCredential The credentials to use for connecting to remote computers we want to deploy modules to via remoting. These will NOT be used for repository access. .PARAMETER ThrottleLimit Up to how many computers to deploy the modules to in parallel. Defaults to: 5 Default can be configured under the 'PSFramework.NuGet.Remoting.Throttling' setting. .PARAMETER Repository Repositories to install from. Respects the priority order of repositories. See Get-PSFRepository for available repositories (and their priority). Lower numbers are installed from first. .PARAMETER TrustRepository Whether we should trust the repository installed from and NOT ask users for confirmation. .PARAMETER Type What type of repository to download from. V2 uses classic Save-Module. V3 uses Save-PSResource. Availability depends on the installed PSGet module versions and configured repositories. Use Install-PSFPowerShellGet to deploy the latest versions of the package modules. Only the version on the local computer matters, even when deploying to remote computers. .PARAMETER InputObject The module to install. Takes the output of Get-Module, Find-Module, Find-PSResource and Find-PSFModule, to specify the exact version and name of the module. Even when providing a locally available version, the module will still be downloaded from the repositories chosen. .PARAMETER PathInternal For internal use only. Used to pass scope-based path resolution from Install-PSFModule into Save-PSFModule. .PARAMETER Cmdlet The $PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this command from the user. Should be used when trying to hide Save-PSFModule - e.g. when called from Install-PSFModule. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Save-PSFModule EntraAuth -Path C:\temp Downloads the module "EntraAuth" to the local C:\temp path. .EXAMPLE PS C:\> Save-PSFModule -Name EntraAuth -Path 'C:\Program Files\WindowsPowerShell\Modules' Downloads the latest version of EntraAuth and places it where both PowerShell versions look for modules. .EXAMPLE PS C:\> Save-PSFModule -Name EntraAuth -Path 'C:\Program Files\WindowsPowerShell\Modules' -Force Downloads the latest version of EntraAuth and places it where both PowerShell versions look for modules. If the module has already been installed previously in the same version, it will replace the old install with the newly downloaded one. .EXAMPLE PS C:\> Save-PSFModule -Name EntraAuth -Path '\\server1\C$\Program Files\WindowsPowerShell\Modules' Downloads the latest version of EntraAuth and places it where both PowerShell versions look for modules ... on computer "server1". File transfer happens via SMB - lets hope that works. .EXAMPLE PS C:\> Save-PSFModule -Name EntraAuth -Path 'C:\Program Files\WindowsPowerShell\Modules' -ComputerName server1 Downloads the latest version of EntraAuth and places it where both PowerShell versions look for modules ... on computer "server1". File transfer happens via PSRemoting, assuming our account has local admin rights on the remote computer. .EXAMPLE PS C:\> Save-PSFModule -Name EntraAuth -Path 'C:\Program Files\WindowsPowerShell\Modules' -ComputerName server1 -RemotingCredential $cred Downloads the latest version of EntraAuth and places it where both PowerShell versions look for modules ... on computer "server1". File transfer happens via PSRemoting, assuming the account in $cred has local admin rights on the remote computer. .EXAMPLE PS C:\> Save-PSFModule -Name EntraAuth -Path '/usr/local/share/powershell/Modules' -ComputerName $sessions Downloads the latest version of EntraAuth and places it where both PowerShell versions look for modules on linux distributions ... on the computers previously connected. On PowerShell 7, these can be remoting sessions established via SSH. File transfer happens via PSRemoting. #> [CmdletBinding(PositionalBinding = $false, DefaultParameterSetName = 'ByName', SupportsShouldProcess = $true)] Param ( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'ByName')] [string[]] $Name, [Parameter(ParameterSetName = 'ByName')] [string] $Version, [Parameter(ParameterSetName = 'ByName')] [switch] $Prerelease, [Parameter(Mandatory = $true, Position = 1)] [string] $Path, [PSFComputer[]] $ComputerName, [switch] $SkipDependency, [switch] $AuthenticodeCheck = (Get-PSFConfigValue -FullName 'PSFramework.NuGet.Install.AuthenticodeSignature.Check'), [switch] $Force, [PSCredential] $Credential, [PSCredential] $RemotingCredential, [ValidateRange(1, [int]::MaxValue)] [int] $ThrottleLimit = (Get-PSFConfigValue -FullName 'PSFramework.NuGet.Remoting.Throttling'), [PsfArgumentCompleter('PSFramework.NuGet.Repository')] [string[]] $Repository = ((Get-PSFrepository).Name | Sort-Object -Unique), [switch] $TrustRepository, [ValidateSet('All', 'V2', 'V3')] [string] $Type = 'All', [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ByObject')] [object[]] $InputObject, [Parameter(DontShow = $true)] $PathInternal, [Parameter(DontShow = $true)] $Cmdlet = $PSCmdlet ) begin { $repositories = Resolve-Repository -Name $Repository -Type $Type -Cmdlet $Cmdlet # Terminates if no repositories found if ($PathInternal) { $resolvedPaths = $PathInternal $shouldProcessMessage = "Saving modules to $(@($PathInternal)[0].Scope)" } else { $shouldProcessMessage = "Saving modules to $Path" $managedSessions = New-ManagedSession -ComputerName $ComputerName -Credential $RemotingCredential -Cmdlet $Cmdlet -Type Temporary if ($ComputerName -and -not $managedSessions) { Stop-PSFFunction -String 'Save-PSFModule.Error.NoComputerValid' -StringValues ($ComputerName -join ', ') -EnableException ($ErrorActionPreference -eq 'Stop') -Cmdlet $Cmdlet return } $resolvedPaths = Resolve-RemotePath -Path $Path -ComputerName $managedSessions.Session -ManagedSession $managedSessions -TargetHandling Any -Cmdlet $Cmdlet # Errors for bad paths, terminates if no path } $tempDirectory = New-PSFTempDirectory -Name Staging -ModuleName PSFramework.NuGet } process { if (Test-PSFFunctionInterrupt) { return } try { $installData = switch ($PSCmdlet.ParameterSetName) { ByObject { Resolve-ModuleTarget -InputObject $InputObject -Cmdlet $Cmdlet } ByName { Resolve-ModuleTarget -Name $Name -Version $Version -Prerelease:$Prerelease -Cmdlet $Cmdlet } } if (-not $Cmdlet.ShouldProcess(($installData.TargetName -join ', '), $shouldProcessMessage)) { return } Save-StagingModule -InstallData $installData -Path $tempDirectory -Repositories $repositories -Cmdlet $Cmdlet -Credential $Credential -SkipDependency:$SkipDependency -AuthenticodeCheck:$AuthenticodeCheck -TrustRepository:$TrustRepository Publish-StagingModule -Path $tempDirectory -TargetPath $resolvedPaths -Force:$Force -Cmdlet $Cmdlet -ThrottleLimit $ThrottleLimit } finally { # Cleanup Managed sessions only if created locally. With -PathInternal, managed sessions are managed by the caller. if (-not $PathInternal) { $managedSessions | Where-Object Type -EQ 'Temporary' | ForEach-Object Session | Remove-PSSession } Remove-PSFTempItem -Name Staging -ModuleName PSFramework.NuGet } } } function Update-PSFModuleManifest { <# .SYNOPSIS Modifies an existing module manifest. .DESCRIPTION Modifies an existing module manifest. The manifest in question must have a ModuleVersion and a RootModule entry present. .PARAMETER Path Path to the manifest file to modify. .PARAMETER Guid The guid of the module. Usually has no effect. .PARAMETER Author The author that wrote the module. .PARAMETER CompanyName The company that owns the module (if any). .PARAMETER Copyright The Copyright short-string. Example: 'Copyright (c) 2025 Contoso ltd.' .PARAMETER RootModule The root file of the module. For script based modules, that would be the psm1 file. For binary modules the root .dll file. Paths relative to the module root path. Examples: - MyModule.psm1 - bin\MyModule.dll .PARAMETER ModuleVersion The version of the module. Most package services reject module uploads with versions that already exist in the service. .PARAMETER Description The description the module should include. A description is required for successful module uploads. Most package services use the description field to explain the module in their module lists. .PARAMETER ProcessorArchitecture The architecture thhe module requires. Do not provide unless you actually use hardware features for a specific architecture set. .PARAMETER CompatiblePSEditions What PowerShell editions this module is compatible with. - Desktop: Windows PowerShell - Core: PowerShell 6+ Has little effect, other than documentation. When set to "Desktop"-only, loading the module into a core session will lead to it being imported into an implicit remoting session instead. .PARAMETER PowerShellVersion The minimum version of PowerShell to require for your module. There is no option to define a maximum version. To declare "this module only runs on Windows PowerShell" use -CompatiblePSEditions instead. .PARAMETER ClrVersion What minimum version of the Common Language Runtime you require. If this has you wondering "What is the Common Language Runtime" you do not need to specify this parameter. If it does not, you still probably won't need it. .PARAMETER DotNetFrameworkVersion What version of the .NET Framework to require as a minimum. A pointless requirement compared to requiring a minimum version of PowerShell. Usually not necessary. .PARAMETER PowerShellHostName What PowerShell host your module requires. This can enforce your module only being loaded into a specific hosting process, such as "This only works in the Powershell ISE". Use this to read the name you need to provide here: $host.Name Usually only useful for modules that act as PlugIn for the PowerShell ISE. Example values: - "ConsoleHost" - "Windows PowerShell ISE Host" .PARAMETER PowerShellHostVersion The minimum version of the host you require. Use this to read the current version of a host: $host.Version -as [string] .PARAMETER RequiredModules What modules your module requires to run. Taking a dependency like this means, that when someone installs your module, they also automatically download all the dependencies without needing additional input. Can either take a string or a hashtable with the default module definitions (see below): Examples: - "PSFramework" # any version of the PSFramework - @{ ModuleName = "PSFramework"; ModuleVersion = "1.2.346" } # The module "PSFramework" with AT LEAST version 1.2.346 - @{ ModuleName = "PSFramework"; RequiredVersion = "1.2.346" } # The module "PSFramework" with EXACTLY version 1.2.346 Generally it is recommended to NOT use "RequiredVersion" unless as an emergency stopgap while you try to fix a compatibility issue. Using "RequiredVersion" significantly raises the risk of conflict between modules taking a dependency on the same module. It also prevents updating the dependency independently, which your users may need to do (e.g. critical security patch) without waiting on you. Generally, it is recommended to be cautious about what module you take a dependency on, when you do not control the dependency. For non-public modules, you can minimize the risk of breaking things by having an internal repository and testing new versions of modules you take a dependency on, before introducing them into your environment. .PARAMETER TypesToProcess Type extension XML to load when importing the module. These allow you to add methods and properties to existing objects, without calling Add-Member on each of them. For more details, see: https://learn.microsoft.com/en-us/powershell/scripting/developer/cmdlet/extending-output-objects .PARAMETER FormatsToProcess Format definition XML to load when importing the module. These allow you to determine how objects your commands return should be displayed. For more details, see: https://learn.microsoft.com/en-us/powershell/scripting/developer/format/formatting-file-overview You can use the module PSModuleDevelopment and its "New-PSMDFormatTableDefinition" command to auto-generate the XML for your objects. .PARAMETER ScriptsToProcess Any scripts to run before your module imports. Any failure here will stop the module import. This should NOT be used to load nested files of your module project! Generally, this parameter is not needed, instead place the import sequence in your psm1 file. For an example layout that does that, check out the PSModuleDevelopment module's default module template: Invoke-PSMDTemplate MiniModule .PARAMETER RequiredAssemblies Assemblies you require. These DLL files will be loaded as part of your import sequence. Failure to do so (e.g. file not found, or dependency not found) will cause your module import to fail. Can be the name of an assembly from GAC or the relative path to the file within your module's folder layout. .PARAMETER FileList List of files your module contains. Documentation only, has no effect. .PARAMETER ModuleList The modules included in your module. Generally not needed. .PARAMETER FunctionsToExport What functions your module makes available. Functions are PowerShell-native commands written in script code and usually the main point of writing a module. You should not export '*', as that makes it hard for PowerShell to know what commands your module exposes. This will lead to issues with automatically importing it when just running a command by name from your module. .PARAMETER AliasesToExport What aliases your module makes available. Aliases not listed here will not lead to automatic module import if needed. Do not export '*'. .PARAMETER VariablesToExport Not really used, no point in doing so. .PARAMETER CmdletsToExport Cmdlets your module makes available. Cmdlets are PowerShell-native commands written in C#* and compiled into a .DLL This is usually only needed when writing a binary module in C# or a hybrid module with a significant portion of compiled code. *Usually. Technically, other languages are also possible, but they all must be compiled into an assembly. .PARAMETER DscResourcesToExport What DSC resources your module provides. If you are wondering what DSC (Desired State Configuration) is, you are probably missing out, but this parameter is not (yet) for you. .PARAMETER Tags Tags to include in your module. Modules in nuget repositories can be searched by their tag. .PARAMETER LicenseUri The link to the license your module uses. This will be shown in the PSGallery and is usually a good idea to include in your module manifest. .PARAMETER IconUri The link to the icon to display with your module. Only affects how the module is displayed in the PSGallery. .PARAMETER ProjectUri The link to your project. This will be shown in the PSGallery and is usually a good idea to include in your module manifest. .PARAMETER ReleaseNotes What changed in the latest version of your module? Either provide the change text or the link to where your changes are being tracked. .PARAMETER Prerelease The prerelease tag, such as "Alpha" or "RC1". Including this will hide your module in most repositories by flagging it as a prerelease version. Only uses who include "-AllowPrerelease" in their Install-PSFModule call will install this version. Adding this is a good way to provide a test preview power users can test, without affecting the broader audience right away. .PARAMETER ExternalModuleDependencies Modules your own module requires, that are not distributed via powershell repositories. For example, if your module requires the "ActiveDirectory" module, this is the place to specify it. Generally only needed for modules not distribtued via gallery, such as RSAT tools to manage windows features or vendor modules that require you to deploy the module via installer. Uses the same module notation syntax as "-RequiredModules". .PARAMETER HelpInfoUri Where to get more information about your module. .PARAMETER DefaultCommandPrefix Default prefix to include with commands in your module. Generally not recommended for use. .PARAMETER NestedModules DO NOT USE. DON'T. IT'S A MISTAKE. CEASE AND DESIST! Nested modules allow you to include a module inside of your own module, which will be invisible to outsiders. Compared to traditional dependencies via RequiredModules this has the advantage of you getting EXACTLY the version you are expecting. Theoretically, this sounds good - it gives you the full control over what module version, zero risk of accidental breakage when the original author updates the module. Right? Not really. The key issue is, that most modules cannot coexist in different versions of the same module in the same process or at least runspace. The module you include as a NestedModule can - and WILL - still conflict with other modules requiring the same dependency. So you still get all the same version conflicts a RequiredModule with "RequiredVersion" defined has, but with horribly worse error message to the user (who is not aware of a potential conflict AND IS NOT INFORMED OF A CONFLICT!!!). By whatever is holy, sacred or venerable to you, please do not use NestedModules. .PARAMETER PassThru Rather than modifying the file, return the new manifest text as string. .PARAMETER Cmdlet The PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this command from the user. .PARAMETER Continue In case of error, when not specifying ErrorAction as stop, this command will call the continue statement. By default, it will just end with a warning. This parameter makes it easier to integrate in some flow control scenarios but is mostly intended for internal use only. .EXAMPLE PS C:\> Update-PSFModuleManifest -Path .\MyModule\MyModule.psd1 -FunctionsToExport $functions.BaseName Updates MyModule.psd1 to export the functions stored in $functions. This will _replace_ the existing entries. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')] [string] $Path, [guid] $Guid, [string] $Author, [string] $CompanyName, [string] $Copyright, [string] $RootModule, [version] $ModuleVersion, [string] $Description, [ValidateSet('', 'X86', 'Amd64')] [string] $ProcessorArchitecture, [ValidateSet('Core', 'Desktop')] [string[]] $CompatiblePSEditions, [version] $PowerShellVersion, [version] $ClrVersion, [version] $DotNetFrameworkVersion, [string] $PowerShellHostName, [version] $PowerShellHostVersion, [object[]] $RequiredModules, [string[]] $TypesToProcess, [string[]] $FormatsToProcess, [string[]] $ScriptsToProcess, [string[]] $RequiredAssemblies, [string[]] $FileList, [object[]] $ModuleList, [string[]] $FunctionsToExport, [string[]] $AliasesToExport, [string[]] $VariablesToExport, [string[]] $CmdletsToExport, [string[]] $DscResourcesToExport, [string[]] $Tags, [string] $LicenseUri, [string] $IconUri, [string] $ProjectUri, [string] $ReleaseNotes, [string] $Prerelease, [object[]] $ExternalModuleDependencies, [uri] $HelpInfoUri, [string] $DefaultCommandPrefix, [object[]] $NestedModules, [switch] $PassThru, [Parameter(DontShow = $true)] $Cmdlet = $PSCmdlet, [Parameter(DontShow = $true)] [switch] $Continue ) begin { #region Utility Functions function ConvertTo-ModuleRequirement { [OutputType([System.Collections.Specialized.OrderedDictionary])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] [AllowEmptyCollection()] [AllowNull()] [AllowEmptyString()] $InputObject, [bool] $EnableException, $Cmdlet ) process { foreach ($item in $InputObject) { if (-not $item) { continue } if ($item -is [string]) { $item; continue } if (-not $item.ModuleName) { Stop-PSFFunction -String 'Update-PSFModuleManifest.Error.InvalidModuleReference' -StringValues $item -Target $item -EnableException $EnableException -Cmdlet $Cmdlet -Category InvalidArgument -Continue } $data = [ordered]@{ ModuleName = $item.ModuleName } if ($item.RequiredVersion) { $data.RequiredVersion = '{0}' -f $item.RequiredVersion } elseif ($item.ModuleVersion) { $data.ModuleVersion = '{0}' -f $item.ModuleVersion } $data } } } function Update-ManifestProperty { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [OutputType([System.Management.Automation.Language.ScriptBlockAst])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Management.Automation.Language.Ast] $Ast, [Parameter(Mandatory = $true)] [string] $Property, [Parameter(Mandatory = $true)] $Value, [Parameter(Mandatory = $true)] [ValidateSet('String', 'StringArray', 'HashtableArray')] [string] $Type ) $mainHash = $Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.HashtableAst] -and $args[0].KeyValuePairs.Item1.Value -contains 'RootModule' -and $args[0].KeyValuePairs.Item1.Value -Contains 'ModuleVersion' }, $true) $entry = $mainhash.KeyValuePairs | Where-Object { $_.Item1.Value -eq $Property } $stringValue = switch ($Type) { 'String' { "$Value" | ConvertTo-Psd1 } 'StringArray' { , @(, @($Value)) | ConvertTo-Psd1 } 'HashtableArray' { , @(, @($Value)) | ConvertTo-Psd1 } } $format = '{0}' #region Case: Key Already Exists if ($entry) { $start = $entry.Item2.Extent.StartOffset $end = $entry.Item2.Extent.EndOffset } #endregion Case: Key Already Exists #region Case: Key Does not exist else { $line = $Ast.Extent.Text -split "`n" | Where-Object { $_ -match "#\s+$Property = " } # Entry already exists but is commented out if ($line) { $format = "$Property = {0}" $index = $Ast.Extent.Text.IndexOf($line) $start = $index + $line.Length - $line.TrimStart().Length $end = $index + $line.Length } # Entry does not exist already else { $indent = ($Ast.Extent.Text -split "`n" | Where-Object { $_ -match "^\s+ModuleVersion" }) -replace '^(\s*).+$', '$1' $format = "$($indent)$($Property) = {0}`n" $start = $mainHash.Extent.EndOffset - 1 $end = $mainHash.Extent.EndOffset - 1 } } #endregion Case: Key Does not exist $newText = $Ast.Extent.Text.SubString(0, $start) + ($format -f $stringValue) + $Ast.Extent.Text.SubString($end) [System.Management.Automation.Language.Parser]::ParseInput($newText, [ref]$null, [ref]$null) } function Update-PrivateDataProperty { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [OutputType([System.Management.Automation.Language.ScriptBlockAst])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Management.Automation.Language.Ast] $Ast, [string[]] $Tags, [string] $LicenseUri, [string] $IconUri, [string] $ProjectUri, [string] $ReleaseNotes, [string] $Prerelease, [object[]] $ExternalModuleDependencies, [bool] $EnableException, $Cmdlet ) $mainHash = $Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.HashtableAst] -and $args[0].KeyValuePairs.Item1.Value -contains 'RootModule' -and $args[0].KeyValuePairs.Item1.Value -Contains 'ModuleVersion' }, $true) $privateData = [ordered]@{ PSData = [ordered]@{ } } $replacements = @{ } $privateDataAst = $mainHash.KeyValuePairs | Where-Object { $_.Item1.Value -eq 'PrivateData' } | ForEach-Object { $_.Item2.PipelineElements[0].Expression } if ($privateDataAst) { foreach ($pair in $privateDataAst.KeyValuePairs) { if ($pair.Item1.Value -ne 'PSData') { $id = "%PSF_$(Get-Random)%" $privateData[$pair.Item1.Value] = $id $replacements[$id] = $pair.Item2.Extent.Text continue } foreach ($subPair in $pair.Item2.PipelineElements[0].Expression.KeyValuePairs) { $id = "%PSF_$(Get-Random)%" $privateData.PSData[$subPair.Item1.Value] = $id $replacements[$id] = $subPair.Item2.Extent.Text } } } if ($Tags) { $privateData.PSData['Tags'] = $Tags } if ($LicenseUri) { $privateData.PSData['LicenseUri'] = $LicenseUri } if ($IconUri) { $privateData.PSData['IconUri'] = $IconUri } if ($ProjectUri) { $privateData.PSData['ProjectUri'] = $ProjectUri } if ($ReleaseNotes) { $privateData.PSData['ReleaseNotes'] = $ReleaseNotes } if ($Prerelease) { $privateData.PSData['Prerelease'] = $Prerelease } if ($ExternalModuleDependencies) { $privateData.PSData['ExternalModuleDependencies'] = ConvertTo-ModuleRequirement -InputObject $ExternalModuleDependencies -Cmdlet $Cmdlet -EnableException $EnableException } $privateDataString = $privateData | ConvertTo-Psd1 -Depth 5 foreach ($pair in $replacements.GetEnumerator()) { $privateDataString = $privateDataString -replace "'$($pair.Key)'", $pair.Value } if (-not $privateDataAst) { $newManifest = $ast.Extent.Text.Insert(($mainHash.Extent.EndOffset - 1), "PrivateData = $privateDataString`n") } else { $newManifest = $ast.Extent.Text.SubString(0, $privateDataAst.Extent.StartOffset) + $privateDataString + $ast.Extent.Text.SubString($privateDataAst.Extent.EndOffset) } [System.Management.Automation.Language.Parser]::ParseInput($newManifest, [ref]$null, [ref]$null) } #endregion Utility Functions } process { $killIt = $ErrorActionPreference -eq 'Stop' $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$null, [ref]$null) $mainHash = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.HashtableAst] -and $args[0].KeyValuePairs.Item1.Value -contains 'RootModule' -and $args[0].KeyValuePairs.Item1.Value -Contains 'ModuleVersion' }, $true) if (-not $mainHash) { Stop-PSFFunction -String 'Update-PSFModuleManifest.Error.BadManifest' -StringValues (Get-Item -Path $Path).BaseName, $Path -Cmdlet $Cmdlet -EnableException $killIt -Continue:$Continue return } #region Main Properties $stringProperties = 'Guid', 'Author', 'CompanyName', 'Copyright', 'RootModule', 'ModuleVersion', 'Description', 'ProcessorArchitecture', 'PowerShellVersion', 'ClrVersion', 'DotNetFrameworkVersion', 'PowerShellHostName', 'PowerShellHostVersion', 'HelpInfoUri', 'DefaultCommandPrefix' foreach ($property in $stringProperties) { if ($PSBoundParameters.Keys -notcontains $property) { continue } $ast = Update-ManifestProperty -Ast $ast -Property $property -Value $PSBoundParameters.$property -Type String } $stringArrayProperties = 'CompatiblePSEditions', 'TypesToProcess', 'FormatsToProcess', 'ScriptsToProcess', 'RequiredAssemblies', 'FileList', 'FunctionsToExport', 'AliasesToExport', 'VariablesToExport', 'CmdletsToExport', 'DscResourcesToExport' foreach ($property in $stringArrayProperties) { if ($PSBoundParameters.Keys -notcontains $property) { continue } $ast = Update-ManifestProperty -Ast $ast -Property $property -Value $PSBoundParameters.$property -Type StringArray } $moduleProperties = 'RequiredModules', 'ModuleList', 'NestedModules' foreach ($property in $moduleProperties) { if ($PSBoundParameters.Keys -notcontains $property) { continue } $ast = Update-ManifestProperty -Ast $ast -Property $property -Value ($PSBoundParameters.$property | ConvertTo-ModuleRequirement -EnableException $killIt -Cmdlet $Cmdlet) -Type StringArray } #endregion Main Properties #region PrivateData Content if ($Tags -or $LicenseUri -or $IconUri -or $ProjectUri -or $ReleaseNotes -or $Prerelease -or $ExternalModuleDependencies) { $updateParam = $PSBoundParameters | ConvertTo-PSFHashtable -ReferenceCommand Update-PrivateDataProperty $updateParam.Cmdlet = $Cmdlet $updateParam.EnableException = $killIt $ast = Update-PrivateDataProperty -Ast $ast @updateParam } #endregion PrivateData Content if ($PassThru) { $ast.Extent.Text } else { $ast.Extent.Text | Set-Content -Path $Path } } } function Get-PSFPowerShellGet { <# .SYNOPSIS Returns the availability state for PowerShellGet. .DESCRIPTION Returns the availability state for PowerShellGet. Will verify, whether required prerequisites for module installation or publishing exist for v1/v2 versions of PowerShellGet. It will only check for the all users configuration, ignoring binaries stored in appdata. .PARAMETER ComputerName The computer to scan. Defaults to localhost. .PARAMETER Credential Credentials to use for the connection to the remote computers. .EXAMPLE PS C:\> Get-PSFPowerShellGet Returns, what the local PowerShellGet configuration is like. #> [CmdletBinding()] Param ( [Parameter(ValueFromPipeline = $true)] [PSFComputer[]] $ComputerName = $env:COMPUTERNAME, [PSCredential] $Credential ) begin { $code = { $modules = Get-Module -Name PowerShellGet -ListAvailable $modulesV3 = Get-Module -Name Microsoft.PowerShell.PSResourceGet -ListAvailable $isOnWindows = $PSVersionTable.PSVersion.Major -lt 6 -or $isWindows if ($isOnWindows) { $nugetPath = "$env:ProgramFiles\Microsoft\Windows\PowerShell\PowerShellGet\NuGet.exe" $v2CanInstall = Test-Path -Path "$env:ProgramFiles\PackageManagement\ProviderAssemblies\nuget\2.8.5.208\Microsoft.PackageManagement.NuGetProvider.dll" } else { $nugetPath = "$HOME/.config/powershell/powershellget/NuGet.exe" $v2CanInstall = $true } if ($modules | Where-Object { $_.Version.Major -lt 3 -and $_.Version -ge ([version]'2.5.0') }) { $v2CanInstall = $true } [PSCustomObject]@{ PSTypeName = 'PSFramework.NuGet.GetReport' ComputerName = $env:COMPUTERNAME V2 = ($modules | Where-Object { $_.Version.Major -lt 3 }) -as [bool] V3 = $modulesV3 -as [bool] V2CanInstall = $v2CanInstall V2CanPublish = Test-Path -Path $nugetPath Modules = $modules } } } process { Invoke-PSFCommand -ComputerName $ComputerName -ScriptBlock $code -Credential $Credential } } function Install-PSFPowerShellGet { <# .SYNOPSIS Deploys the different versions of PowerShellGet and PSResourceGet. .DESCRIPTION Deploys the different versions of PowerShellGet and PSResourceGet. With this command you can bulk-deploy PowerShell package management at scale. It can install: + latest version of PowerShellGet & PackageManagement (elsewhere referred to as V2/classic) + binaries needed to use PowerShellGet & PackageManagement without bootstrapping from the internet + latest version of Microsoft.PowerShell.PSResourceget (elsewhere referred to as V3/modern) It can do all that via PSRemoting, no SMB access needed. This command needs no internet access to deploy them - you can transport it into an offline environment and still profit from that. .PARAMETER Type What should be deployed/installed. + V2Binaries: What is required to use Get V2. + V2Latest: The latest version of Get V2 + V3Latest: The latest version of Get V3 Defaults to: V2Binaries .PARAMETER ComputerName The computer(s) to install to. Can be names, ADComputer objects, SQL Server connection strings or alreadya established PSSessions. Defaults to: localhost .PARAMETER Credential Credentials to use for establishing new remoting connections. .PARAMETER SourcePath Custom Path to get the module sources to deplo. You can download the latest module & binary versions from an online machine and then transport them into an offline environment. This allows you to update the version of Get V3 being deployed, without having to update (or wait for an update) of PSFramework.NuGet. .PARAMETER Offline Force a full offline mode. By default, the module will on install automatically try to check online for a newer version. It will still continue anyway if this fails, but if you want to avoid the network traffic & signals, use this switch. .PARAMETER NotInternal Do not use the internally provided PowerShellGet module versions. This REQUIRES you to either provide the module data via -SourcePath or to have live online access. .EXAMPLE PS C:\> Install-PSFPowerShell -Type V3Latest -ComputerName (Get-ADComputer -Filter * -SearchBase $myOU) This will install the latest version of PSResourceGet (V3) on all computers under the OU distinguishedName stored in $myOU #> [CmdletBinding()] Param ( [ValidateSet('V2Binaries', 'V2Latest', 'V3Latest')] [string[]] $Type = 'V2Binaries', [Parameter(ValueFromPipeline = $true)] [PSFComputer[]] $ComputerName = $env:COMPUTERNAME, [PSCredential] $Credential, [string] $SourcePath = (Join-Path -Path (Get-PSFPath -Name AppData) -ChildPath 'PowerShell/PSFramework/modules/PowerShellGet'), [switch] $Offline, [switch] $NotInternal ) begin { #region Functions function Resolve-PowerShellGet { [OutputType([hashtable])] [CmdletBinding()] param ( [string] $Type, [string] $SourcePath, [switch] $Offline, [switch] $NotInternal ) #region V2Binaries if ('V2Binaries' -eq $Type) { @{ Type = $Type NuGet = [System.IO.File]::ReadAllBytes("$script:ModuleRoot\bin\NuGet.exe") PkgMgmt = [System.IO.File]::ReadAllBytes("$script:ModuleRoot\bin\Microsoft.PackageManagement.NuGetProvider.dll") } return } #endregion V2Binaries $internalVersion = Get-Content -Path "$script:ModuleRoot\modules\modules.json" | ConvertFrom-Json if ($NotInternal) { $internalVersion = @{ } } $sourceVersion = @{ } $onlineVersion = @{ } $sourceFile = Join-Path -Path $SourcePath -ChildPath modules.json if (Test-Path -Path $sourceFile) { $sourceVersion = Get-Content -Path $sourceFile | ConvertFrom-Json } #region Check Online if (-not $Offline) { $links = @( 'PSGetV2' 'PSGetV3' 'PSPkgMgmt' ) foreach ($link in $links) { $resolvedUrl = Resolve-AkaMsLink -Name $link if (-not $resolvedUrl) { continue } $onlineVersion[$link] = [PSCustomObject]@{ Type = $link Name = ($resolvedUrl -split '/')[-2] Version = ($resolvedUrl -split '/')[-1] Resolved = $resolvedUrl FileName = '' } $onlineVersion[$link].FileName = '{0}-{1}.zip' -f $onlineVersion[$link].Name, $onlineVersion[$link].Version } } #endregion Check Online $source = 'Internal' $typeTag = switch ($Type) { 'V2Latest' { 'PSGetV2' } 'V3Latest' { 'PSGetV3' } } if ($sourceVersion.$typeTag.Version -and $sourceVersion.$typeTag.Version -ne $internalVersion.$typeTag.Version) { $source = 'Source' } if ($onlineVersion.$typeTag.Version -and $onlineVersion.$typeTag.Version -ne $internalVersion.$typeTag.Version) { $source = 'Online' } # If online version is newer than internal, download to appdata as cached version if ('Online' -eq $source) { if (-not (Test-Path -Path $SourcePath)) { $null = New-Item -Path $SourcePath -ItemType Directory -Force } Save-PSFPowerShellGet -Path $SourcePath # This can never happen if the user specified a path, so no risk of overwriting. } $rootPath = switch ($source) { Internal { "$script:ModuleRoot\modules" } Source { $SourcePath } Online { $SourcePath } } $actualConfiguration = Import-PSFPowerShellDataFile -Path (Join-Path -Path $rootPath -ChildPath 'modules.json') $data = @{ Type = $Type Config = $actualConfiguration } switch ($Type) { 'V2Latest' { $data.PSGetV2 = [System.IO.File]::ReadAllBytes((Join-Path -Path $rootPath -ChildPath $actualConfiguration.PSGetV2.FileName)) $data.PSPkgMgmt = [System.IO.File]::ReadAllBytes((Join-Path -Path $rootPath -ChildPath $actualConfiguration.PSPkgMgmt.FileName)) } 'V3Latest' { $data.PSGetV3 = [System.IO.File]::ReadAllBytes((Join-Path -Path $rootPath -ChildPath $actualConfiguration.PSGetV3.FileName)) } } $data } #endregion Functions #region Actual Code $code = { param ( $Data ) #region Functions function Install-ZipModule { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( $Config, [string] $ModulesFolder, [string] $TempFolder ) $modulePath = Join-Path -Path $ModulesFolder -ChildPath ('{0}/{1}' -f $Config.Name, ($Config.Version -replace '\-.+$')) if (Test-Path -Path $modulePath) { return } $null = New-Item -Path $modulePath -ItemType Directory -Force Expand-Archive -Path (Join-Path -Path $TempFolder -ChildPath $Config.FileName) -DestinationPath $modulePath } #endregion Functions ## Create temporary folder $tempFolder = (New-Item -Path $env:TEMP -Name "PSGet-$(Get-Random)" -ItemType Directory -Force).FullName #region Write binary data if ($Data.NuGet) { [System.IO.File]::WriteAllBytes((Join-Path -Path $tempFolder -ChildPath 'NuGet.exe'), $Data.NuGet) } if ($Data.PkgMgmt) { [System.IO.File]::WriteAllBytes((Join-Path -Path $tempFolder -ChildPath 'Microsoft.PackageManagement.NuGetProvider.dll'), $Data.PkgMgmt) } if ($Data.PSGetV2) { [System.IO.File]::WriteAllBytes((Join-Path -Path $tempFolder -ChildPath $Data.Config.PSGetV2.FileName), $Data.PSGetV2) } if ($Data.PSGetV3) { [System.IO.File]::WriteAllBytes((Join-Path -Path $tempFolder -ChildPath $Data.Config.PSGetV3.FileName), $Data.PSGetV3) } if ($Data.PSPkgMgmt) { [System.IO.File]::WriteAllBytes((Join-Path -Path $tempFolder -ChildPath $Data.Config.PSPkgMgmt.FileName), $Data.PSPkgMgmt) } #endregion Write binary data #region Copy to destination $isOnWindows = $PSVersionTable.PSVersion.Major -lt 6 -or $isWindows switch ($Data.Type) { #region V2 Bootstrap V2Binaries { if ($isOnWindows) { if (-not (Test-Path -Path "$env:ProgramFiles\Microsoft\Windows\PowerShell\PowerShellGet")) { $null = New-Item -Path "$env:ProgramFiles\Microsoft\Windows\PowerShell\PowerShellGet" -ItemType Directory -Force } Copy-Item -Path (Join-Path -Path $tempFolder -ChildPath 'NuGet.exe') -Destination "$env:ProgramFiles\Microsoft\Windows\PowerShell\PowerShellGet" -Force if (-not (Test-Path -Path "$env:ProgramFiles\PackageManagement\ProviderAssemblies\nuget\2.8.5.208")) { $null = New-Item -Path "$env:ProgramFiles\PackageManagement\ProviderAssemblies\nuget\2.8.5.208" -ItemType Directory -Force } Copy-Item -Path (Join-Path -Path $tempFolder -ChildPath 'Microsoft.PackageManagement.NuGetProvider.dll') -Destination "$env:ProgramFiles\PackageManagement\ProviderAssemblies\nuget\2.8.5.208" -Force } else { Copy-Item -Path (Join-Path -Path $tempFolder -ChildPath 'NuGet.exe') -Destination "$HOME/.config/powershell/powershellget" -Force } } #endregion V2 Bootstrap #region V2 Latest V2Latest { $modulesFolder = "$env:ProgramFiles\WindowsPowerShell\modules" if (-not $isOnWindows) { $modulesFolder = "/usr/local/share/powershell/Modules" } Install-ZipModule -Config $data.Config.PSGetV2 -ModulesFolder $modulesFolder -TempFolder $tempFolder Install-ZipModule -Config $data.Config.PSPkgMgmt -ModulesFolder $modulesFolder -TempFolder $tempFolder } #endregion V2 Latest #region V3 Latest V3Latest { $modulesFolder = "$env:ProgramFiles\WindowsPowerShell\modules" if (-not $isOnWindows) { $modulesFolder = "/usr/local/share/powershell/Modules" } Install-ZipModule -Config $data.Config.PSGetV3 -ModulesFolder $modulesFolder -TempFolder $tempFolder } #endregion V3 Latest } #endregion Copy to destination ## Cleanup Remove-Item -Path $tempFolder -Recurse -Force } #endregion Actual Code #region Resolve Source Configuration $stayOffline = $Offline $useInternal = -not $NotInternal if ($PSBoundParameters.Keys -contains 'SourcePath') { if ($PSBoundParameters.Keys -notcontains 'Offline') { $stayOffline = $true } if ($PSBoundParameters.Keys -notcontains 'NotInternal') { $useInternal = $false } } #endregion Resolve Source Configuration } process { # If installing the latest V2 modules, you'll also want the binaries needed if ('V2Latest' -in $Type -and 'V2Binaries' -notin $Type) { $Type = @($Type) + 'V2Binaries' } foreach ($typeEntry in $Type) { # Get Binaries / Modules to deploy $binaries = Resolve-PowerShellGet -Type $typeEntry -Offline:$stayOffline -SourcePath $SourcePath -NotInternal:$useInternal # Execute Deployment Invoke-PSFCommand -ComputerName $ComputerName -ScriptBlock $code -Credential $Credential -ArgumentList $binaries } } end { Search-PSFPowerShellGet } } function Save-PSFPowerShellGet { <# .SYNOPSIS Downloads and provides the latest packages for both PowerShellGet V2 and V3. .DESCRIPTION Downloads and provides the latest packages for both PowerShellGet V2 and V3. These can then be used by this module to deploy and bootstrap offline computers with package management tooling. .PARAMETER Path The path where to deploy the module packages as zip-files. Must be a directory. Defaults to: %AppData%/PowerShell/PSFramework/modules/PowerShellGet .EXAMPLE PS C:\> Save-PSFPowerShellGet Downloads and deploys the latest version of Get V2 & V3 to "%AppData%/PowerShell/PSFramework/modules/PowerShellGet" #> [CmdletBinding()] param ( [PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')] [string] $Path ) if (-not $Path) { $rootPath = Join-Path -Path (Get-PSFPath -Name AppData) -ChildPath 'PowerShell/PSFramework/modules/PowerShellGet' if (-not (Test-Path -LiteralPath $rootPath)) { $null = New-Item -LiteralPath $rootPath -ItemType Directory -Force } } $links = @( 'PSGetV2' 'PSGetV3' 'PSPkgMgmt' ) $pkgData = @{ } foreach ($link in $links) { $resolvedUrl = Resolve-AkaMsLink -Name $link if (-not $resolvedUrl) { Stop-PSFFunction -String 'Save-PowerShellGet.Error.UnableToResolve' -StringValues $link -EnableException $true -Cmdlet $PSCmdlet } $pkgData[$link] = [PSCustomObject]@{ Type = $link Name = ($resolvedUrl -split '/')[-2] Version = ($resolvedUrl -split '/')[-1] Resolved = $resolvedUrl FileName = '' } $pkgData[$link].FileName = '{0}-{1}.zip' -f $pkgData[$link].Name, $pkgData[$link].Version } $directory = New-PSFTempDirectory -Name psget -ModuleName PSFramework.NuGet foreach ($entry in $pkgData.Values) { Invoke-WebRequest -Uri $entry.Resolved -OutFile "$directory\temp-$($entry.Type).zip" $rootFolder = "$directory\$($entry.Type)" Expand-Archive -Path "$directory\temp-$($entry.Type).zip" -DestinationPath $rootFolder -Force # Cleanup nupkg residue $contentTypesPath = Join-Path -Path $rootFolder -ChildPath '[Content_Types].xml' Remove-Item -LiteralPath $contentTypesPath # LiteralPath so that the brackets don't interfere $relsPath = Join-Path -Path $rootFolder -ChildPath '_rels' Remove-Item -LiteralPath $relsPath -Force -Recurse $specPath = Join-Path -Path $rootFolder -ChildPath "$($entry.Name).nuspec" Remove-Item -LiteralPath $specPath -Force -Recurse $packagePath = Join-Path -Path $rootFolder -ChildPath 'package' Remove-Item -LiteralPath $packagePath -Force -Recurse # Cleanup Original download zip Remove-Item "$directory\temp-$($entry.Type).zip" # Create new zip file and delete old folder Compress-Archive -Path "$rootFolder\*" -DestinationPath "$directory\$($entry.FileName)" Remove-Item -LiteralPath $rootFolder -Recurse -Force } $pkgData | ConvertTo-Json | Set-Content -Path "$directory\modules.json" Copy-Item -Path $directory\* -Destination $Path -Force -Recurse Remove-PSFTempItem -Name psget -ModuleName PSFramework.NuGet } function Search-PSFPowerShellGet { <# .SYNOPSIS Scan for available PowerShellGet versions. .DESCRIPTION Scan for available PowerShellGet versions. The module caches the availability of PowerShellGet features on import. It also automatically updates those settings when it knows to do so. However, if you change the configuration outside of the PSFramework.NuGet module, you may need to manually trigger the scan for the module to take the changes into account. For example, if you use Install-Module, rather than Install-PSFModule to install the latest version of PowerShellGet, use this command to make the module aware of the fact. Otherwise, this will automatically be run the next time the module is loaded. .PARAMETER UseCache Whether to respect the already available data and not do anything after all. Mostly for internal use. .EXAMPLE PS C:\> Search-PSFPowerShellGet Scan for available PowerShellGet versions. #> [CmdletBinding()] Param ( [switch] $UseCache ) process { if ($UseCache -and $script:psget.Count -gt 0) { return } $configuration = Get-PSFPowerShellGet $script:psget = @{ 'v2' = $configuration.V2 'v2CanInstall' = $configuration.V2CanInstall 'v2CanPublish' = $configuration.V2CanPublish 'v3' = $configuration.V3 } } } function Get-PSFRepository { <# .SYNOPSIS Lists available PowerShell repositories. .DESCRIPTION Lists available PowerShell repositories. Includes both classic (V2 | Get-PSRepository) and new (V3 | Get-PSResourceRepository) repositories. This will also include additional metadata, including priority, which in this module is also applicable to classic repositories. Note on Status: In V2 repositories, the status can show "NoPublish" or "NoInstall". This is determined by whether it has been bootstrapped at the system level. If you have already bootstrapped it in user-mode, this may not be reflected correctly. If your computer is internet-facing, it can also automatically bootstrap itself without any issues. .PARAMETER Name Name of the repository to list. .PARAMETER Type What kind of repository to return: + All: (default) Return all, irrespective of type + V2: Only return classic repositories, as would be returned by Get-PSRepository + V3: Only return modern repositories, as would be returned by Get-PSResourceRepository .EXAMPLE PS C:\> Get-PSFRepository List all available repositories. #> [CmdletBinding()] Param ( [PsfArgumentCompleter('PSFramework.NuGet.Repository')] [string[]] $Name = '*', [ValidateSet('All','V2','V3')] [string] $Type = 'All' ) begin { Search-PSFPowerShellGet -UseCache } process { if ($script:psget.V3 -and $Type -in 'All','V3') { foreach ($repository in Get-PSResourceRepository -Name $Name -ErrorAction Ignore) { if (-not $repository) { continue } [PSCustomObject]@{ PSTypeName = 'PSFramework.NuGet.Repository' Name = $repository.Name Type = 'V3' Status = 'OK' Trusted = $repository.Trusted Priority = Get-PSFConfigValue -FullName "PSFramework.NuGet.Repositories.$($repository.Name).Priority" -Fallback $repository.Priority Uri = $repository.Uri Object = $repository Credential = Get-PSFConfigValue -FullName "PSFramework.NuGet.Repositories.$($repository.Name).Credential" } } } if ($script:psget.V2 -and $Type -in 'All','V2') { $status = 'OK' if (-not $script:psget.v2CanPublish) { $status = 'NoPublish' } if (-not $script:psget.v2CanInstall) { $status = 'NoInstall' } foreach ($repository in Get-PSRepository -Name $Name -ErrorAction Ignore) { if (-not $repository) { continue } [PSCustomObject]@{ PSTypeName = 'PSFramework.NuGet.Repository' Name = $repository.Name Type = 'V2' Status = $status Trusted = $repository.Trusted Priority = Get-PSFConfigValue -FullName "PSFramework.NuGet.Repositories.$($repository.Name).Priority" -Fallback 100 Uri = $repository.SourceLocation Object = $repository Credential = Get-PSFConfigValue -FullName "PSFramework.NuGet.Repositories.$($repository.Name).Credential" } } } } } function Set-PSFRepository { <# .SYNOPSIS Configure existing powershell repositories or define a new one. .DESCRIPTION Configure existing powershell repositories or define a new one. This allows you to modify their metadata, notably registering credentials to use on all requests or modifying its priority. For defining new repositories, it is required to at least define "Type" and "Uri" Some updates - the Uri and Trusted state - require updating the configuration on the PSGet repository settings, rather than just being contained within this module. The command will handle that, which will be slightly slower and also affect direct use of the PSGet commands (such as install-Module or Install-PSResource). Settings will apply to all repositories with the same name. If you have the same repository configured in both V2 and V3, they BOTH will receive the update. .PARAMETER Name Name of the repository to modify. Wildcards not supported (unless you actually name a repository with a wildcard in the name. In which case you probably want reconsider your naming strategy.) .PARAMETER Priority The priority the repository should have. Lower-numbered repositories will beu sed before repositories with higher numbers. .PARAMETER Credential Credentials to use on all requests against the repository. .PARAMETER Uri The Uri from which modules are installed (and to which they are published). Will update the PSGet repositories objects. .PARAMETER Trusted Whether the repository is considered trusted. .PARAMETER Type What version of PSGet it should use. - Any: Will register as V3 if available, otherwise V2. Will not update to V3 if already on V2. - Update: Will register under highest version available, upgrading from older versions if already available on old versions - All: Will register on ALL available versions - V2: Will only register on V2. V3 - if present and configured - will be unregistered. - V2Preferred: Will only register on V2. If V2 does not exist, existing V3 repositories will be allowed. - V3: Will only register on V3. If V2 is present, it will be unregistered, irrespective of whether V3 is available. .PARAMETER Persist Whether the settings should be remembered. If settings are not persisted, they only last until the console is closed. When persisting credentials, they are - at least on windows - stored encrypted in registry (HKCU) and are only readable by the same user on the same computer. .EXAMPLE PS C:\> Set-PSFRepository -Name AzDevOps -Credential $cred Assigns for the repository "AzDevOps" the credentials stored in $cred. All subsequent PSGet calls through this module will be made using those credentials. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(PositionalBinding = $false)] param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [PsfArgumentCompleter('PSFramework.NuGet.Repository')] [string] $Name, [int] $Priority, [PSCredential] $Credential, [string] $Uri, [bool] $Trusted, [ValidateSet('Any', 'Update', 'All', 'V2', 'V2Preferred', 'V3')] [string] $Type, [switch] $Persist ) process { # Not all changes require a repository update run $mustUpdate = $false if ($PSBoundParameters.Keys -contains 'Priority') { Set-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Priority" -Value $Priority if ($Persist) { Register-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Priority" } } if ($PSBoundParameters.Keys -contains 'Credential') { Set-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Credential" -Value $Credential if ($Persist) { Register-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Credential" } } if ($Uri) { Set-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Uri" -Value $Uri $mustUpdate = $true if ($Persist) { Register-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Uri" } } if ($PSBoundParameters.Keys -contains 'Trusted') { Set-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Trusted" -Value $Trusted $mustUpdate = $true if ($Persist) { Register-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Trusted" } } if ($Type) { Set-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Type" -Value $Type $mustUpdate = $true if ($Persist) { Register-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Type" } } if ($mustUpdate) { Update-PSFRepository } } } function Update-PSFRepository { <# .SYNOPSIS Executes configured repository settings. .DESCRIPTION Executes configured repository settings. Using configuration settings - for example applied per GPO or configuration file - it is possible to define intended repositories. The configuration settings must be named as 'PSFramework.NuGet.Repositories.<Repository Name>.<Setting>' Available settings: - Uri: Url or filesystem path to the repository. Used for both install and publish. - Priority: Priority of a PowerShell Repository. Numeric value, determines repository precedence. - Type: What kind of PowerShellGet version to apply the configuration to. Details on the options below. Defaults to 'Any'. - Trusted: Whether the repository should be trusted. Can be set to 0, 1, $false or $true. Defaults to $true. - Present: Whether the repository should exist at all. Can be set to 0, 1, $false or $true. Defaults to $true. Allows creating delete orders. Does not differentiate between V2 & V3 - Proxy: Link to the proxy to use. Property only available when creating a new repository, not for updating an existing one. Supported "Type" settings to handle different PowerShellGet versions: - Any: Will register as V3 if available, otherwise V2. Will not update to V3 if already on V2. - Update: Will register under highest version available, upgrading from older versions if already available on old versions - All: Will register on ALL available versions - V2: Will only register on V2. V3 - if present and configured - will be unregistered. - V2Preferred: Will only register on V2. If V2 does not exist, existing V3 repositories will be allowed. - V3: Will only register on V3. If V2 is present, it will be unregistered, irrespective of whether V3 is available. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Update-PSFRepository Executes configured repository settings, creating, updating and deleting repositories as defined. #> [CmdletBinding(SupportsShouldProcess = $true)] Param ( ) begin { #region Functions function Compare-Repository { [CmdletBinding()] param ( $Actual, $Configured ) $supportedTypes = 'Any', 'Update', 'All', 'V2', 'V2Preferred', 'V3' foreach ($configuredRepo in $Configured) { # Incomplete configurations are processed in a limited manner, as not enough information is present to create or delete $isIncomplete = -not $configuredRepo.Type -or -not $configuredRepo.Uri if (-not $isIncomplete -and $configuredRepo.Type -notin $supportedTypes) { Write-PSFMessage -Level Warning -String 'Update-PSFRepository.Error.InvalidType' -StringValues $configuredRepo.Type, ($supportedTypes -join ', ') continue } $matching = $Actual | Where-Object Name -EQ $configuredRepo._Name # An incomplete configuration can only be used to modify an existing repository, so skip if nothing matches if ($isIncomplete -and -not $matching) { continue } if (-not $isIncomplete) { $shouldExist = -not ($configuredRepo.PSObject.Properties.Name -contains 'Present' -and -not $configuredRepo.Present) $mayBeV2 = $configuredRepo.Type -in 'Any', 'Update', 'All', 'V2', 'V2Preferred' if ('Update' -eq $configuredRepo.Type -and $script:psget.V3) { $mayBeV2 = $false } $mustBeV2 = $configuredRepo.Type -in 'All', 'V2' $mayBeV3 = $configuredRepo.Type -in 'Any', 'Update', 'All', 'V3', 'V2Preferred' if ('V2Preferred' -eq $configuredRepo.Type -and $script:psget.V2) { $mayBeV3 = $false } $mustBeV3 = $configuredRepo.Type -in 'Update', 'All', 'V3' # Case: Should not exist and does not if (-not $shouldExist -and -not $matching) { continue } #region Deletion foreach ($matchingRepo in $matching) { if ( # Should exist $shouldExist -and ( $matchingRepo.Type -eq 'V2' -and $mayBeV2 -or $matchingRepo.Type -eq 'V3' -and $mayBeV3 ) ) { continue } [PSCustomObject]@{ Type = 'Delete' Configured = $configuredRepo Actual = $matchingRepo Changes = @{ } } } if (-not $shouldExist) { continue } #endregion Deletion #region Creation # Case: Should exist but does not if ($shouldExist -and -not $matching) { [PSCustomObject]@{ Type = 'Create' Configured = $configuredRepo Actual = $null Changes = @{ } } continue } # Case: Must exist on V2 but does not if ($mustBeV2 -and $matching.Type -notcontains 'V2' -and $script:psget.V2) { [PSCustomObject]@{ Type = 'Create' Configured = $configuredRepo Actual = $matching Changes = @{ } } } # Case: Must exist on V3 but does not if ($mustBeV3 -and $matching.Type -notcontains 'V3' -and $script:psget.V3) { [PSCustomObject]@{ Type = 'Create' Configured = $configuredRepo Actual = $matching Changes = @{ } } } # If there is no matching, existing repository, there is no need to update if (-not $matching) { continue } #endregion Creation } #region Update foreach ($matchingRepo in $matching) { $intendedUri = $configuredRepo.Uri if ('V2' -eq $matchingRepo.Type) { $intendedUri = $intendedUri -replace 'v3/index.json$', 'v2' } $trusted = $configuredRepo.Trusted -as [int] if ($null -eq $trusted -and $configuredRepo.Trusted -in 'True', 'False') { $trusted = $configuredRepo.Trusted -eq 'True' } if ($null -eq $trusted) { $trusted = $true } $changes = @{ } if (-not $isIncomplete -and $matchingRepo.Uri -ne $intendedUri) { $changes.Uri = $intendedUri } if ($matchingRepo.Trusted -ne $trusted) { $changes.Trusted = $trusted -as [bool] } if ( $configuredRepo.Priority -and $matchingRepo.Type -ne 'V2' -and $matchingRepo.Priority -ne $configuredRepo.Priority ) { $changes.Priority = $configuredRepo.Priority } if ($changes.Count -eq 0) { continue } [PSCustomObject]@{ Type = 'Update' Configured = $configuredRepo Actual = $matchingRepo Changes = $changes } } #endregion Update } } function New-Repository { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( $Change ) $registerV2 = $script:psget.V2 $registerV3 = $script:psget.V3 if ($Change.Actual.Type -contains 'V3') { $registerV3 = $false } if ($Change.Actual.Type -contains 'V2') { $registerV2 = $false } # If any already exists, we obviously want to create the other and need not process types again if (-not $Change.Actual) { switch ($Change.Configured.Type) { 'Any' { if ($registerV3) { $registerV2 = $false } } 'Update' { if ($registerV3) { $registerV2 = $false } } 'V2' { $registerV3 = $false } 'V2Preferrred' { $registerV3 = $false } 'V3' { $registerV2 = $false } } } $trusted = $Change.Configured.Trusted -as [int] if ($null -eq $trusted -and $Change.Configured.Trusted -in 'True', 'False') { $trusted = $Change.Configured.Trusted -eq 'True' } if ($null -eq $trusted) { $trusted = $true } if ($registerV2) { $uri = $Change.Configured.Uri -replace 'v3/index.json$', 'v2' $param = @{ Name = $Change.Configured._Name SourceLocation = $uri PublishLocation = $uri ErrorAction = 'Stop' } if ($trusted) { $param.InstallationPolicy = 'Trusted' } if ($Change.Configured.Proxy) { $param.Proxy = $Change.Configured.Proxy } try { Register-PSRepository @param } catch { Write-PSFMessage -Level Warning -String 'Update-PSFRepository.Register.Failed' -StringValues V2, $param.Name, $uri -ErrorRecord $_ } } if ($registerV3) { $param = @{ Name = $Change.Configured._Name Uri = $Change.Configured.Uri Trusted = $trusted ErrorAction = 'Stop' } if ($null -ne $Change.Configured.Priority) { $param.Priority = $Change.Configured.Priority } if ($Change.Configured.Proxy) { $param.Proxy = $Change.Configured.Proxy } try { Register-PSResourceRepository @param } catch { Write-PSFMessage -Level Warning -String 'Update-PSFRepository.Register.Failed' -StringValues V3, $param.Name, $param.Uri -ErrorRecord $_ } } } function Remove-Repository { [CmdletBinding(SupportsShouldProcess = $true)] param ( $Change ) switch ($Change.Actual.Type) { 'V2' { Invoke-PSFProtectedCommand -ActionString 'Update-PSFRepository.Repository.Unregister' -ActionStringValues $change.Actual.Type, $Change.Actual.Name -ScriptBlock { Unregister-PSRepository -Name $change.Actual.Name -ErrorAction Stop } -Target $change.Actual.Name -PSCmdlet $PSCmdlet -EnableException $false } 'V3' { Invoke-PSFProtectedCommand -ActionString 'Update-PSFRepository.Repository.Unregister' -ActionStringValues $change.Actual.Type, $Change.Actual.Name -ScriptBlock { Unregister-PSResourceRepository -Name $change.Actual.Name -ErrorAction Stop } -Target $change.Actual.Name -PSCmdlet $PSCmdlet -EnableException $false } } } function Set-Repository { [CmdletBinding(SupportsShouldProcess = $true)] param ( $Change ) $param = @{ Name = $change.Actual.Name } switch ($Change.Actual.Type) { 'V2' { if ($Change.Changes.Uri) { $param.SourceLocation = $Change.Changes.Uri $param.PublishLocation = $Change.Changes.Uri } if ($Change.Changes.Keys -contains 'Trusted') { if ($Change.Changes.Trusted) { $param.InstallationPolicy = 'Trusted' } else { $param.InstallationPolicy = 'Untrusted' } } Invoke-PSFProtectedCommand -ActionString 'Update-PSFRepository.Repository.Update' -ActionStringValues $change.Actual.Type, $Change.Actual.Name, ($param.Keys -join ',') -ScriptBlock { Set-PSRepository @param -ErrorAction Stop } -Target $change.Actual.Name -PSCmdlet $PSCmdlet -EnableException $false } 'V3' { if ($Change.Changes.Uri) { $param.Uri = $Change.Changes.Uri } if ($Change.Changes.Keys -contains 'Priority') { $param.Priority = $Change.Changes.Priority } if ($Change.Changes.Keys -contains 'Trusted') { $param.Trusted = $Change.Changes.Trusted } Invoke-PSFProtectedCommand -ActionString 'Update-PSFRepository.Repository.Update' -ActionStringValues $change.Actual.Type, $Change.Actual.Name, ($param.Keys -join ',') -ScriptBlock { Set-PSResourceRepository @param -ErrorAction Stop } -Target $change.Actual.Name -PSCmdlet $PSCmdlet -EnableException $false } } } #endregion Functions } process { $repositories = Get-PSFRepository $configuredRepositories = Select-PSFConfig -FullName PSFramework.NuGet.Repositories.* -Depth 3 $changes = Compare-Repository -Actual $repositories -Configured $configuredRepositories foreach ($change in $changes) { switch ($change.Type) { 'Create' { New-Repository -Change $change } 'Delete' { Remove-Repository -Change $change } 'Update' { Set-Repository -Change $change } } } } } function Publish-PSFResourceModule { <# .SYNOPSIS Publishes a pseudo-module, the purpose of which is to transport arbitrary files & folders. .DESCRIPTION Publishes a pseudo-module, the purpose of which is to transport arbitrary files & folders. This allows using nuget repositories to distribute arbitrary files, not bound to its direct PowerShell use as "Publish-PSFModule" would enforce. For example, with this, a templating engine could offer commands such as: - Publish-Template - Install-Template - Update-Template .PARAMETER Name Name of the module to create. .PARAMETER Version Version of the module to create. Defaults to "1.0.0". .PARAMETER Path Path to the files and folders to include. .PARAMETER Repository The repository to publish to. .PARAMETER Type What kind of repository to publish to. - All (default): All types of repositories are eligible. - V2: Only repositories from the old PowerShellGet are eligible. - V3: Only repositories from the new PSResourceGet are eligible. If multiple repositories of the same name are found, the one at the highest version among them is chosen. .PARAMETER Credential The credentials to use to authenticate to the Nuget service. Mostly used for internal repository servers. .PARAMETER ApiKey The ApiKey to use to authenticate to the Nuget service. Mostly used for publishing to the PSGallery. .PARAMETER SkipDependenciesCheck Do not validate dependencies or the module manifest. This removes the need to have the dependencies installed when publishing using PSGet v2 .PARAMETER DestinationPath Rather than publish to a repository, place the finished .nupgk file in this path. Use when doing the final publish step outside of PowerShell code. .PARAMETER RequiredModules The modules your resource module requires. These dependencies will be treated as Resoource modules as well, not regular modules. .PARAMETER Description Description of your resource module. Will be shown in repository services hosting it. .PARAMETER Author The author of your resource module. Defaults to your user name. .PARAMETER Tags Tags to include in your resource module. .PARAMETER LicenseUri Link to the license governing your resource module. .PARAMETER IconUri Link to the icon to present in the PSGallery. .PARAMETER ProjectUri Link to the project your resources originate from. Used in the PSGallery to guide visitors to more information. .PARAMETER ReleaseNotes Release notes for your resource module. Or at least a link to them. .PARAMETER Prerelease The prerelease flag to tag your resource module under. This allows hiding it from most users. .EXAMPLE PS C:\> Publish-PSFResourceModule -Name Psmd.Template.MyFunction -Version 1.1.0 -Path .\MyFunction\* -Repository PSGallery -ApiKey $key Publishes all files under the MyFunction folder to the PSGallery. The resource module will be named "Psmd.Template.MyFunction" and versioned as '1.1.0' #> [CmdletBinding(DefaultParameterSetName = 'ToRepository')] Param ( [PsfValidateScript('PSFramework.Validate.SafeName', ErrorString = 'PSFramework.Validate.SafeName')] [string] $Name, [string] $Version = '1.0.0', [Parameter(Mandatory = $true)] [PsfPath] $Path, [Parameter(Mandatory = $true, ParameterSetName = 'ToRepository')] [PsfValidateSet(TabCompletion = 'PSFramework.NuGet.Repository')] [PsfArgumentCompleter('PSFramework.NuGet.Repository')] [string[]] $Repository, [Parameter(ParameterSetName = 'ToRepository')] [ValidateSet('All', 'V2', 'V3')] [string] $Type = 'All', [Parameter(ParameterSetName = 'ToRepository')] [PSCredential] $Credential, [Parameter(ParameterSetName = 'ToRepository')] [string] $ApiKey, [Parameter(ParameterSetName = 'ToRepository')] [switch] $SkipDependenciesCheck, [Parameter(Mandatory = $true, ParameterSetName = 'ToPath')] [PsfDirectory] $DestinationPath, [object[]] $RequiredModules, [string] $Description = '<Dummy Description>', [string] $Author, [string[]] $Tags, [string] $LicenseUri, [string] $IconUri, [string] $ProjectUri, [string] $ReleaseNotes, [string] $Prerelease ) begin { $killIt = $ErrorActionPreference -eq 'Stop' $stagingDirectory = New-PSFTempDirectory -ModuleName 'PSFramework.NuGet' -Name Publish.ResourceModule -DirectoryName $Name $publishParam = $PSBoundParameters | ConvertTo-PSFHashtable -ReferenceCommand Publish-PSFModule -Exclude Path, ErrorAction } process { try { New-DummyModule -Path $stagingDirectory -Name $Name -Version $Version -RequiredModules $RequiredModules -Description $Description -Author $Author $resources = New-Item -Path $stagingDirectory -Name Resources -ItemType Directory -Force $Path | Copy-Item -Destination $resources.FullName -Recurse -Force -Confirm:$false -WhatIf:$false Publish-PSFModule @publishParam -Path $stagingDirectory -ErrorAction Stop } catch { Stop-PSFFunction -String 'Publish-PSFResourceModule.Error' -StringValues $Name, ($Repository -join ', ') -EnableException $killIt -ErrorRecord $_ -Cmdlet $PSCmdlet return } finally { Remove-PSFTempItem -ModuleName 'PSFramework.NuGet' -Name Publish.ResourceModule } } } function Save-PSFResourceModule { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER Name Name of the module to download. .PARAMETER Version Version constrains for the resource to save. Will use the latest version available within the limits. Examples: - "1.0.0": EXACTLY this one version - "1.0.0-1.999.999": Any version between the two limits (including the limit values) - "[1.0.0-2.0.0)": Any version greater or equal to 1.0.0 but less than 2.0.0 - "2.3.0-": Any version greater or equal to 2.3.0. Supported Syntax: <Prefix><Version><Connector><Version><Suffix> Prefix: "[" (-ge) or "(" (-gt) or nothing (-ge) Version: A valid version of 2-4 elements or nothing Connector: A "," or a "-" Suffix: "]" (-le) or ")" (-lt) or nothing (-le) .PARAMETER Prerelease Whether to include prerelease versions in the potential results. .PARAMETER Path Where to store the resource. .PARAMETER SkipDependency Do not include any dependencies. Works with PowerShellGet V1/V2 as well. .PARAMETER AuthenticodeCheck Whether resource modules must be correctly signed by a trusted source. Uses "Get-PSFModuleSignature" for validation. Defaults to: $false Default can be configured under the 'PSFramework.NuGet.Install.AuthenticodeSignature.Check' setting. .PARAMETER Force Overwrite files and folders that already exist in the target path. By default it will skip modules that do already exist in the target path. .PARAMETER Credential The credentials to use for connecting to the Repository. .PARAMETER Repository Repositories to install from. Respects the priority order of repositories. See Get-PSFRepository for available repositories (and their priority). Lower numbers are installed from first. .PARAMETER TrustRepository Whether we should trust the repository installed from and NOT ask users for confirmation. .PARAMETER Type What type of repository to download from. V2 uses classic Save-Module. V3 uses Save-PSResource. Availability depends on the installed PSGet module versions and configured repositories. Use Install-PSFPowerShellGet to deploy the latest versions of the package modules. Only the version on the local computer matters, even when deploying to remote computers. .PARAMETER InputObject The resource module to install. Takes the output of Get-Module, Find-Module, Find-PSResource and Find-PSFModule, to specify the exact version and name of the resource module. Even when providing a locally available version, the resource module will still be downloaded from the repositories chosen. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Save-PSFResourceModule -Name Psmd.Templates.MiniModule -Path . Downloads the resource module "Psmd.Templates.MiniModule" and extracts its resources into the current path. #> [CmdletBinding(SupportsShouldProcess = $true)] Param ( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'ByName')] [string[]] $Name, [Parameter(ParameterSetName = 'ByName')] [string] $Version, [Parameter(ParameterSetName = 'ByName')] [switch] $Prerelease, [Parameter(Mandatory = $true, Position = 1)] [PSFDirectory] $Path, [switch] $SkipDependency, [switch] $AuthenticodeCheck = (Get-PSFConfigValue -FullName 'PSFramework.NuGet.Install.AuthenticodeSignature.Check'), [switch] $Force, [PSCredential] $Credential, [PsfArgumentCompleter('PSFramework.NuGet.Repository')] [string[]] $Repository = ((Get-PSFrepository).Name | Sort-Object -Unique), [switch] $TrustRepository, [ValidateSet('All', 'V2', 'V3')] [string] $Type = 'All', [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ByObject')] [object[]] $InputObject ) begin { $killIt = $ErrorActionPreference -eq 'Stop' } process { $tempDirectory = New-PSFTempDirectory -ModuleName 'PSFramework.NuGet' -Name ResourceModule try { $saveParam = $PSBoundParameters | ConvertTo-PSFHashtable -ReferenceCommand Save-PSFModule -Exclude Path, ErrorAction Invoke-PSFProtectedCommand -ActionString 'Save-PSFResourceModule.Downloading' -ActionStringValues ($Name -join ', ') -ScriptBlock { $null = Save-PSFModule @saveParam -Path $tempDirectory -ErrorAction Stop -WhatIf:$false -Confirm:$false } -PSCmdlet $PSCmdlet -EnableException $killIt -WhatIf:$false -Confirm:$false if (Test-PSFFunctionInterrupt) { return } foreach ($pathEntry in $Path) { foreach ($module in Get-ChildItem -Path $tempDirectory) { foreach ($versionFolder in Get-ChildItem -LiteralPath $module.FullName) { $dataPath = Join-Path -Path $versionFolder.FullName -ChildPath 'Resources' if (-not (Test-Path -Path $dataPath)) { Write-PSFMessage -String 'Save-PSFResourceModule.Skipping.InvalidResource' -StringValues $module.Name, $versionFolder.Name continue } if (-not $PSCmdlet.ShouldProcess("$($module.Name) ($($versionFolder.Name))", "Deploy to $pathEntry")) { continue } foreach ($item in Get-ChildItem -LiteralPath $dataPath) { $targetPath = Join-Path -Path $pathEntry -ChildPath $item.Name if (-not $Force -and (Test-path -Path $targetPath)) { Write-PSFMessage -String 'Save-PSFResourceModule.Skipping.AlreadyExists' -StringValues $module.Name, $versionFolder.Name, $item.Name, $pathEntry continue } Invoke-PSFProtectedCommand -ActionString 'Save-PSFResourceModule.Deploying' -ActionStringValues $module.Name, $versionFolder.Name, $item.Name, $pathEntry -ScriptBlock { Move-Item -LiteralPath $item.FullName -Destination $pathEntry -Force -ErrorAction Stop -Confirm:$false -WhatIf:$false } -Target $item.Name -PSCmdlet $PSCmdlet -EnableException $killIt -Continue -Confirm:$false -WhatIf:$false } } } } } finally { Remove-PSFTempItem -ModuleName 'PSFramework.NuGet' -Name ResourceModule } } } function Get-PSFModuleSignature { <# .SYNOPSIS Verifies, whether a module is properly signed. .DESCRIPTION Verifies, whether a module is properly signed. Iterates over every module file and verifies its signature. The result reports: - Overall signing status - Signatures not Timestamped count - Status Summary - Subject of signing certs summary - Issuer of signing certs summary A module should be considered signed, when ... - the over signing status is valid - the subjects are expected (A microsoft module being signed by a microsoft code signing cert, etc.) - the issuer CAs are expected (A microsoft module being signed by a cert issued by Microsoft, etc.) .PARAMETER Path Path to the module(s) to scan. Should be the path to either a module-root or a psd1 file. .EXAMPLE PS C:\> Get-PSFModuleSignature -Path . Returns, whether the module in the current path is signed. .EXAMPLE PS C:\> Get-PSFModuleSignature -Path \\contoso.com\it\coding\modules\ContosoTools Verifies the code signing of the module stored in \\contoso.com\it\coding\modules\ContosoTools .EXAMPLE PS C:\> Get-Module | Get-PSFModuleSignature Verifies for each currently loaded module, whether they are signed. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('ModuleBase', 'FullName')] [string[]] $Path ) begin { function Resolve-ModulePath { [CmdletBinding()] param ( [string[]] $Path, $Cmdlet ) foreach ($pathItem in $Path) { try { $resolvedPaths = Resolve-Path $pathItem } catch { $record = [System.Management.Automation.ErrorRecord]::new( [Exception]::new("Path not found: $pathItem", $_.Exception), "InvalidPath", [System.Management.Automation.ErrorCategory]::InvalidArgument, $pathItem ) $Cmdlet.WriteError($record) continue } foreach ($resolvedPath in $resolvedPaths) { $item = Get-Item -LiteralPath $resolvedPath if ($item.PSIsContainer) { $manifests = Get-ChildItem -LiteralPath $item.FullName -Filter *.psd1 -Recurse -ErrorAction SilentlyContinue if (-not $manifests) { $record = [System.Management.Automation.ErrorRecord]::new( [Exception]::new("No module found in: $resolvedPath (resolved from $pathItem)"), "ObjectNotFound", [System.Management.Automation.ErrorCategory]::InvalidArgument, $pathItem ) $Cmdlet.WriteError($record) continue } foreach ($manifest in $manifests) { $manifest.Directory.FullName } continue } if ($item.Extension -in '.psd1', 'psm1') { $item.Directory.FullName continue } if (Get-Item -Path "$($item.Directory.FullName)\*.psd1") { $item.Directory.FullName continue } $record = [System.Management.Automation.ErrorRecord]::new( [Exception]::new("Unexpected file: $resolvedPaht from $pathItem"), "UnexpectedPath", [System.Management.Automation.ErrorCategory]::InvalidArgument, $pathItem ) $Cmdlet.WriteError($record) } } } function Get-ModuleSignatureInternal { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Path ) $signatureStatus = Get-ChildItem -LiteralPath $Path -Recurse -File | ForEach-Object { $currentItem = $_.FullName try { Get-AuthenticodeSignature -LiteralPath $currentItem } catch { [PSCustomObject]@{ PSTypeName = 'System.Management.Automation.Signature' SignerCertificate = $null TimeStamperCertificate = $null Status = 'AccessError' StatusMessage = $_ Path = $currentItem SignatureType = $null IsOSBinary = $false } } } $manifest = Get-ChildItem -LiteralPath $Path -Filter *.psd1 | Select-Object -First 1 $manifestData = @{} if ($manifest) { $manifestData = Import-PowerShellDataFile -LiteralPath $manifest.FullName } [PSCustomObject]@{ ModuleBase = $Path Name = $manifest.BaseName Version = $manifestData.ModuleVersion IsSigned = -not @($signatureStatus).Where{ $_.Status -notin 'Valid', 'UnknownError' } FileCount = @($signatureStatus).Count NoTimestampCount = @($signatureStatus).Where{ $_.SignerCertificate -and -not $_.TimeStamperCertificate }.Count ByStatus = ConvertTo-SigningSummary -Results $signatureStatus -Type Status ByIssuer = ConvertTo-SigningSummary -Results $signatureStatus -Type Issuer BySubject = ConvertTo-SigningSummary -Results $signatureStatus -Type Subject Signatures = $signatureStatus } } function ConvertTo-SigningSummary { [CmdletBinding()] param ( [AllowEmptyCollection()] $Results, [Parameter(Mandatory = $true)] [ValidateSet('Issuer', 'Subject', 'Status')] [string] $Type ) $groupBy = @{ Issuer = { $_.SignerCertificate.Issuer } Subject = { $_.SignerCertificate.Subject } Status = 'Status' } $groups = $Results | Group-Object $groupBy[$Type] $hash = @{ } foreach ($group in $groups) { $hash[$group.Name] = $group.Group } $entry = [PSCustomObject]@{ TotalCount = @($Results).Count GroupCount = @($groups).Count Results = $hash } Add-Member -InputObject $entry -MemberType ScriptMethod -Name ToString -Force -Value { $lines = foreach ($pair in $this.Results.GetEnumerator()) { if (-not $pair.Key) { continue } if ($pair.Key -eq 'UnknownError') { '{0}: {1} (Usually: File format that cannot be signed)' -f $pair.Value.Count, $pair.Key } else { '{0}: {1}' -f $pair.Value.Count, $pair.Key } } $lines -join "`n" } $entry } } process { foreach ($inputPath in Resolve-ModulePath -Path $Path -Cmdlet $PSCmdlet | Sort-Object -Unique) { Get-ModuleSignatureInternal -Path $inputPath } } } <# This is an example configuration file By default, it is enough to have a single one of them, however if you have enough configuration settings to justify having multiple copies of it, feel totally free to split them into multiple files. #> <# # Example Configuration Set-PSFConfig -Module 'PSFramework.NuGet' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" #> Set-PSFConfig -Module 'PSFramework.NuGet' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." Set-PSFConfig -Module 'PSFramework.NuGet' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." Set-PSFConfig -Module 'PSFramework.NuGet' -Name 'Install.AuthenticodeSignature.Check' -Value $false -Initialize -Validation 'bool' -Description 'Whether on installation or download of module its code-signing will be checked first.' Set-PSFConfig -Module 'PSFramework.NuGet' -Name 'Remoting.DefaultConfiguration' -Value 'Microsoft.PowerShell' -Initialize -Validation string -Description 'The PSSessionConfiguration to use when initializing new PS remoting sessions' Set-PSFConfig -Module 'PSFramework.NuGet' -Name 'Remoting.Throttling' -Value 5 -Initialize -Validation integerpositive -Description 'Up to how many remote computers to deploy to in parallel.' <# Stored scriptblocks are available in [PsfValidateScript()] attributes. This makes it easier to centrally provide the same scriptblock multiple times, without having to maintain it in separate locations. It also prevents lengthy validation scriptblocks from making your parameter block hard to read. Set-PSFScriptblock -Name 'PSFramework.NuGet.ScriptBlockName' -Scriptblock { } #> Register-PSFTeppScriptblock -Name 'PSFramework.NuGet.ModuleScope' -ScriptBlock { foreach ($scope in Get-PSFModuleScope) { @{ Text = $scope.Name; ToolTip = $scope.Description } } } -Global <# # Example: Register-PSFTeppScriptblock -Name "PSFramework.NuGet.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' } #> Register-PSFTeppScriptblock -Name "PSFramework.NuGet.Repository" -ScriptBlock { foreach ($repository in Get-PSFRepository | Group-Object Name) { @{ Text = $repository.Name Tooltip = '[{0}] {1}' -f (($repository.Group.Type | Sort-Object) -join ', '), $repository.Name } } } <# # Example: Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name PSFramework.NuGet.alcohol #> # What Get Version is available <# $script:psget = @{ 'v2' = $configuration.V2 'v2CanInstall' = $configuration.V2CanInstall 'v2CanPublish' = $configuration.V2CanPublish 'v3' = $configuration.V3 } #> $script:psget = @{ } # What paths are available for Install-PSFModule's "-Scope" parameter $script:moduleScopes = @{ } # Static Override values provided when using Disable-ModuleCommand $script:ModuleCommandReturns = @{ } # Load what PowerShellGet versions are available Search-PSFPowerShellGet # Ensure all configured repositories exist, and all unintended repositories are gone Update-PSFRepository New-PSFLicense -Product 'PSFramework.NuGet' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2023-03-13") -Text @" Copyright (c) 2023 Friedrich Weinmann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "@ $code = { if ($PSVersionTable.PSVersion.Major -le 5) { return "$([Environment]::GetFolderPath("ProgramFiles"))\WindowsPowerShell\Modules" } if ($IsWindows) { return "$([Environment]::GetFolderPath("ProgramFiles"))\PowerShell\Modules" } '/usr/local/share/powershell/Modules' } $scopeParam = @{ Name = 'AllUsers' ScriptBlock = $code Description = 'Default path for modules visible to all users.' } Register-PSFModuleScope @scopeParam $code = { if ($PSVersionTable.PSVersion.Major -le 5) { return "$([Environment]::GetFolderPath("ProgramFiles"))\WindowsPowerShell\Modules" } if ($IsWindows) { return "$([Environment]::GetFolderPath("ProgramFiles"))\WindowsPowerShell\Modules" } '/usr/local/share/powershell/Modules' } $scopeParam = @{ Name = 'AllUsersWinPS' ScriptBlock = $code Description = 'Default PS 5.1 path for modules visible to all users. Modules will be available to all versions of PowerShell. Will still work on non-Windows systems, but be no different to "AllUsers".' } Register-PSFModuleScope @scopeParam $code = { if ($PSVersionTable.PSVersion.Major -le 5) { return "$([Environment]::GetFolderPath("MyDocuments"))\WindowsPowerShell\Modules" } if ($IsWindows) { return "$([Environment]::GetFolderPath("MyDocuments"))\PowerShell\Modules" } '~/.local/share/powershell/Modules' } $scopeParam = @{ Name = 'CurrentUser' ScriptBlock = $code Description = 'Default path for modules visible to the current user only.' } Register-PSFModuleScope @scopeParam foreach ($setting in Select-PSFConfig -FullName PSFramework.NuGet.ModuleScopes.* -Depth 3) { $regParam = @{ Name = $setting._Name Path = $setting.Path } if ($setting.Description) { $regParam.Description = $setting.Description } Register-PSFModuleScope @regparam } #endregion Load compiled code |