PSAppDeployToolkit.WinGet.psm1
<#
.SYNOPSIS PSAppDeployToolkit.WinGet - This module script a basic scaffold to use with PSAppDeployToolkit modules destined for the PowerShell Gallery. .DESCRIPTION This module can be directly imported from the command line via Import-Module, but it is usually imported by the Invoke-AppDeployToolkit.ps1 script. PSAppDeployToolkit is licensed under the BSD 3-Clause License - Copyright (C) 2024 Mitch Richters. All rights reserved. .NOTES BSD 3-Clause License Copyright (c) 2024, Mitch Richters Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #> #----------------------------------------------------------------------------- # # MARK: Module Initialization Code # #----------------------------------------------------------------------------- # Throw if this psm1 file isn't being imported via our manifest. if (!([System.Environment]::StackTrace.Split("`n").Trim() -like '*Microsoft.PowerShell.Commands.ModuleCmdletBase.LoadModuleManifest(*')) { throw [System.Management.Automation.ErrorRecord]::new( [System.InvalidOperationException]::new("This module must be imported via its .psd1 file, which is recommended for all modules that supply a .psd1 file."), 'ModuleImportError', [System.Management.Automation.ErrorCategory]::InvalidOperation, $MyInvocation.MyCommand.ScriptBlock.Module ) } # Build out lookup table for all cmdlets used within module, starting with the core cmdlets. $CommandTable = [System.Collections.Generic.Dictionary[System.String, System.Management.Automation.CommandInfo]]::new() $ExecutionContext.SessionState.InvokeCommand.GetCmdlets() | & { process { if ($_.PSSnapIn -and $_.PSSnapIn.Name.Equals('Microsoft.PowerShell.Core') -and $_.PSSnapIn.IsDefault) { $CommandTable.Add($_.Name, $_) } } } # Expand command lookup table with cmdlets used through this module. & { $RequiredModules = [System.Collections.ObjectModel.ReadOnlyCollection[Microsoft.PowerShell.Commands.ModuleSpecification]]$( @{ ModuleName = 'Appx'; Guid = 'aeef2bef-eba9-4a1d-a3d2-d0b52df76deb'; ModuleVersion = '1.0' } @{ ModuleName = 'Dism'; Guid = '389c464d-8b8d-48e9-aafe-6d8a590d6798'; ModuleVersion = '1.0' } @{ ModuleName = 'Microsoft.PowerShell.Archive'; Guid = 'eb74e8da-9ae2-482a-a648-e96550fb8733'; ModuleVersion = '1.0' } @{ ModuleName = 'Microsoft.PowerShell.Management'; Guid = 'eefcb906-b326-4e99-9f54-8b4bb6ef3c6d'; ModuleVersion = '1.0' } @{ ModuleName = 'Microsoft.PowerShell.Utility'; Guid = '1da87e53-152b-403e-98dc-74d7b4d63d59'; ModuleVersion = '1.0' } @{ ModuleName = 'PSAppDeployToolkit'; Guid = '8c3c366b-8606-4576-9f2d-4051144f7ca2'; ModuleVersion = '4.0.4' } @{ ModuleName = 'psyml'; Guid = 'a88e2e67-a937-4d98-a4d3-0b03d3ade169'; ModuleVersion = '1.0.0' } ) (& $Script:CommandTable.'Import-Module' -FullyQualifiedName $RequiredModules -Global -Force -PassThru -ErrorAction Stop).ExportedCommands.Values | & { process { $CommandTable.Add($_.Name, $_) } } } # Set required variables to ensure module functionality. & $Script:CommandTable.'New-Variable' -Name ErrorActionPreference -Value ([System.Management.Automation.ActionPreference]::Stop) -Option Constant -Force & $Script:CommandTable.'New-Variable' -Name InformationPreference -Value ([System.Management.Automation.ActionPreference]::Continue) -Option Constant -Force & $Script:CommandTable.'New-Variable' -Name ProgressPreference -Value ([System.Management.Automation.ActionPreference]::SilentlyContinue) -Option Constant -Force # Ensure module operates under the strictest of conditions. & $Script:CommandTable.'Set-StrictMode' -Version 3 # Store build information pertaining to this module's state. & $Script:CommandTable.'New-Variable' -Name Module -Option Constant -Force -Value ([ordered]@{ Manifest = & $Script:CommandTable.'Import-LocalizedData' -BaseDirectory $PSScriptRoot -FileName 'PSAppDeployToolkit.WinGet.psd1' Compiled = $MyInvocation.MyCommand.Name.Equals('PSAppDeployToolkit.WinGet.psm1') }).AsReadOnly() # Remove any previous functions that may have been defined. if ($Module.Compiled) { & $Script:CommandTable.'New-Variable' -Name FunctionPaths -Option Constant -Value ($MyInvocation.MyCommand.ScriptBlock.Ast.EndBlock.Statements | & { process { if ($_ -is [System.Management.Automation.Language.FunctionDefinitionAst]) { return "Microsoft.PowerShell.Core\Function::$($_.Name)" } } }) & $Script:CommandTable.'Remove-Item' -LiteralPath $FunctionPaths -Force -ErrorAction Ignore } # Define enum for all known WinGet exit codes. enum ADTWinGetExitCode { INTERNAL_ERROR = -1978335231 INVALID_CL_ARGUMENTS = -1978335230 COMMAND_FAILED = -1978335229 MANIFEST_FAILED = -1978335228 CTRL_SIGNAL_RECEIVED = -1978335227 SHELLEXEC_INSTALL_FAILED = -1978335226 UNSUPPORTED_MANIFESTVERSION = -1978335225 DOWNLOAD_FAILED = -1978335224 CANNOT_WRITE_TO_UPLEVEL_INDEX = -1978335223 INDEX_INTEGRITY_COMPROMISED = -1978335222 SOURCES_INVALID = -1978335221 SOURCE_NAME_ALREADY_EXISTS = -1978335220 INVALID_SOURCE_TYPE = -1978335219 PACKAGE_IS_BUNDLE = -1978335218 SOURCE_DATA_MISSING = -1978335217 NO_APPLICABLE_INSTALLER = -1978335216 INSTALLER_HASH_MISMATCH = -1978335215 SOURCE_NAME_DOES_NOT_EXIST = -1978335214 SOURCE_ARG_ALREADY_EXISTS = -1978335213 NO_APPLICATIONS_FOUND = -1978335212 NO_SOURCES_DEFINED = -1978335211 MULTIPLE_APPLICATIONS_FOUND = -1978335210 NO_MANIFEST_FOUND = -1978335209 EXTENSION_PUBLIC_FAILED = -1978335208 COMMAND_REQUIRES_ADMIN = -1978335207 SOURCE_NOT_SECURE = -1978335206 MSSTORE_BLOCKED_BY_POLICY = -1978335205 MSSTORE_APP_BLOCKED_BY_POLICY = -1978335204 EXPERIMENTAL_FEATURE_DISABLED = -1978335203 MSSTORE_INSTALL_FAILED = -1978335202 COMPLETE_INPUT_BAD = -1978335201 YAML_INIT_FAILED = -1978335200 YAML_INVALID_MAPPING_KEY = -1978335199 YAML_DUPLICATE_MAPPING_KEY = -1978335198 YAML_INVALID_OPERATION = -1978335197 YAML_DOC_BUILD_FAILED = -1978335196 YAML_INVALID_EMITTER_STATE = -1978335195 YAML_INVALID_DATA = -1978335194 LIBYAML_ERROR = -1978335193 MANIFEST_VALIDATION_WARNING = -1978335192 MANIFEST_VALIDATION_FAILURE = -1978335191 INVALID_MANIFEST = -1978335190 UPDATE_NOT_APPLICABLE = -1978335189 UPDATE_ALL_HAS_FAILURE = -1978335188 INSTALLER_SECURITY_CHECK_FAILED = -1978335187 DOWNLOAD_SIZE_MISMATCH = -1978335186 NO_UNINSTALL_INFO_FOUND = -1978335185 EXEC_UNINSTALL_COMMAND_FAILED = -1978335184 ICU_BREAK_ITERATOR_ERROR = -1978335183 ICU_CASEMAP_ERROR = -1978335182 ICU_REGEX_ERROR = -1978335181 IMPORT_INSTALL_FAILED = -1978335180 NOT_ALL_PACKAGES_FOUND = -1978335179 JSON_INVALID_FILE = -1978335178 SOURCE_NOT_REMOTE = -1978335177 UNSUPPORTED_RESTSOURCE = -1978335176 RESTSOURCE_INVALID_DATA = -1978335175 BLOCKED_BY_POLICY = -1978335174 RESTAPI_INTERNAL_ERROR = -1978335173 RESTSOURCE_INVALID_URL = -1978335172 RESTAPI_UNSUPPORTED_MIME_TYPE = -1978335171 RESTSOURCE_INVALID_VERSION = -1978335170 SOURCE_DATA_INTEGRITY_FAILURE = -1978335169 STREAM_READ_FAILURE = -1978335168 PACKAGE_AGREEMENTS_NOT_ACCEPTED = -1978335167 PROMPT_INPUT_ERROR = -1978335166 UNSUPPORTED_SOURCE_REQUEST = -1978335165 RESTAPI_ENDPOINT_NOT_FOUND = -1978335164 SOURCE_OPEN_FAILED = -1978335163 SOURCE_AGREEMENTS_NOT_ACCEPTED = -1978335162 CUSTOMHEADER_EXCEEDS_MAXLENGTH = -1978335161 MISSING_RESOURCE_FILE = -1978335160 MSI_INSTALL_FAILED = -1978335159 INVALID_MSIEXEC_ARGUMENT = -1978335158 FAILED_TO_OPEN_ALL_SOURCES = -1978335157 DEPENDENCIES_VALIDATION_FAILED = -1978335156 MISSING_PACKAGE = -1978335155 INVALID_TABLE_COLUMN = -1978335154 UPGRADE_VERSION_NOT_NEWER = -1978335153 UPGRADE_VERSION_UNKNOWN = -1978335152 ICU_CONVERSION_ERROR = -1978335151 PORTABLE_INSTALL_FAILED = -1978335150 PORTABLE_REPARSE_POINT_NOT_SUPPORTED = -1978335149 PORTABLE_PACKAGE_ALREADY_EXISTS = -1978335148 PORTABLE_SYMLINK_PATH_IS_DIRECTORY = -1978335147 INSTALLER_PROHIBITS_ELEVATION = -1978335146 PORTABLE_UNINSTALL_FAILED = -1978335145 ARP_VERSION_VALIDATION_FAILED = -1978335144 UNSUPPORTED_ARGUMENT = -1978335143 BIND_WITH_EMBEDDED_NULL = -1978335142 NESTEDINSTALLER_NOT_FOUND = -1978335141 EXTRACT_ARCHIVE_FAILED = -1978335140 NESTEDINSTALLER_INVALID_PATH = -1978335139 PINNED_CERTIFICATE_MISMATCH = -1978335138 INSTALL_LOCATION_REQUIRED = -1978335137 ARCHIVE_SCAN_FAILED = -1978335136 PACKAGE_ALREADY_INSTALLED = -1978335135 PIN_ALREADY_EXISTS = -1978335134 PIN_DOES_NOT_EXIST = -1978335133 CANNOT_OPEN_PINNING_INDEX = -1978335132 MULTIPLE_INSTALL_FAILED = -1978335131 MULTIPLE_UNINSTALL_FAILED = -1978335130 NOT_ALL_QUERIES_FOUND_SINGLE = -1978335129 PACKAGE_IS_PINNED = -1978335128 PACKAGE_IS_STUB = -1978335127 APPTERMINATION_RECEIVED = -1978335126 DOWNLOAD_DEPENDENCIES = -1978335125 DOWNLOAD_COMMAND_PROHIBITED = -1978335124 SERVICE_UNAVAILABLE = -1978335123 RESUME_ID_NOT_FOUND = -1978335122 CLIENT_VERSION_MISMATCH = -1978335121 INVALID_RESUME_STATE = -1978335120 CANNOT_OPEN_CHECKPOINT_INDEX = -1978335119 RESUME_LIMIT_EXCEEDED = -1978335118 INVALID_AUTHENTICATION_INFO = -1978335117 AUTHENTICATION_TYPE_NOT_SUPPORTED = -1978335116 AUTHENTICATION_FAILED = -1978335115 AUTHENTICATION_INTERACTIVE_REQUIRED = -1978335114 AUTHENTICATION_CANCELLED_BY_USER = -1978335113 AUTHENTICATION_INCORRECT_ACCOUNT = -1978335112 NO_REPAIR_INFO_FOUND = -1978335111 REPAIR_NOT_APPLICABLE = -1978335110 EXEC_REPAIR_FAILED = -1978335109 REPAIR_NOT_SUPPORTED = -1978335108 ADMIN_CONTEXT_REPAIR_PROHIBITED = -1978335107 SQLITE_CONNECTION_TERMINATED = -1978335106 DISPLAYCATALOG_API_FAILED = -1978335105 NO_APPLICABLE_DISPLAYCATALOG_PACKAGE = -1978335104 SFSCLIENT_API_FAILED = -1978335103 NO_APPLICABLE_SFSCLIENT_PACKAGE = -1978335102 LICENSING_API_FAILED = -1978335101 INSTALL_PACKAGE_IN_USE = -1978334975 INSTALL_INSTALL_IN_PROGRESS = -1978334974 INSTALL_FILE_IN_USE = -1978334973 INSTALL_MISSING_DEPENDENCY = -1978334972 INSTALL_DISK_FULL = -1978334971 INSTALL_INSUFFICIENT_MEMORY = -1978334970 INSTALL_NO_NETWORK = -1978334969 INSTALL_CONTACT_SUPPORT = -1978334968 INSTALL_REBOOT_REQUIRED_TO_FINISH = -1978334967 INSTALL_REBOOT_REQUIRED_FOR_INSTALL = -1978334966 INSTALL_REBOOT_INITIATED = -1978334965 INSTALL_CANCELLED_BY_USER = -1978334964 INSTALL_ALREADY_INSTALLED = -1978334963 INSTALL_DOWNGRADE = -1978334962 INSTALL_BLOCKED_BY_POLICY = -1978334961 INSTALL_DEPENDENCIES = -1978334960 INSTALL_PACKAGE_IN_USE_BY_APPLICATION = -1978334959 INSTALL_INVALID_PARAMETER = -1978334958 INSTALL_SYSTEM_NOT_SUPPORTED = -1978334957 INSTALL_UPGRADE_NOT_SUPPORTED = -1978334956 INVALID_CONFIGURATION_FILE = -1978286079 INVALID_YAML = -1978286078 INVALID_FIELD_TYPE = -1978286077 UNKNOWN_CONFIGURATION_FILE_VERSION = -1978286076 SET_APPLY_FAILED = -1978286075 DUPLICATE_IDENTIFIER = -1978286074 MISSING_DEPENDENCY = -1978286073 DEPENDENCY_UNSATISFIED = -1978286072 ASSERTION_FAILED = -1978286071 MANUALLY_SKIPPED = -1978286070 WARNING_NOT_ACCEPTED = -1978286069 SET_DEPENDENCY_CYCLE = -1978286068 INVALID_FIELD_VALUE = -1978286067 MISSING_FIELD = -1978286066 TEST_FAILED = -1978286065 TEST_NOT_RUN = -1978286064 GET_FAILED = -1978286063 UNIT_NOT_INSTALLED = -1978285823 UNIT_NOT_FOUND_REPOSITORY = -1978285822 UNIT_MULTIPLE_MATCHES = -1978285821 UNIT_INVOKE_GET = -1978285820 UNIT_INVOKE_TEST = -1978285819 UNIT_INVOKE_SET = -1978285818 UNIT_MODULE_CONFLICT = -1978285817 UNIT_IMPORT_MODULE = -1978285816 UNIT_INVOKE_INVALID_RESULT = -1978285815 UNIT_SETTING_CONFIG_ROOT = -1978285808 UNIT_IMPORT_MODULE_ADMIN = -1978285807 NOT_SUPPORTED_BY_PROCESSOR = -1978285806 } #----------------------------------------------------------------------------- # # MARK: Convert-ADTArrayToRegexCaptureGroup # #----------------------------------------------------------------------------- function Convert-ADTArrayToRegexCaptureGroup { <# .SYNOPSIS Accepts one or more strings and converts the results into a regex capture group. .DESCRIPTION This function accepts one or more strings and converts the results into a regex capture group. .PARAMETER InputObject One or more string objects to parse and return as a regex capture group. .INPUTS System.String. Convert-ADTArrayToRegexCaptureGroup accepts accepts one or more string objects for returning as a regex capture group. .OUTPUTS System.String. Convert-ADTArrayToRegexCaptureGroup returns a string object of the concatenated input as a regex capture group. .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [AllowNull()] [System.String[]]$InputObject ) begin { # Open collector to hold escaped and parsed values. $items = [System.Collections.Generic.List[System.String]]::new() } process { # Process incoming data and store in the collector. foreach ($item in $InputObject.Where({ ![System.String]::IsNullOrWhiteSpace($_) })) { $items.Add([System.Text.RegularExpressions.Regex]::Escape($item)) } } end { # Return collected strings as a regex capture group. if ($items.Count) { return "($([System.String]::Join('|', $items)))" } } } #----------------------------------------------------------------------------- # # MARK: Convert-ADTFunctionParamsToArgArray # #----------------------------------------------------------------------------- function Convert-ADTFunctionParamsToArgArray { <# .SYNOPSIS Converts the provided parameter metadata into an argument array for applications, with presets for MSI, Dell Command | Update, and WinGet. .DESCRIPTION This function accepts parameter metadata and with this, the parameter set name and a help message tag, converts the parameters into an array of arguments for applications. There are presets available for MSI, Dell Command | Update, WinGet, and PnpUtil, or a completely custom arrangement can be accomodated. .PARAMETER BoundParameters A hashtable of parameters to process. .PARAMETER Invocation The script or function's InvocationInfo ($MyInvocation) to process. .PARAMETER ParameterSetName The ParameterSetName to use as a filter against the Invocation's parameters. .PARAMETER HelpMessage The HelpMessage field to use as a filter against the Invocation's parameters. .PARAMETER Exclude One or more parameter names to exclude from the results. .PARAMETER Ordered Instructs that the returned parameters are in the exact order they're read from the BoundParameters or Invocation. .PARAMETER Preset The preset of which to use when generating an argument array. Current presets are MSI, Dell Command | Update, WinGet, PnpUtil, and PowerShell. .PARAMETER ArgValSeparator For non-preset modes, the separator between an argument's name and value. .PARAMETER ArgPrefix For non-preset modes, the prefix to apply to an argument's name. .PARAMETER ValueWrapper For non-preset modes, what, if anything, to use as characters to wrap around the value (e.g. --ArgName="Value"). .PARAMETER MultiValDelimiter For non-preset modes, how to handle parameters where their value is an array of data. .INPUTS System.Collections.IDictionary. Convert-ADTFunctionParamsToArgArray can accept one or more IDictionary objects for processing. System.Management.Automation.InvocationInfo. Convert-ADTFunctionParamsToArgArray can accept one or more InvocationInfo objects for processing. .OUTPUTS System.String[]. Convert-ADTFunctionParamsToArgArray returns one or more string objects representing the converted parameters. .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'ParameterSetName', Justification = "This parameter is used within delegates that PSScriptAnalyzer has no visibility of. See https://github.com/PowerShell/PSScriptAnalyzer/issues/1472 for more details.")] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'HelpMessage', Justification = "This parameter is used within delegates that PSScriptAnalyzer has no visibility of. See https://github.com/PowerShell/PSScriptAnalyzer/issues/1472 for more details.")] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Ordered', Justification = "This parameter is used within delegates that PSScriptAnalyzer has no visibility of. See https://github.com/PowerShell/PSScriptAnalyzer/issues/1472 for more details.")] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'MultiValDelimiter', Justification = "This parameter is used within delegates that PSScriptAnalyzer has no visibility of. See https://github.com/PowerShell/PSScriptAnalyzer/issues/1472 for more details.")] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ParameterSetName = 'BoundParametersPreset', ValueFromPipeline = $true)] [Parameter(Mandatory = $true, ParameterSetName = 'BoundParametersCustom', ValueFromPipeline = $true)] [AllowEmptyCollection()] [System.Collections.IDictionary]$BoundParameters, [Parameter(Mandatory = $true, ParameterSetName = 'InvocationPreset', HelpMessage = 'Primary parameter', ValueFromPipeline = $true)] [Parameter(Mandatory = $true, ParameterSetName = 'InvocationCustom', HelpMessage = 'Primary parameter', ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] [System.Management.Automation.InvocationInfo]$Invocation, [Parameter(Mandatory = $false, ParameterSetName = 'InvocationPreset', HelpMessage = 'Primary parameter')] [Parameter(Mandatory = $false, ParameterSetName = 'InvocationCustom', HelpMessage = 'Primary parameter')] [ValidateNotNullOrEmpty()] [System.String]$ParameterSetName, [Parameter(Mandatory = $false, ParameterSetName = 'InvocationPreset', HelpMessage = 'Primary parameter')] [Parameter(Mandatory = $false, ParameterSetName = 'InvocationCustom', HelpMessage = 'Primary parameter')] [ValidateNotNullOrEmpty()] [System.String]$HelpMessage, [Parameter(Mandatory = $false, ParameterSetName = 'BoundParametersPreset', HelpMessage = 'Primary parameter')] [Parameter(Mandatory = $false, ParameterSetName = 'BoundParametersCustom', HelpMessage = 'Primary parameter')] [Parameter(Mandatory = $false, ParameterSetName = 'InvocationPreset', HelpMessage = 'Primary parameter')] [Parameter(Mandatory = $false, ParameterSetName = 'InvocationCustom', HelpMessage = 'Primary parameter')] [ValidateNotNullOrEmpty()] [System.String[]]$Exclude, [Parameter(Mandatory = $false, ParameterSetName = 'InvocationPreset', HelpMessage = 'Primary parameter')] [Parameter(Mandatory = $false, ParameterSetName = 'InvocationCustom', HelpMessage = 'Primary parameter')] [System.Management.Automation.SwitchParameter]$Ordered, [Parameter(Mandatory = $true, ParameterSetName = 'BoundParametersPreset')] [Parameter(Mandatory = $true, ParameterSetName = 'InvocationPreset')] [ValidateSet('MSI', 'WinGet', 'DellCommandUpdate', 'PnpUtil', 'PowerShell')] [System.String]$Preset, [Parameter(Mandatory = $true, ParameterSetName = 'BoundParametersCustom')] [Parameter(Mandatory = $true, ParameterSetName = 'InvocationCustom')] [ValidateSet(' ', '=', "`n")] [System.String]$ArgValSeparator, [Parameter(Mandatory = $false, ParameterSetName = 'BoundParametersCustom')] [Parameter(Mandatory = $false, ParameterSetName = 'InvocationCustom')] [ValidateSet('-', '--', '/')] [System.String]$ArgPrefix, [Parameter(Mandatory = $false, ParameterSetName = 'BoundParametersCustom')] [Parameter(Mandatory = $false, ParameterSetName = 'InvocationCustom')] [ValidateSet("'", '"')] [System.String]$ValueWrapper, [Parameter(Mandatory = $false, ParameterSetName = 'BoundParametersPreset')] [Parameter(Mandatory = $false, ParameterSetName = 'InvocationPreset')] [Parameter(Mandatory = $false, ParameterSetName = 'BoundParametersCustom')] [Parameter(Mandatory = $false, ParameterSetName = 'InvocationCustom')] [ValidateSet(',', '|')] [System.String]$MultiValDelimiter = ',' ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState # Set up regex for properly trimming lines. Yes, reflection == long lines. $invalidends = "$($MyInvocation.MyCommand.Parameters.Values.GetEnumerator().Where({$_.Name.Equals('ArgValSeparator')}).Attributes.Where({$_ -is [System.Management.Automation.ValidateSetAttribute]}).ValidValues | & $Script:CommandTable.'Convert-ADTArrayToRegexCaptureGroup')+$" $nullvalues = "\s$($MyInvocation.MyCommand.Parameters.Values.GetEnumerator().Where({$_.Name.Equals('ValueWrapper')}).Attributes.Where({$_ -is [System.Management.Automation.ValidateSetAttribute]}).ValidValues | & $Script:CommandTable.'Convert-ADTArrayToRegexCaptureGroup'){2}$" # Set up the string for formatting. $string = switch ($Preset) { MSI { "{0}=`"{1}`""; break } WinGet { "--{0}`n{1}"; break } DellCommandUpdate { "-{0}={1}"; break } PnpUtil { "/{0}`n{1}"; break } PowerShell { "-{0}`n`"{1}`""; break } default { "$($ArgPrefix){0}$($ArgValSeparator)$($ValueWrapper){1}$($ValueWrapper)"; break } } # Persistent scriptblocks stored in RAM for Convert-ADTFunctionParamsToArgArray. $script = if ($Preset -eq 'MSI') { { # For switches, we want to convert the $true/$false into 1/0 respectively. if ($_.Value -isnot [System.Management.Automation.SwitchParameter]) { [System.String]::Format($string, $_.Key.ToUpper(), $_.Value -join $MultiValDelimiter).Split("`n").Trim() } else { [System.String]::Format($string, $_.Key.ToUpper(), [System.UInt32][System.Boolean]$_.Value).Split("`n").Trim() } } } else { { # For switches, we only want true switches, and we drop the $true value entirely. $notswitch = $_.Value -isnot [System.Management.Automation.SwitchParameter] if ($notswitch -or $_.Value) { $name = if ($Preset -eq 'PowerShell') { $_.Key } else { $_.Key.ToLower() } $value = if ($notswitch) { $_.Value -join $MultiValDelimiter } [System.String]::Format($string, $name, $value).Split("`n").Trim() -replace $nullvalues } } } # Amend exclusions with default parameter values. $Exclude = $( $Exclude [System.Management.Automation.PSCmdlet]::CommonParameters [System.Management.Automation.PSCmdlet]::OptionalCommonParameters ) } process { try { try { # If we're processing an invocation, get its bound parameters as required. if ($Invocation) { $bpdvParams = & $Script:CommandTable.'Get-ADTBoundParametersAndDefaultValues' -Invocation $MyInvocation -HelpMessage 'Primary parameter' $BoundParameters = & $Script:CommandTable.'Get-ADTBoundParametersAndDefaultValues' @bpdvParams } # Process the parameters into an argument array and return to the caller. return $BoundParameters.GetEnumerator().Where({ $Exclude -notcontains $_.Key }).ForEach($script) -replace $invalidends | & $Script:CommandTable.'Where-Object' { ![System.String]::IsNullOrWhiteSpace($_) } } catch { # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to convert the provided input to an argument array." } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Convert-ADTWinGetQueryOutput # #----------------------------------------------------------------------------- function Convert-ADTWinGetQueryOutput { [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String[]]$WinGetOutput ) # Process each collected line into an object. try { $WinGetOutput.Trim().TrimEnd('.') | & { begin { # Define variables for heading data that'll be the first line via the pipe. $listHeading = $headIndices = $null } process { if ($_ -notmatch '^\w+') { return } # Use our first valid line to set up the keys for each property. if (!$listHeading) { # Get all headings and the indices from the output. $listHeading = $_ -split '\s+' $headIndices = $($listHeading | & { process { $args[0].IndexOf($_) } } $_; 10000) return } # Establish hashtable to hold contents we're converting. $obj = [ordered]@{} # Begin conversion and return object to the pipeline. for ($i = 0; $i -lt $listHeading.Length; $i++) { $thisi = [System.Math]::Min($headIndices[$i], $_.Length) $nexti = [System.Math]::Min($headIndices[$i + 1], $_.Length) $value = $_.Substring($thisi, $nexti - $thisi).Trim() $obj.Add($listHeading[$i], $(if (![System.String]::IsNullOrWhiteSpace($value)) { $value })) } return [pscustomobject]$obj } } } catch { $naerParams = @{ Exception = [System.IO.InvalidDataException]::new("Failed to parse provided WinGet output.", $_.Exception) Category = [System.Management.Automation.ErrorCategory]::InvalidResult ErrorId = 'WinGetListOutputParseFailure' TargetObject = $WinGetOutput RecommendedAction = "Please review the WinGet output manually, then try again." } throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams) } } #----------------------------------------------------------------------------- # # MARK: Get-ADTRedirectedUri # #----------------------------------------------------------------------------- function Get-ADTRedirectedUri { <# .SYNOPSIS Returns the resolved URI from the provided permalink. .DESCRIPTION This function gets the resolved/redirected URI from the provided input and returns it to the caller. .PARAMETER Uri The URL that requires redirection resolution. .PARAMETER Headers Any headers that need to be provided for URI redirection resolution. .INPUTS None You cannot pipe objects to this function. .OUTPUTS System.Uri Get-ADTRedirectedUri returns a Uri of the resolved/redirected URI. .EXAMPLE Get-ADTRedirectedUri -Uri https://aka.ms/getwinget Returns the absolute URI for the specified short link, e.g. https://github.com/microsoft/winget-cli/releases/download/v1.8.1911/Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [CmdletBinding()] [OutputType([System.Uri])] param ( [Parameter(Mandatory = $true)] [ValidateScript({ if (![System.Uri]::IsWellFormedUriString($_.AbsoluteUri, [System.UriKind]::Absolute)) { $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTValidateScriptErrorRecord' -ParameterName Uri -ProvidedValue $_ -ExceptionMessage 'The specified input is not a valid Uri.')) } return !!$_ })] [System.Uri]$Uri, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.Collections.IDictionary]$Headers = @{ Accept = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' } ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } process { try { try { # Create web request. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Retrieving the redirected URI for [$Uri]." $webReq = [System.Net.WebRequest]::Create($Uri) $webReq.AllowAutoRedirect = $false $Headers.GetEnumerator() | & { process { $webReq.($_.Key) = $_.Value } } # Get a response and close it out. $reqRes = $webReq.GetResponse() $resLoc = $reqRes.GetResponseHeader('Location') $reqRes.Close() # If $resLoc is empty, return the provided URI so something is returned to the caller. if (![System.String]::IsNullOrWhiteSpace($resLoc)) { $Uri = $resLoc } # Return the redirected URI to the caller. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Retrieved redirected URI [$Uri] from the provided input." return $Uri } catch { # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to determine the redirected URI for [$Uri]." } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Get-ADTUriFileName # #----------------------------------------------------------------------------- function Get-ADTUriFileName { <# .SYNOPSIS Returns the filename of the provided URI. .DESCRIPTION This function gets the filename of the provided URI from the provided input and returns it to the caller. .PARAMETER Uri The URL that to retrieve the filename from. .INPUTS None You cannot pipe objects to this function. .OUTPUTS System.String Get-ADTUriFileName returns a string value of the URI's filename. .EXAMPLE Get-ADTUriFileName -Uri https://aka.ms/getwinget Returns the filename for the specified URI, redirected or otherwise. e.g. Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [ValidateScript({ if (![System.Uri]::IsWellFormedUriString($_.AbsoluteUri, [System.UriKind]::Absolute)) { $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTValidateScriptErrorRecord' -ParameterName Uri -ProvidedValue $_ -ExceptionMessage 'The specified input is not a valid Uri.')) } return !!$_ })] [System.Uri]$Uri ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } process { try { try { # Re-write the URI to factor in any redirections. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Retrieving the file name for URI [$Uri]." $Uri = & $Script:CommandTable.'Get-ADTRedirectedUri' -Uri $Uri # Create web request. $webReq = [System.Net.WebRequest]::Create($Uri) $webReq.AllowAutoRedirect = $false $webReq.Accept = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' # Get a response and close it out. $reqRes = $webReq.GetResponse() $resCnt = $reqRes.GetResponseHeader('Content-Disposition') $reqRes.Close() # If $resCnt is empty, the provided URI likely has the filename in it. $filename = if (!$resCnt.Contains('filename')) { & $Script:CommandTable.'Remove-ADTInvalidFileNameChars' -Name $Uri.ToString().Split('/')[-1] } else { & $Script:CommandTable.'Remove-ADTInvalidFileNameChars' -Name $resCnt.Split(';').Trim().Where({ $_.StartsWith('filename=') }).Split('=')[-1] } # Return the determined filename to the caller. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Resolved filename [$filename] from the provided URI." return $filename } catch { # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to determine the filename for URI [$Uri]." } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Get-ADTWinGetHashMismatchArgumentList # #----------------------------------------------------------------------------- function Get-ADTWinGetHashMismatchArgumentList { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = "This function is appropriately named and we don't need PSScriptAnalyzer telling us otherwise.")] [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [pscustomobject]$Manifest, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [pscustomobject]$Installer, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$FilePath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$LogFile ) # Internal filter to process manifest install switches. filter Get-ADTWinGetManifestInstallSwitches { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = "This function is appropriately named and we don't need PSScriptAnalyzer telling us otherwise.")] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] [pscustomobject]$InputObject, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$Type ) # Test whether the piped object has InstallerSwitches and it's not null. if (($InputObject.PSObject.Properties.Name -notcontains 'InstallerSwitches') -or ($null -eq $InputObject.InstallerSwitches)) { return } # Return the requested type. This will be null if its not available. return $InputObject.InstallerSwitches.PSObject.Properties | & $Script:CommandTable.'Where-Object' { $_.Name -eq $Type } | & $Script:CommandTable.'Select-Object' -ExpandProperty Value } # Internal function to return default install switches based on type. function Get-ADTDefaultKnownSwitches { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = "This function is appropriately named and we don't need PSScriptAnalyzer telling us otherwise.")] [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$InstallerType ) # Switch on the installer type and return an array of strings for the args. switch -Regex ($InstallerType) { '^(Burn|Wix|Msi)$' { "/quiet" "/norestart" "/log `"$LogFile`"" break } '^Nullsoft$' { "/S" break } '^Inno$' { "/VERYSILENT" "/NORESTART" "/LOG=`"$LogFile`"" break } default { $naerParams = @{ Exception = [System.InvalidOperationException]::new("The installer type '$_' is unsupported.") Category = [System.Management.Automation.ErrorCategory]::InvalidData ErrorId = 'WinGetInstallerTypeUnknown' TargetObject = $_ RecommendedAction = "Please report the installer type to the project's maintainer for further review." } $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)) } } } # Add standard msiexec.exe args. if ($FilePath.EndsWith('msi')) { "/i `"$FilePath`"" } # If we're not overriding, get silent switches from manifest and $Custom if we can. if (!$Override) { # Try to get switches from the installer, then the manifest, then by what the installer is, either from the installer or the manifest. if ($switches = $Installer | Get-ADTWinGetManifestInstallSwitches -Type Silent) { # First check the installer array for a silent switch. $switches & $Script:CommandTable.'Write-ADTLogEntry' -Message "Using Silent switches from the manifest's installer data." } elseif ($switches = $Manifest | Get-ADTWinGetManifestInstallSwitches -Type Silent) { # Fall back to the manifest itself. $switches & $Script:CommandTable.'Write-ADTLogEntry' -Message "Using Silent switches from the manifest's top level." } elseif ($instType = $Installer | & $Script:CommandTable.'Get-ADTWinGetHashMismatchInstallerType') { # We have no defined switches, try to determine switches from the installer's defined type. Get-ADTDefaultKnownSwitches -InstallerType $instType & $Script:CommandTable.'Write-ADTLogEntry' -Message "Using default switches for the manifest installer's installer type ($instType)." } elseif ($instType = $Manifest | & $Script:CommandTable.'Get-ADTWinGetHashMismatchInstallerType') { # The installer array doesn't define a type, see if the manifest itself does. Get-ADTDefaultKnownSwitches -InstallerType $instType & $Script:CommandTable.'Write-ADTLogEntry' -Message "Using default switches for the manifest's installer type ($instType)." } elseif ($switches = $Installer | Get-ADTWinGetManifestInstallSwitches -Type SilentWithProgress) { # We're shit out of luck... circle back and see if we have _anything_ we can use. $switches & $Script:CommandTable.'Write-ADTLogEntry' -Message "Using SilentWithProgress switches from the manifest's installer data." } elseif ($switches = $Manifest | Get-ADTWinGetManifestInstallSwitches -Type SilentWithProgress) { # Last-ditch effort. It's this or bust. $switches & $Script:CommandTable.'Write-ADTLogEntry' -Message "Using SilentWithProgress switches from the manifest's top level." } else { $naerParams = @{ Exception = [System.InvalidOperationException]::new("Unable to determine how to silently install the application.") Category = [System.Management.Automation.ErrorCategory]::InvalidResult ErrorId = 'WinGetInstallerTypeUnknown' TargetObject = $PSBoundParameters RecommendedAction = "Please report this issue to the project's maintainer for further review." } $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)) } # Append any custom switches the caller has provided. if ($Custom) { $Custom } } else { # Override replaces anything the manifest provides. $Override } } #----------------------------------------------------------------------------- # # MARK: Get-ADTWinGetHashMismatchDownload # #----------------------------------------------------------------------------- function Get-ADTWinGetHashMismatchDownload { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [pscustomobject]$Installer ) & $Script:CommandTable.'Write-ADTLogEntry' -Message "Downloading [$($Installer.InstallerUrl)], please wait..." try { # Download WinGet app and store path to binary. $wgFilePath = "$([System.IO.Directory]::CreateDirectory("$([System.IO.Path]::GetTempPath())$(& $Script:CommandTable.'Get-Random')").FullName)\$(& $Script:CommandTable.'Get-ADTUriFileName' -Uri $Installer.InstallerUrl)" & $Script:CommandTable.'Invoke-ADTWebDownload' -Uri $Installer.InstallerUrl -OutFile $wgFilePath # If downloaded file is a zip, we need to expand it and modify our file path before returning. if ($wgFilePath -match 'zip$') { & $Script:CommandTable.'Write-ADTLogEntry' -Message "Downloaded installer is a zip file, expanding its contents." & $Script:CommandTable.'Expand-Archive' -LiteralPath $wgFilePath -DestinationPath ([System.IO.Path]::GetTempPath()) -Force $wgFilePath = "$([System.IO.Path]::GetTempPath())$($Installer.NestedInstallerFiles.RelativeFilePath)" } return $wgFilePath } catch { $naerParams = @{ Exception = [System.InvalidOperationException]::new("Failed to download [$($Installer.InstallerUrl)].", $_.Exception) Category = [System.Management.Automation.ErrorCategory]::InvalidOperation ErrorId = 'WinGetInstallerDownloadFailure' RecommendedAction = "Please verify the installer's URI is valid, then try again." } $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)) } } #----------------------------------------------------------------------------- # # MARK: Get-ADTWinGetHashMismatchExitCodes # #----------------------------------------------------------------------------- function Get-ADTWinGetHashMismatchExitCodes { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = "This function is appropriately named and we don't need PSScriptAnalyzer telling us otherwise.")] [CmdletBinding()] [OutputType([System.Int32])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [pscustomobject]$Manifest, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [pscustomobject]$Installer, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$FilePath ) # Try to get switches from the installer, then the manifest, then by whatever known defaults we have. if ($Installer.PSObject.Properties.Name.Contains('InstallerSuccessCodes')) { return $Installer.InstallerSuccessCodes } elseif ($Manifest.PSObject.Properties.Name.Contains('InstallerSuccessCodes')) { return $Manifest.InstallerSuccessCodes } else { # Zero is valid for everything. 0 # Factor in two msiexec.exe-specific exit codes. if ($FilePath.EndsWith('msi')) { 1641 # Machine needs immediate reboot. 3010 # Reboot should be rebooted. } } } #----------------------------------------------------------------------------- # # MARK: Get-ADTWinGetHashMismatchInstaller # #----------------------------------------------------------------------------- function Get-ADTWinGetHashMismatchInstaller { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [pscustomobject]$Manifest, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$Scope, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Architecture, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$InstallerType ) # Get correct installation data from the manifest based on scope and system architecture. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Processing installer metadata from the package manifest." $systemArch = $Script:ADT.ArchLookupTable.([PSADT.OperatingSystem.OSHelper]::GetArchitecture()) $nativeArch = $Manifest.Installers.Architecture -contains $systemArch $cultureName = [System.Globalization.CultureInfo]::CurrentUICulture.Name $wgInstaller = $Manifest.Installers | & $Script:CommandTable.'Where-Object' { (!$_.PSObject.Properties.Name.Contains('Scope') -or ($_.Scope -eq $Scope)) -and (!$_.PSObject.Properties.Name.Contains('InstallerLocale') -or ($_.InstallerLocale -eq $cultureName)) -and (!$InstallerType -or (($instType = $_ | & $Script:CommandTable.'Get-ADTWinGetHashMismatchInstallerType') -and ($instType -eq $InstallerType))) -and ($_.Architecture.Equals($Architecture) -or ($haveArch = $_.Architecture -eq $systemArch) -or (!$haveArch -and !$nativeArch)) } # Validate the output. The yoda notation is to keep PSScriptAnalyzer happy. if ($null -eq $wgInstaller) { # We found nothing and therefore can't continue. $naerParams = @{ Exception = [System.InvalidOperationException]::new("Error occurred while processing installer metadata from the package's manifest.") Category = [System.Management.Automation.ErrorCategory]::InvalidResult ErrorId = 'WinGetManifestInstallerResultNull' TargetObject = $wgInstaller RecommendedAction = "Please review the package's installer metadata within the manifest, then try again." } $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)) } elseif ($wgInstaller -is [System.Collections.IEnumerable]) { # We got multiple values. Get all unique installer types from the metadata and check for uniqueness. if (!$wgInstaller.Count.Equals((($wgInstTypes = $wgInstaller | & $Script:CommandTable.'Get-ADTWinGetHashMismatchInstallerType' | & $Script:CommandTable.'Select-Object' -Unique) | & $Script:CommandTable.'Measure-Object').Count)) { # Something's gone wrong as we've got duplicate installer types. $naerParams = @{ Exception = [System.InvalidOperationException]::new("Error determining correct installer metadata from the package's manifest.") Category = [System.Management.Automation.ErrorCategory]::InvalidResult ErrorId = 'WinGetManifestInstallerResultInconclusive' TargetObject = $wgInstaller RecommendedAction = "Please review the package's installer metadata within the manifest, then try again." } $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)) } # Installer types were unique, just return the first one and hope for the best. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Found installer types ['$([System.String]::Join("', '", $wgInstTypes))']; using [$($wgInstTypes[0])] metadata." $wgInstaller = $wgInstaller | & $Script:CommandTable.'Where-Object' { ($_ | & $Script:CommandTable.'Get-ADTWinGetHashMismatchInstallerType').Equals($wgInstTypes[0]) } } # Return installer metadata to the caller. return $wgInstaller } #----------------------------------------------------------------------------- # # MARK: Get-ADTWinGetHashMismatchInstallerType # #----------------------------------------------------------------------------- filter Get-ADTWinGetHashMismatchInstallerType { if ($_.PSObject.Properties.Name.Contains('NestedInstallerType')) { return $_.NestedInstallerType } elseif ($_.PSObject.Properties.Name.Contains('InstallerType')) { return $_.InstallerType } } #----------------------------------------------------------------------------- # # MARK: Get-ADTWinGetHashMismatchManifest # #----------------------------------------------------------------------------- function Get-ADTWinGetHashMismatchManifest { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$Id, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$Version ) # Set up vars and get package manifest. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Downloading and parsing the package manifest from GitHub." try { $wgUriBase = "https://raw.githubusercontent.com/microsoft/winget-pkgs/master/manifests/{0}/{1}/{2}/{3}.installer.yaml" $wgPkgsUri = [System.String]::Format($wgUriBase, $Id.Substring(0, 1).ToLower(), $Id.Replace('.', '/'), $Version, $Id) $wgPkgYaml = & $Script:CommandTable.'Invoke-RestMethod' -UseBasicParsing -Uri $wgPkgsUri -Verbose:$false $wgManifest = $wgPkgYaml | & $Script:CommandTable.'ConvertFrom-Yaml' return $wgManifest } catch { $naerParams = @{ Exception = [System.IO.InvalidDataException]::new("Failed to download or parse the package manifest from GitHub.", $_.Exception) Category = [System.Management.Automation.ErrorCategory]::InvalidResult ErrorId = 'WinGetManifestParseFailure' RecommendedAction = "Please review the package's manifest, then try again." } $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)) } } #----------------------------------------------------------------------------- # # MARK: Get-ADTWinGetPath # #----------------------------------------------------------------------------- function Get-ADTWinGetPath { [CmdletBinding()] [OutputType([System.IO.FileInfo])] param ( ) # For the system user, get the path from Program Files directly. For some systems, we can't rely on the # output of Get-AppxPackage as it'll update, but Get-AppxPackage won't reflect the new path fast enough. $wingetPath = if ($Script:ADT.RunningAsSystem) { & $Script:CommandTable.'Get-ChildItem' -Path "$([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::ProgramFiles))\WindowsApps\Microsoft.DesktopAppInstaller*\winget.exe" | & $Script:CommandTable.'Sort-Object' -Descending | & $Script:CommandTable.'Select-Object' -First 1 } elseif (($wingetCommand = & $Script:CommandTable.'Get-Command' -Name winget.exe -ErrorAction Ignore)) { $wingetCommand.Source } elseif ([System.IO.File]::Exists(($appxPath = "$(& $Script:CommandTable.'Get-AppxPackage' -Name Microsoft.DesktopAppInstaller -AllUsers:$Script:ADT.RunningAsSystem | & $Script:CommandTable.'Sort-Object' -Property Version -Descending | & $Script:CommandTable.'Select-Object' -ExpandProperty InstallLocation -First 1)\winget.exe"))) { $appxPath } # Throw if we didn't find a WinGet path. if (!$wingetPath) { $naerParams = @{ Exception = [System.IO.FileNotFoundException]::new("Failed to find a valid path to winget.exe on this system.") Category = [System.Management.Automation.ErrorCategory]::InvalidResult ErrorId = 'MicrosoftDesktopAppInstallerVersionError' RecommendedAction = "Please invoke [Repair-ADTWinGetPackageManager], then try again." } $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)) } # Return the found path to the caller. return [System.IO.FileInfo]$wingetPath } #----------------------------------------------------------------------------- # # MARK: Invoke-ADTWebDownload # #----------------------------------------------------------------------------- function Invoke-ADTWebDownload { <# .SYNOPSIS Wraps around Invoke-WebRequest to provide logging and retry support. .DESCRIPTION This function allows callers to download files as part of a deployment with logging and retry support. .PARAMETER Uri The URL that to retrieve the file from. .PARAMETER OutFile The path of where to save the file to. .PARAMETER Headers Any headers that need to be provided for file transfer. .PARAMETER Sha256Hash An optional SHA256 reference file hash for download verification. .PARAMETER PassThru Returns the WebResponseObject object from Invoke-WebRequest. .INPUTS None You cannot pipe objects to this function. .OUTPUTS Microsoft.PowerShell.Commands.WebResponseObject Invoke-ADTWebDownload returns the results from Invoke-WebRequest if PassThru is specified. .EXAMPLE Invoke-ADTWebDownload -Uri https://aka.ms/getwinget -OutFile "$($adtSession.DirSupportFiles)\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" Downloads the latest WinGet installer to the SupportFiles directory. .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [CmdletBinding()] [OutputType([Microsoft.PowerShell.Commands.WebResponseObject])] param ( [Parameter(Mandatory = $true)] [ValidateScript({ if (![System.Uri]::IsWellFormedUriString($_.AbsoluteUri, [System.UriKind]::Absolute)) { $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTValidateScriptErrorRecord' -ParameterName Uri -ProvidedValue $_ -ExceptionMessage 'The specified input is not a valid Uri.')) } return !!$_ })] [System.Uri]$Uri, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$OutFile, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.Collections.IDictionary]$Headers = @{ Accept = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' }, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Sha256Hash, [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$PassThru ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } process { try { try { # Commence download and return the result if passing through. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Downloading $Uri." $iwrParams = & $Script:CommandTable.'Get-ADTBoundParametersAndDefaultValues' -Invocation $MyInvocation -Exclude Sha256Hash $iwrResult = & $Script:CommandTable.'Invoke-ADTCommandWithRetries' -Command $Script:CommandTable.'Invoke-WebRequest' -UseBasicParsing @iwrParams -Verbose:$false # Validate the hash if one was provided. if ($PSBoundParameters.ContainsKey('Sha256Hash') -and (($fileHash = & $Script:CommandTable.'Get-FileHash' -LiteralPath $OutFile).Hash -ne $Sha256Hash)) { $naerParams = @{ Exception = [System.BadImageFormatException]::new("The downloaded file has an invalid file hash of [$($fileHash.Hash)].", $OutFile) Category = [System.Management.Automation.ErrorCategory]::InvalidData ErrorId = 'DownloadedFileInvalid' TargetObject = $fileHash RecommendedAction = "Please compare the downloaded file's hash against the provided value and try again." } throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams) } # Return any results from Invoke-WebRequest if we have any and we're passing through. if ($PassThru -and $iwrResult) { return $iwrResult } } catch { # Re-writing the ErrorRecord with Write-Object ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Error downloading setup file(s) from the provided URL of [$Uri]." } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Invoke-ADTWinGetDeploymentOperation # #----------------------------------------------------------------------------- function Invoke-ADTWinGetDeploymentOperation { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateSet('install', 'repair', 'uninstall', 'upgrade')] [System.String]$Action ) dynamicparam { # Define parameter dictionary for returning at the end. $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() # Add in parameters for specific modes. if ($Action -match '^(install|upgrade)$') { if ($Action -eq 'upgrade') { $paramDictionary.Add('Include-Unknown', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Include-Unknown', [System.Management.Automation.SwitchParameter], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.AliasAttribute]::new('IncludeUnknown') ) )) } $paramDictionary.Add('Ignore-Security-Hash', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Ignore-Security-Hash', [System.Management.Automation.SwitchParameter], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.AliasAttribute]::new('AllowHashMismatch') ) )) $paramDictionary.Add('DebugHashMismatch', [System.Management.Automation.RuntimeDefinedParameter]::new( 'DebugHashMismatch', [System.Management.Automation.SwitchParameter], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } ) )) $paramDictionary.Add('Architecture', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Architecture', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateSetAttribute]::new('x86', 'x64', 'arm64') ) )) $paramDictionary.Add('Custom', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Custom', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new() ) )) $paramDictionary.Add('Header', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Header', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new() ) )) $paramDictionary.Add('Installer-Type', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Installer-Type', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateSetAttribute]::new('Inno', 'Wix', 'Msi', 'Nullsoft', 'Zip', 'Msix', 'Exe', 'Burn', 'MSStore', 'Portable') [System.Management.Automation.AliasAttribute]::new('InstallerType') ) )) $paramDictionary.Add('Locale', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Locale', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new() ) )) $paramDictionary.Add('Location', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Location', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new() ) )) $paramDictionary.Add('Override', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Override', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new() ) )) $paramDictionary.Add('Scope', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Scope', [String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateSetAttribute]::new('Any', 'User', 'System', 'UserOrUnknown', 'SystemOrUnknown') ) )) $paramDictionary.Add('Skip-Dependencies', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Skip-Dependencies', [System.Management.Automation.SwitchParameter], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.AliasAttribute]::new('SkipDependencies') ) )) } if ($Action -ne 'repair') { $paramDictionary.Add('Force', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Force', [System.Management.Automation.SwitchParameter], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } ) )) } # Add in parameters used by all actions. $paramDictionary.Add('Query', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Query', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateSetAttribute]::new('Equals', 'EqualsCaseInsensitive') ) )) $paramDictionary.Add('Query', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Query', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new() ) )) $paramDictionary.Add('Id', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Id', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new() ) )) $paramDictionary.Add('Log', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Log', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new() ) )) $paramDictionary.Add('Mode', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Mode', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateSetAttribute]::new('Silent', 'Interactive') ) )) $paramDictionary.Add('Moniker', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Moniker', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new() ) )) $paramDictionary.Add('Name', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Name', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new() ) )) $paramDictionary.Add('Source', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Source', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new() ) )) $paramDictionary.Add('Version', [System.Management.Automation.RuntimeDefinedParameter]::new( 'Version', [System.String], $( [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false } [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new() ) )) # Return the populated dictionary. return $paramDictionary } begin { # Internal function to generate arguments array for WinGet. function Out-ADTWinGetDeploymentArgumentList { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.Collections.Generic.Dictionary[System.String, System.Object]]$BoundParameters, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String[]]$Exclude ) # Ensure the action is also excluded. $PSBoundParameters.Exclude = $('Action'; 'MatchOption'; 'Ignore-Security-Hash'; 'DebugHashMismatch'; $(if ($Exclude) { $Exclude } )) # Output each item for the caller to collect. return $( $Action & $Script:CommandTable.'Convert-ADTFunctionParamsToArgArray' @PSBoundParameters -Preset WinGet if ($PSBoundParameters.ContainsKey('MatchOption') -and ($PSBoundParameters.MatchOption -eq 'Equals')) { '--exact' } '--accept-source-agreements' if ($Action -ne 'Uninstall') { '--accept-package-agreements' } ) } # Define internal scriptblock for invoking WinGet. This is a # scriptblock so Write-ADTLogEntry uses this function's source. $wingetInvoker = { return , [System.String[]](& $wingetPath $wingetArgs 2>&1 | & { begin { $waleParams = @{PassThru = $true } } process { if ($_ -match '^\w+') { $waleParams.Severity = if ($_ -match 'exit code: \d+') { 3 } else { 1 } & $Script:CommandTable.'Write-ADTLogEntry' @waleParams -Message ($_.Trim() -replace '((?<![.:])|:)$', '.') } } }) } # Throw if an id, name, or moniker hasn't been provided. This is done like this # and not via parameter sets because this is what Install-WinGetPackage does. if (!$PSBoundParameters.ContainsKey('Id') -and !$PSBoundParameters.ContainsKey('Name') -and !$PSBoundParameters.ContainsKey('Moniker')) { $naerParams = @{ Exception = [System.ArgumentException]::new("Please specify a package by Id, Name, or Moniker.") Category = [System.Management.Automation.ErrorCategory]::InvalidArgument ErrorId = "WinGet$([System.Globalization.CultureInfo]::CurrentUICulture.TextInfo.ToTitleCase($Action))FilterError" TargetObject = $PSBoundParameters RecommendedAction = "Please specify a package by Id, Name, or Moniker; then try again." } $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)) } # Try to get the path to WinGet before proceeding. try { $wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath' } catch { $PSCmdlet.ThrowTerminatingError($_) } # Get the active ADT session object if one's in play. $adtSession = if (& $Script:CommandTable.'Test-ADTSessionActive') { & $Script:CommandTable.'Get-ADTSession' } # Default the scope to "Machine" for the safety of users. # It's super easy to install user-scoped apps into the SYSTEM # user's account, and it's painful to diagnose/clean up. if (($noScope = !$PSBoundParameters.ContainsKey('Scope'))) { $PSBoundParameters.Add('Scope', 'Machine') } # Most of the time, we're only wanting a WinGet package anyway. # Defaulting to the winget source speeds up operations. if ($PSBoundParameters.ContainsKey('Source')) { try { $null = & $Script:CommandTable.'Get-ADTWinGetSource' -Name $PSBoundParameters.Source } catch { $PSCmdlet.ThrowTerminatingError($_) } } else { $PSBoundParameters.Add('Source', 'winget') } # Add in a default log file if the caller hasn't specified one. if (!$PSBoundParameters.ContainsKey('Log')) { $PSBoundParameters.Log = if ($adtSession) { "$((& $Script:CommandTable.'Get-ADTConfig').Toolkit.LogPath)\$($adtSession.InstallName)_WinGet.log" } else { "$([System.IO.Path]::GetTempPath())Invoke-ADTWinGetOperation_$([System.DateTime]::Now.ToString('O').Split('.')[0].Replace(':', $null))_WinGet.log" } } # Attempt to find the package to install. try { $fawgpParams = @{}; if ($PSBoundParameters.ContainsKey('Id')) { $fawgpParams.Add('Id', $PSBoundParameters.Id) } if ($PSBoundParameters.ContainsKey('Name')) { $fawgpParams.Add('Name', $PSBoundParameters.Name) } if ($PSBoundParameters.ContainsKey('Moniker')) { $fawgpParams.Add('Moniker', $PSBoundParameters.Moniker) } if ($PSBoundParameters.ContainsKey('Source')) { $fawgpParams.Add('Source', $PSBoundParameters.Source) } $wgPackage = & $Script:CommandTable.'Find-ADTWinGetPackage' @fawgpParams -InformationAction SilentlyContinue } catch { $PSCmdlet.ThrowTerminatingError($_) } # Set up arguments array for WinGet. $wingetArgs = Out-ADTWinGetDeploymentArgumentList -BoundParameters $PSBoundParameters # Generate action lookup table for verbage. $actionTranslator = @{ Install = 'Installer' Repair = 'Repair' Uninstall = 'Uninstaller' Upgrade = 'Installer' } } end { # Test whether we're debugging the AllowHashMismatch feature. if (($Action -notmatch '^(install|upgrade)$') -or !$PSBoundParameters.ContainsKey('DebugHashMismatch') -or !$PSBoundParameters.DebugHashMismatch) { # Invoke WinGet and print each non-null line. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Executing [$wingetPath $wingetArgs]." $wingetOutput = & $wingetInvoker # If package isn't found, rerun again without --Scope argument. if (($Global:LASTEXITCODE -eq [ADTWinGetExitCode]::NO_APPLICABLE_INSTALLER) -and $noScope) { & $Script:CommandTable.'Write-ADTLogEntry' -Message "Attempting to execute WinGet again without '--scope' argument." $wingetArgs = Out-ADTWinGetDeploymentArgumentList -BoundParameters $PSBoundParameters -Exclude Scope $wingetOutput = & $wingetInvoker } } else { # Going into bypass mode. Simulate WinGet output for the purpose of getting the app's version later on. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Bypassing WinGet as `-DebugHashMismatch` has been passed. This switch should only be used for debugging purposes." $Global:LASTEXITCODE = [ADTWinGetExitCode]::INSTALLER_HASH_MISMATCH.value__ $PSBoundParameters.'Ignore-Security-Hash' = $true } # If we're bypassing a hash failure, process the WinGet manifest ourselves. if (($Global:LASTEXITCODE -eq [ADTWinGetExitCode]::INSTALLER_HASH_MISMATCH) -and $PSBoundParameters.ContainsKey('Ignore-Security-Hash') -and $PSBoundParameters.'Ignore-Security-Hash') { # The hash failed, however we're forcing an override. Set up default parameters for Get-ADTWinGetHashMismatchInstaller and get started. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Installation failed due to mismatched hash, attempting to override as `-IgnoreHashFailure` has been passed." $gawgaiParams = @{}; if ($PSBoundParameters.ContainsKey('Scope')) { $gawgaiParams.Add('Scope', $PSBoundParameters.Scope) } if ($PSBoundParameters.ContainsKey('Architecture')) { $gawgaiParams.Add('Architecture', $PSBoundParameters.Architecture) } if ($PSBoundParameters.ContainsKey('Installer-Type')) { $gawgaiParams.Add('InstallerType', $PSBoundParameters.'Installer-Type') } # Grab the manifest so we can parse out the installation info as required. $wgAppInfo = [ordered]@{ Manifest = & $Script:CommandTable.'Get-ADTWinGetHashMismatchManifest' -Id $wgPackage.Id -Version $wgPackage.Version } $wgAppInfo.Add('Installer', (& $Script:CommandTable.'Get-ADTWinGetHashMismatchInstaller' @gawgaiParams -Manifest $wgAppInfo.Manifest)) $wgAppInfo.Add('FilePath', (& $Script:CommandTable.'Get-ADTWinGetHashMismatchDownload' -Installer $wgAppInfo.Installer)) # Set up arguments to pass to Start-Process. $spParams = @{ WorkingDirectory = $ExecutionContext.SessionState.Path.CurrentLocation.Path ArgumentList = & $Script:CommandTable.'Get-ADTWinGetHashMismatchArgumentList' @wgAppInfo -LogFile $PSBoundParameters.Log FilePath = $(if ($wgAppInfo.FilePath.EndsWith('msi')) { 'msiexec.exe' } else { $wgAppInfo.FilePath }) PassThru = $true Wait = $true } # Commence installation and test the resulting exit code for success. $wingetOutput = $( & $Script:CommandTable.'Write-ADTLogEntry' -Message "Starting package install..." -PassThru & $Script:CommandTable.'Write-ADTLogEntry' -Message "Executing [$($spParams.FilePath) $($spParams.ArgumentList)]" -PassThru if ((& $Script:CommandTable.'Get-ADTWinGetHashMismatchExitCodes' @wgAppInfo) -notcontains ($Global:LASTEXITCODE = (& $Script:CommandTable.'Start-Process' @spParams).ExitCode)) { & $Script:CommandTable.'Write-ADTLogEntry' -Message "Uninstall failed with exit code: $Global:LASTEXITCODE." -PassThru } else { & $Script:CommandTable.'Write-ADTLogEntry' -Message "Successfully installed." -PassThru } ) } # Get the WinGet package code. If we didn't error out, assume zero as it's all we can do. $wingetPackageErrCode = if (($wingetErrLine = $($wingetOutput -match 'exit code: \d+'))) { [System.Int32]($wingetErrLine -replace '^.+:\s(\d+)\.$', '$1') } else { $Global:LASTEXITCODE } # Generate an exception if we received any failure. $wingetException = if ($wingetErrLine) { [System.Runtime.InteropServices.ExternalException]::new($wingetErrLine, $wingetPackageErrCode) } elseif ($Global:LASTEXITCODE) { # All this bullshit is to change crap like '0x800704c7 : unknown error.' to 'Unknown error.'... $wgErrorDef = if ([System.Enum]::IsDefined([ADTWinGetExitCode], $Global:LASTEXITCODE)) { [ADTWinGetExitCode]$Global:LASTEXITCODE } $wgErrorMsg = [System.Text.RegularExpressions.Regex]::Replace($wingetOutput[-1], '^0x\w{8}\s:\s(\w)', { $args[0].Groups[1].Value.ToUpper() }) [System.ComponentModel.Win32Exception]::new($Global:LASTEXITCODE, "WinGet operation finished with exit code 0x$($Global:LASTEXITCODE.ToString('X'))$(if ($wgErrorDef) {" ($wgErrorDef)"}) [$($wgErrorMsg.TrimEnd('.'))].") } # The WinGet cmdlets don't throw on install failure, but we need to within PSADT. Try # to give as much information as we can to the caller, including installer exit code. if ($wingetException) { # Update the session's exit code if one's in play. if ($adtSession) { $adtSession.SetExitCode($wingetPackageErrCode) } # Throw our determined exception out to the caller to handle. $naerParams = @{ Exception = $wingetException Category = [System.Management.Automation.ErrorCategory]::InvalidResult ErrorId = "WinGetPackage$([System.Globalization.CultureInfo]::CurrentUICulture.TextInfo.ToTitleCase($Action))Failure" TargetObject = $wingetOutput RecommendedAction = "Please review the exit code, then try again." } $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)) } # Construct the return object to the caller. We try to closely match WinGet's output as possible. # According to WinGet: The Windows Package Manager does not support the reboot behavior currently. # https://github.com/microsoft/winget-pkgs/blob/master/doc/manifest/schema/1.6.0/installer.md return [pscustomobject]@{ Id = $wgPackage.Id Name = $wgPackage.Name Source = $PSBoundParameters.Source CorrelationData = [System.String]::Empty ExtendedErrorCode = $wingetException RebootRequired = $Global:LASTEXITCODE.Equals(1641) -or ($Global:LASTEXITCODE.Equals(3010)) Status = if ($wingetPackageErrCode) { "$($Action)Error" } else { 'Ok' } "$($actionTranslator.$Action)ErrorCode" = $wingetPackageErrCode } } } #----------------------------------------------------------------------------- # # MARK: Invoke-ADTWinGetQueryOperation # #----------------------------------------------------------------------------- function Invoke-ADTWinGetQueryOperation { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateSet('list', 'search')] [System.String]$Action, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Query, [Parameter(Mandatory = $false)] [ValidateSet('Equals', 'EqualsCaseInsensitive')] [System.String]$MatchOption, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Command, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.UInt32]$Count, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Id, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Moniker, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Name, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Source, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Tag ) # Try to get the path to WinGet before proceeding. try { $wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath' } catch { $PSCmdlet.ThrowTerminatingError($_) } # Confirm the validity of the provided source. if ($PSBoundParameters.ContainsKey('Source')) { try { $null = & $Script:CommandTable.'Get-ADTWinGetSource' -Name $PSBoundParameters.Source } catch { $PSCmdlet.ThrowTerminatingError($_) } } # Set up arguments array for WinGet. $wingetArgs = $( $Action $PSBoundParameters | & $Script:CommandTable.'Convert-ADTFunctionParamsToArgArray' -Preset WinGet -Exclude Action, MatchOption if ($MatchOption -eq 'Equals') { '--exact' } '--accept-source-agreements' ) # Invoke WinGet and return early if we couldn't find a package. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Finding packages matching input criteria, please wait..." if (($wingetOutput = & $wingetPath $wingetArgs 2>&1 | & { process { if ($_ -match '^\w+') { return $_.Trim() } } }) -match '^No.+package found matching input criteria\.$') { & $Script:CommandTable.'Write-ADTLogEntry' -Message "No package found matching input criteria." return } # Convert the cached output to proper PowerShell objects. $wingetObjects = & $Script:CommandTable.'Convert-ADTWinGetQueryOutput' -WinGetOutput $wingetOutput & $Script:CommandTable.'Write-ADTLogEntry' -Message "Found $(($wingetObjCount = ($wingetObjects | & $Script:CommandTable.'Measure-Object').Count)) package$(if (!$wingetObjCount.Equals(1)) { 's' }) matching input criteria." return $wingetObjects } #----------------------------------------------------------------------------- # # MARK: Repair-ADTWinGetDesktopAppInstaller # #----------------------------------------------------------------------------- function Repair-ADTWinGetDesktopAppInstaller { # Update WinGet to the latest version. Don't rely in 3rd party store API services for this. # https://learn.microsoft.com/en-us/windows/package-manager/winget/#install-winget-on-windows-sandbox & $Script:CommandTable.'Write-ADTLogEntry' -Message "Installing/updating $(($pkgName = "Microsoft.DesktopAppInstaller")) dependency, please wait..." # Define installation file info. $packages = @( @{ Name = 'C++ Desktop Bridge Runtime dependency' Uri = ($uri = [System.Uri]'https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx') FilePath = "$([System.IO.Path]::GetTempPath())$($uri.Segments[-1])" } @{ Name = 'Windows UI Library dependency' Uri = ($uri = [System.Uri]'https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.6/Microsoft.UI.Xaml.2.8.x64.appx') FilePath = "$([System.IO.Path]::GetTempPath())$($uri.Segments[-1])" } @{ Name = 'latest WinGet msixbundle' Uri = ($uri = & $Script:CommandTable.'Get-ADTRedirectedUri' -Uri 'https://aka.ms/getwinget') FilePath = "$([System.IO.Path]::GetTempPath())$($uri.Segments[-1])" } ) # Download all packages. foreach ($package in $packages) { & $Script:CommandTable.'Write-ADTLogEntry' -Message "Downloading [$($package.Name)], please wait..." & $Script:CommandTable.'Invoke-ADTWebDownload' -Uri $package.Uri -OutFile $package.FilePath } # Set the log file path. $logFile = if (& $Script:CommandTable.'Test-ADTSessionActive') { "$((& $Script:CommandTable.'Get-ADTConfig').Toolkit.LogPath)\$((& $Script:CommandTable.'Get-ADTSession').InstallName)_Dism.log" } else { "$([System.IO.Path]::GetFileNameWithoutExtension($packages[(-1)].FilePath)).log" } # Pre-provision package in the system. $aappParams = @{ Online = $true SkipLicense = $true PackagePath = $packages[(-1)].FilePath DependencyPackagePath = $packages[(0)..($packages.Count - 2)].FilePath LogPath = $logFile } & $Script:CommandTable.'Write-ADTLogEntry' -Message "Pre-provisioning [$pkgName] $($packages[-1].Uri.Segments[-2].Trim('/')), please wait..." $null = & $Script:CommandTable.'Add-AppxProvisionedPackage' @aappParams } #----------------------------------------------------------------------------- # # MARK: Repair-ADTWinGetVisualStudioRuntime # #----------------------------------------------------------------------------- function Repair-ADTWinGetVisualStudioRuntime { # Set required variables for install operation. $pkgArch = @('x86', 'x64')[[System.Environment]::Is64BitProcess] $pkgName = "Microsoft Visual C++ 2015-2022 Redistributable ($pkgArch)" $uriPath = "https://aka.ms/vs/17/release/vc_redist.$pkgArch.exe" & $Script:CommandTable.'Write-ADTLogEntry' -Message "Preparing $pkgName dependency, please wait..." # Get the active ADT session object for log naming, if available. $adtSession = if (& $Script:CommandTable.'Test-ADTSessionActive') { & $Script:CommandTable.'Get-ADTSession' } # Set up the filename for the download. $fileName = & $Script:CommandTable.'Get-Random' # Set the log filename. $logFile = if ($adtSession) { "$((& $Script:CommandTable.'Get-ADTConfig').Toolkit.LogPath)\$($adtSession.InstallName)_MSVCRT.log" } else { "$([System.IO.Path]::GetTempPath())$fileName.log" } # Define arguments for installation. $spParams = @{ FilePath = "$([System.IO.Path]::GetTempPath())$fileName.exe" ArgumentList = "/install", "/quiet", "/norestart", "/log `"$logFile`"" } # Download and extract installer. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Downloading [$pkgName], please wait..." & $Script:CommandTable.'Invoke-ADTWebDownload' -Uri $uriPath -OutFile $spParams.FilePath # Invoke installer and throw if we failed. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Installing [$pkgName], please wait..." if (($exitCode = (& $Script:CommandTable.'Start-Process' @spParams -Wait -PassThru).ExitCode)) { if ($adtSession) { $adtSession.SetExitCode($exitCode) } $naerParams = @{ Exception = [System.Runtime.InteropServices.ExternalException]::new("The installation of [$pkgName] failed with exit code [$exitCode].", $exitCode) Category = [System.Management.Automation.ErrorCategory]::InvalidResult ErrorId = 'VcRedistInstallFailure' TargetObject = $exitCode RecommendedAction = "Please review the exit code, then try again." } throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams) } } #----------------------------------------------------------------------------- # # MARK: Assert-ADTWinGetPackageManager # #----------------------------------------------------------------------------- function Assert-ADTWinGetPackageManager { <# .SYNOPSIS Verifies that WinGet is installed properly. .DESCRIPTION Verifies that WinGet is installed properly. Note: The cmdlet doesn't ensure that the latest version of WinGet is installed. It just verifies that the installed version of Winget is supported by installed version of this module. .INPUTS None You cannot pipe objects to this function. .OUTPUTS None This function does not return any output. .EXAMPLE Assert-ADTWinGetPackageManager If the current version of WinGet is installed correctly, the command returns without error. .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [CmdletBinding()] param ( ) # Try to get the WinGet version. try { [System.Version]$wingetVer = (& $Script:CommandTable.'Get-ADTWinGetVersion' -InformationAction SilentlyContinue).Trim('v') } catch { $PSCmdlet.ThrowTerminatingError($_) } # Test that the retrieved version is greater than or equal to our minimum. if ($wingetVer -lt $Script:ADT.WinGetMinVersion) { $naerParams = @{ Exception = [System.Activities.VersionMismatchException]::new("The installed WinGet version of [$wingetVer] is less than [$($Script:ADT.WinGetMinVersion)].", [System.Activities.WorkflowIdentity]::new('winget.exe', $wingetVer, $wingetPath.FullName), [System.Activities.WorkflowIdentity]::new('winget.exe', $Script:ADT.WinGetMinVersion, $wingetPath.FullName)) Category = [System.Management.Automation.ErrorCategory]::InvalidResult ErrorId = 'WinGetMinimumVersionError' RecommendedAction = "Please run [Repair-ADTWinGetPackageManager] as an admin, then try again." } $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)) } } #----------------------------------------------------------------------------- # # MARK: Find-ADTWinGetPackage # #----------------------------------------------------------------------------- function Find-ADTWinGetPackage { <# .SYNOPSIS Searches for packages from configured sources. .DESCRIPTION Searches for packages from configured sources. .PARAMETER Query Specify one or more strings to search for. By default, the command searches all configured sources. .PARAMETER MatchOption Specify matching logic used for search. .PARAMETER Command Specify the name of the command defined in the package manifest. .PARAMETER Count Limits the number of items returned by the command. .PARAMETER Id Specify the package identifier for the package you want to list. .PARAMETER Moniker Specify the moniker of the package you want to list. .PARAMETER Name Specify the name of the package to list. .PARAMETER Source Specify the name of the WinGet source to search. The most common sources are `msstore` and `winget`. .PARAMETER Tag Specify a package tag to search for. .INPUTS None You cannot pipe objects to this function. .OUTPUTS None This function does not return any output. .EXAMPLE Find-WinGetPackage -Id Microsoft.PowerShell This example shows how to search for packages by package identifier. By default, the command searches all configured sources. The command performs a case-insensitive substring match against the PackageIdentifier property of the packages. .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0)] [ValidateNotNullOrEmpty()] [System.String]$Query, [Parameter(Mandatory = $false)] [ValidateSet('Equals', 'EqualsCaseInsensitive')] [System.String]$MatchOption, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Command, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.UInt32]$Count, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Id, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Moniker, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Name, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Source, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Tag ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState # Throw if at least one filtration method isn't provided. if (!($PSBoundParameters.Keys -match '^(Query|Command|Id|Moniker|Name|Tag)$')) { $naerParams = @{ Exception = [System.ArgumentException]::new("At least one search parameter must be provided to this function.") Category = [System.Management.Automation.ErrorCategory]::InvalidArgument ErrorId = "WinGetPackageSearchError" TargetObject = $PSBoundParameters RecommendedAction = "Please review the specified parameters, then try again." } $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)) } } process { try { try { # Send this to the backend common function. if (!($wgPackage = & $Script:CommandTable.'Invoke-ADTWinGetQueryOperation' -Action Search @PSBoundParameters)) { $naerParams = @{ Exception = [System.IO.InvalidDataException]::new("No packages matched the given input criteria.") Category = [System.Management.Automation.ErrorCategory]::InvalidResult ErrorId = "WinGetPackageNotFoundError" TargetObject = $PSBoundParameters RecommendedAction = "Please review the specified input, then try again." } throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams) } return $wgPackage } catch { # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to find the specified WinGet package." } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Get-ADTWinGetPackage # #----------------------------------------------------------------------------- function Get-ADTWinGetPackage { <# .SYNOPSIS Lists installed packages. .DESCRIPTION This command lists all of the packages installed on your system. The output includes packages installed from WinGet sources and packages installed by other methods. Packages that have package identifiers starting with `MSIX` or `ARP` could not be correlated to a WinGet source. .PARAMETER Query Specify one or more strings to search for. By default, the command searches all configured sources. .PARAMETER MatchOption Specify matching logic used for search. .PARAMETER Command Specify the name of the command defined in the package manifest. .PARAMETER Count Limits the number of items returned by the command. .PARAMETER Id Specify the package identifier for the package you want to list. .PARAMETER Moniker Specify the moniker of the package you want to list. .PARAMETER Name Specify the name of the package to list. .PARAMETER Source Specify the name of the WinGet source of the package. .PARAMETER Tag Specify a package tag to search for. .INPUTS None You cannot pipe objects to this function. .OUTPUTS None This function does not return any output. .EXAMPLE Get-ADTWinGetPackage This example shows how to list all packages installed on your system. .EXAMPLE Get-ADTWinGetPackage -Id "Microsoft.PowerShell" This example shows how to get an installed package by its package identifier. .EXAMPLE Get-ADTWinGetPackage -Name "PowerShell" This example shows how to get installed packages that match a name value. The command does a substring comparison of the provided name with installed package names. .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0)] [ValidateNotNullOrEmpty()] [System.String]$Query, [Parameter(Mandatory = $false)] [ValidateSet('Equals', 'EqualsCaseInsensitive')] [System.String]$MatchOption, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Command, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.UInt32]$Count, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Id, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Moniker, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Name, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Source, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Tag ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } process { try { try { # Send this to the backend common function. return (& $Script:CommandTable.'Invoke-ADTWinGetQueryOperation' -Action List @PSBoundParameters) } catch { # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to get the specified WinGet package." } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Get-ADTWinGetSource # #----------------------------------------------------------------------------- function Get-ADTWinGetSource { <# .SYNOPSIS Lists configured WinGet sources. .DESCRIPTION Lists the configured WinGet sources. .PARAMETER Name Lists the configured WinGet sources. .INPUTS None You cannot pipe objects to this function. .OUTPUTS PSObject This function returns one or more objects for each WinGet source. .EXAMPLE Get-ADTWinGetSource Lists all configured WinGet sources. .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Name ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState # Try to get the path to WinGet before proceeding. try { $wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath' } catch { $PSCmdlet.ThrowTerminatingError($_) } } process { try { try { # Get all sources, returning early if there's none (1:1 API with `Get-WinGetSource`). & $Script:CommandTable.'Write-ADTLogEntry' -Message "Getting list of WinGet sources, please wait..." if (($wgSrcRes = & $wingetPath source list 2>&1).Equals('There are no sources configured.')) { & $Script:CommandTable.'Write-ADTLogEntry' -Message "There are no WinGet sources configured on this system." return } # Convert the results into proper PowerShell data. $wgSrcObjs = & $Script:CommandTable.'Convert-ADTWinGetQueryOutput' -WinGetOutput $wgSrcRes # Filter by the name if specified. if ($PSBoundParameters.ContainsKey('Name')) { if (!($wgSrcObj = $wgSrcObjs | & { process { if ($_.Name -eq $Name) { return $_ } } } | & $Script:CommandTable.'Select-Object' -First 1)) { $naerParams = @{ Exception = [System.ArgumentException]::new("No source found matching the given value [$Name].") Category = [System.Management.Automation.ErrorCategory]::InvalidArgument ErrorId = 'WinGetSourceNotFoundFailure' TargetObject = $wgSrcObjs RecommendedAction = "Please review the configured sources, then try again." } throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams) } & $Script:CommandTable.'Write-ADTLogEntry' -Message "Found WinGet source [$Name]." return $wgSrcObj } & $Script:CommandTable.'Write-ADTLogEntry' -Message "Found $(($wgSrcObjCount = ($wgSrcObjs | & $Script:CommandTable.'Measure-Object').Count)) WinGet source$(if (!$wgSrcObjCount.Equals(1)) { 's' }): ['$([System.String]::Join("', '", $wgSrcObjs.Name))']." return $wgSrcObjs } catch { # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to get the specified WinGet source(s)." } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Get-ADTWinGetVersion # #----------------------------------------------------------------------------- function Get-ADTWinGetVersion { <# .SYNOPSIS Gets the installed version of WinGet. .DESCRIPTION Gets the installed version of WinGet. .INPUTS None You cannot pipe objects to this function. .OUTPUTS System.String This function returns the installed WinGet's version number as a string. .EXAMPLE Get-ADTWinGetVersion -All Gets the installed version of WinGet. .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [CmdletBinding()] [OutputType([System.String])] param ( ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState # Try to get the path to WinGet before proceeding. try { $wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath' } catch { $PSCmdlet.ThrowTerminatingError($_) } } process { try { try { # Get the WinGet version and return it to the caller. The API here 1:1 matches WinGet's PowerShell module, rightly or wrongly. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Running [$wingetPath] with [--version] parameter." $wingetVer = & $wingetPath --version # If we've got a null string, we're probably missing the Visual Studio Runtime or something. if ([System.String]::IsNullOrWhiteSpace($wingetVer)) { $naerParams = @{ Exception = [System.InvalidOperationException]::new("The installed version of WinGet was unable to run.") Category = [System.Management.Automation.ErrorCategory]::PermissionDenied ErrorId = 'WinGetNullOutputError' RecommendedAction = "Please run [Repair-ADTWinGetPackageManager] as an admin, then try again." } throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams) } & $Script:CommandTable.'Write-ADTLogEntry' -Message "Installed WinGet version is [$($wingetVer)]." return $wingetVer } catch { # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to get the WinGet version." } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Install-ADTWinGetPackage # #----------------------------------------------------------------------------- function Install-ADTWinGetPackage { <# .SYNOPSIS Installs a WinGet Package. .DESCRIPTION This command installs a WinGet package from a configured source. The command includes parameters to specify values used to search for packages in the configured sources. By default, the command searches the winget source. All string-based searches are case-insensitive substring searches. Wildcards are not supported. .PARAMETER Query Specify one or more strings to search for. By default, the command searches all configured sources. .PARAMETER MatchOption Specify matching logic used for search. .PARAMETER AllowHashMismatch Allows you to download package even when the SHA256 hash for an installer or a dependency does not match the SHA256 hash in the WinGet package manifest. .PARAMETER Architecture Specify the processor architecture for the WinGet package installer. .PARAMETER Custom Use this parameter to pass additional arguments to the installer. The parameter takes a single string value. To add multiple arguments, include the arguments in the string. The arguments must be provided in the format expected by the installer. If the string contains spaces, it must be enclosed in quotes. This string is added to the arguments defined in the package manifest. .PARAMETER Force Force the installer to run even when other checks WinGet would perform would prevent this action. .PARAMETER Header Custom value to be passed via HTTP header to WinGet REST sources. .PARAMETER Id Specify the package identifier to search for. The command does a case-insensitive full text match, rather than a substring match. .PARAMETER InstallerType A package may contain multiple installer types. .PARAMETER Locale Specify the locale of the installer package. The locale must provided in the BCP 47 format, such as `en-US`. For more information, see Standard locale names (/globalization/locale/standard-locale-names). .PARAMETER Location Specify the file path where you want the packed to be installed. The installer must be able to support alternate install locations. .PARAMETER Log Specify the location for the installer log. The value can be a fully-qualified or relative path and must include the file name. For example: `$env:TEMP\package.log`. .PARAMETER Mode Specify the output mode for the installer. .PARAMETER Moniker Specify the moniker of the WinGet package to install. For example, the moniker for the Microsoft.PowerShell package is `pwsh`. .PARAMETER Name Specify the name of the package to be installed. .PARAMETER Override Use this parameter to override the existing arguments passed to the installer. The parameter takes a single string value. To add multiple arguments, include the arguments in the string. The arguments must be provided in the format expected by the installer. If the string contains spaces, it must be enclosed in quotes. This string overrides the arguments specified in the package manifest. .PARAMETER Scope Specify WinGet package installer scope. .PARAMETER SkipDependencies Specifies that the command shouldn't install the WinGet package dependencies. .PARAMETER Source Specify the name of the WinGet source from which the package should be installed. .PARAMETER Version Specify the version of the package. .PARAMETER DebugHashMismatch Forces the AllowHashMismatch for debugging purposes. .INPUTS None You cannot pipe objects to this function. .OUTPUTS PSObject This function returns a PSObject containing the outcome of the operation. .EXAMPLE Install-WinGetPackage -Id Microsoft.PowerShell This example shows how to install a package by the specifying the package identifier. If the package identifier is available from more than one source, you must provide additional search criteria to select a specific instance of the package. If more than one source is configured with the same package identifier, the user must disambiguate. .EXAMPLE Install-WinGetPackage -Name "PowerToys (Preview)" This example shows how to install a package by specifying the package name. .EXAMPLE Install-WinGetPackage Microsoft.PowerShell -Version 7.4.4.0 This example shows how to install a specific version of a package using a query. The command does a query search for packages matching `Microsoft.PowerShell`. The results of the search a limited to matches with the version of `7.4.4.0`. .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0)] [ValidateNotNullOrEmpty()] [System.String]$Query, [Parameter(Mandatory = $false)] [ValidateSet('Equals', 'EqualsCaseInsensitive')] [System.String]$MatchOption, [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$AllowHashMismatch, [Parameter(Mandatory = $false)] [ValidateSet('x86', 'x64', 'arm64')] [System.String]$Architecture, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Custom, [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$Force, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Header, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Id, [Parameter(Mandatory = $false)] [ValidateSet('Inno', 'Wix', 'Msi', 'Nullsoft', 'Zip', 'Msix', 'Exe', 'Burn', 'MSStore', 'Portable')] [System.String]$InstallerType, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Locale, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Location, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Log, [Parameter(Mandatory = $false)] [ValidateSet('Silent', 'Interactive')] [System.String]$Mode, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Moniker, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Name, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Override, [Parameter(Mandatory = $false)] [ValidateSet('Any', 'User', 'System', 'UserOrUnknown', 'SystemOrUnknown')] [System.String]$Scope, [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$SkipDependencies, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Source, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Version, [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$DebugHashMismatch ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } process { try { try { # Send this to the backend common function. return (& $Script:CommandTable.'Invoke-ADTWinGetDeploymentOperation' -Action Install @PSBoundParameters) } catch { # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to install the specified WinGet package." } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Invoke-ADTWinGetOperation # #----------------------------------------------------------------------------- function Invoke-ADTWinGetOperation { <# .SYNOPSIS PSAppDeployToolkit - This script performs the installation or uninstallation of an application(s). .DESCRIPTION - The script is provided as a template to perform an install, uninstall, or repair of an application(s). - The script either performs an "Install", "Uninstall", or "Repair" deployment type. - The install deployment type is broken down into 3 main sections/phases: Pre-Install, Install, and Post-Install. The script imports the PSAppDeployToolkit module which contains the logic and functions required to install or uninstall an application. .PARAMETER Id The WinGet package identifier for the deployment. .PARAMETER DeploymentType The type of deployment to perform. Default is: Install. .PARAMETER DeployMode Specifies whether the installation should be run in Interactive, Silent, or NonInteractive mode. Default is: Interactive. Options: Interactive = Shows dialogs, Silent = No dialogs, NonInteractive = Very silent, i.e. no blocking apps. NonInteractive mode is automatically set if it is detected that the process is not user interactive. .PARAMETER AllowRebootPassThru Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. SCCM) if detected from an installation. If 3010 is passed back to SCCM, a reboot prompt will be triggered. .EXAMPLE powershell.exe -File Invoke-AppDeployToolkit.ps1 -DeployMode Silent .EXAMPLE powershell.exe -File Invoke-AppDeployToolkit.ps1 -AllowRebootPassThru .EXAMPLE powershell.exe -File Invoke-AppDeployToolkit.ps1 -DeploymentType Uninstall .EXAMPLE Invoke-AppDeployToolkit.exe -DeploymentType "Install" -DeployMode "Silent" .INPUTS None. You cannot pipe objects to this script. .OUTPUTS None. This script does not generate any output. .LINK https://psappdeploytoolkit.com #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$Id, [Parameter(Mandatory = $false)] [ValidateSet('Install', 'Uninstall', 'Repair')] [System.String]$DeploymentType = 'Install', [Parameter(Mandatory = $false)] [ValidateSet('Interactive', 'Silent', 'NonInteractive')] [System.String]$DeployMode = 'Interactive', [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$AllowRebootPassThru ) ##================================================ ## MARK: Pre-initialization ##================================================ # Set strict error handling across entire operation. $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop $ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue & $Script:CommandTable.'Set-StrictMode' -Version 1 $mainError = $null # Confirm WinGet is healthy, then try to find the specified package. try { & $Script:CommandTable.'Assert-ADTWinGetPackageManager' } catch { try { & $Script:CommandTable.'Invoke-ADTWinGetRepair' & $Script:CommandTable.'Assert-ADTWinGetPackageManager' } catch { $PSCmdlet.ThrowTerminatingError($_) } } # Try to find the specified package. try { $wgPackage = & $Script:CommandTable.'Find-ADTWinGetPackage' -Id $Id } catch { $PSCmdlet.ThrowTerminatingError($_) } ##================================================ ## MARK: Variables ##================================================ $adtSession = @{ # App variables. AppName = ($wgPackage.Name -replace ([regex]::Escape($wgPackage.Version))).Trim() AppVersion = $wgPackage.Version # Script variables. DeployAppScriptFriendlyName = $MyInvocation.MyCommand.Name DeployAppScriptVersion = $MyInvocation.MyCommand.Module.Version DeployAppScriptParameters = $PSBoundParameters } function Install-ADTDeployment { ##================================================ ## MARK: Pre-Install ##================================================ $adtSession.InstallPhase = "Pre-$($adtSession.DeploymentType)" ## Show Welcome Message, close Internet Explorer if required, allow up to 3 deferrals, verify there is enough disk space to complete the install, and persist the prompt. & $Script:CommandTable.'Show-ADTInstallationWelcome' -AllowDefer -DeferTimes 3 -CheckDiskSpace -PersistPrompt -NoMinimizeWindows if ($adtSession.GetExitCode().Equals(60012)) { $Global:LASTEXITCODE = 60012; break } ## Show Progress Message (with the default message). & $Script:CommandTable.'Show-ADTInstallationProgress' ##================================================ ## MARK: Install ##================================================ $adtSession.InstallPhase = $adtSession.DeploymentType ## Install our WinGet package. $null = & $Script:CommandTable.'Install-ADTWinGetPackage' -Id $Id ##================================================ ## MARK: Post-Install ##================================================ $adtSession.InstallPhase = "Post-$($adtSession.DeploymentType)" } function Uninstall-ADTDeployment { ##================================================ ## MARK: Pre-Uninstall ##================================================ $adtSession.InstallPhase = "Pre-$($adtSession.DeploymentType)" ## Show Welcome Message, close Internet Explorer with a 60 second countdown before automatically closing. & $Script:CommandTable.'Show-ADTInstallationWelcome' -CloseProcessesCountdown 60 -NoMinimizeWindows if ($adtSession.GetExitCode().Equals(60012)) { $Global:LASTEXITCODE = 60012; break } ## Show Progress Message (with the default message). & $Script:CommandTable.'Show-ADTInstallationProgress' ##================================================ ## MARK: Uninstall ##================================================ $adtSession.InstallPhase = $adtSession.DeploymentType ## Uninstall our WinGet package. $null = & $Script:CommandTable.'Uninstall-ADTWinGetPackage' -Id $Id ##================================================ ## MARK: Post-Uninstallation ##================================================ $adtSession.InstallPhase = "Post-$($adtSession.DeploymentType)" } function Repair-ADTDeployment { ##================================================ ## MARK: Pre-Repair ##================================================ $adtSession.InstallPhase = "Pre-$($adtSession.DeploymentType)" ## Show Welcome Message, close Internet Explorer with a 60 second countdown before automatically closing. & $Script:CommandTable.'Show-ADTInstallationWelcome' -CloseProcessesCountdown 60 -NoMinimizeWindows if ($adtSession.GetExitCode().Equals(60012)) { $Global:LASTEXITCODE = 60012; break } ## Show Progress Message (with the default message). & $Script:CommandTable.'Show-ADTInstallationProgress' ##================================================ ## MARK: Repair ##================================================ $adtSession.InstallPhase = $adtSession.DeploymentType ## Repair our WinGet package. $null = & $Script:CommandTable.'Repair-ADTWinGetPackage' -Id $Id ##================================================ ## MARK: Post-Repair ##================================================ $adtSession.InstallPhase = "Post-$($adtSession.DeploymentType)" } ##================================================ ## MARK: Initialization ##================================================ # Import the module and instantiate a new session. try { $adtSession = & $Script:CommandTable.'Open-ADTSession' -SessionState $ExecutionContext.SessionState @adtSession @PSBoundParameters -PassThru } catch { $PSCmdlet.ThrowTerminatingError($_) } ##================================================ ## MARK: Invocation ##================================================ try { & "$($adtSession.DeploymentType)-ADTDeployment" & $Script:CommandTable.'Close-ADTSession' $Global:LASTEXITCODE = $adtSession.GetExitCode() } catch { & $Script:CommandTable.'Write-ADTLogEntry' -Message ($mainErrorMessage = & $Script:CommandTable.'Resolve-ADTErrorRecord' -ErrorRecord ($mainError = $_)) -Severity 3 & $Script:CommandTable.'Show-ADTDialogBox' -Text $mainErrorMessage -Icon Stop | & $Script:CommandTable.'Out-Null' & $Script:CommandTable.'Close-ADTSession' -ExitCode 60001 -Force $Global:LASTEXITCODE = 60001 } finally { if ($mainError) { $PSCmdlet.ThrowTerminatingError($mainError) } } } #----------------------------------------------------------------------------- # # MARK: Invoke-ADTWinGetRepair # #----------------------------------------------------------------------------- function Invoke-ADTWinGetRepair { <# .SYNOPSIS PSAppDeployToolkit - This script performs the installation or uninstallation of an application(s). .DESCRIPTION - The script is provided as a template to perform an install, uninstall, or repair of an application(s). - The script either performs an "Install", "Uninstall", or "Repair" deployment type. - The install deployment type is broken down into 3 main sections/phases: Pre-Install, Install, and Post-Install. The script imports the PSAppDeployToolkit module which contains the logic and functions required to install or uninstall an application. .PARAMETER AllowRebootPassThru Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. SCCM) if detected from an installation. If 3010 is passed back to SCCM, a reboot prompt will be triggered. .EXAMPLE powershell.exe -File Invoke-AppDeployToolkit.ps1 -AllowRebootPassThru .INPUTS None. You cannot pipe objects to this script. .OUTPUTS None. This script does not generate any output. .LINK https://psappdeploytoolkit.com #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$AllowRebootPassThru ) ##================================================ ## MARK: Variables ##================================================ $adtSession = @{ # App variables. AppName = "$($MyInvocation.MyCommand.Module.Name) Repair Operation" # Script variables. DeployAppScriptFriendlyName = $MyInvocation.MyCommand.Name DeployAppScriptVersion = $MyInvocation.MyCommand.Module.Version DeployAppScriptParameters = $PSBoundParameters # Script parameters. DeploymentType = 'Repair' DeployMode = 'Silent' } ##================================================ ## MARK: Initialization ##================================================ # Set strict error handling across entire operation. $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop $ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue & $Script:CommandTable.'Set-StrictMode' -Version 1 $mainError = $null # Import the module and instantiate a new session. try { $adtSession = & $Script:CommandTable.'Open-ADTSession' -SessionState $ExecutionContext.SessionState @adtSession @PSBoundParameters -PassThru $adtSession.InstallPhase = $adtSession.DeploymentType } catch { $PSCmdlet.ThrowTerminatingError($_) } ##================================================ ## MARK: Invocation ##================================================ try { & $Script:CommandTable.'Repair-ADTWinGetPackageManager' & $Script:CommandTable.'Close-ADTSession' $Global:LASTEXITCODE = $adtSession.GetExitCode() } catch { & $Script:CommandTable.'Write-ADTLogEntry' -Message (& $Script:CommandTable.'Resolve-ADTErrorRecord' -ErrorRecord ($mainError = $_)) -Severity 3 & $Script:CommandTable.'Close-ADTSession' -ExitCode 60001 -Force:(!(& $Script:CommandTable.'Get-PSCallStack').Command.Equals('Invoke-ADTWinGetOperation')) $Global:LASTEXITCODE = 60001 } finally { if ($mainError) { $PSCmdlet.ThrowTerminatingError($mainError) } } } #----------------------------------------------------------------------------- # # MARK: Repair-ADTWinGetPackage # #----------------------------------------------------------------------------- function Repair-ADTWinGetPackage { <# .SYNOPSIS Repairs a WinGet Package. .DESCRIPTION This command repairs a WinGet package from your computer, provided the package includes repair support. The command includes parameters to specify values used to search for installed packages. By default, all string-based searches are case-insensitive substring searches. Wildcards are not supported. Note: Not all packages support repair. .PARAMETER Query Specify one or more strings to search for. By default, the command searches all configured sources. .PARAMETER MatchOption Specify matching logic used for search. .PARAMETER Id Specify the package identifier to search for. The command does a case-insensitive full text match, rather than a substring match. .PARAMETER Log Specify the location for the installer log. The value can be a fully-qualified or relative path and must include the file name. For example: `$env:TEMP\package.log`. .PARAMETER Mode Specify the output mode for the installer. .PARAMETER Moniker Specify the moniker of the WinGet package to install. For example, the moniker for the Microsoft.PowerShell package is `pwsh`. .PARAMETER Name Specify the name of the package to be installed. .PARAMETER Source Specify the name of the WinGet source from which the package should be installed. .PARAMETER Version Specify the version of the package. .INPUTS None You cannot pipe objects to this function. .OUTPUTS PSObject This function returns a PSObject containing the outcome of the operation. .EXAMPLE Repair-WinGetPackage -Id "Microsoft.GDK.2406" This example shows how to repair a package by specifying the package identifier. If the package identifier is available from more than one source, you must provide additional search criteria to select a specific instance of the package. .EXAMPLE Repair-WinGetPackage -Name "Microsoft Game Development Kit - 240602 (June 2024 Update 2)" This example shows how to repair a package using the package name. Please note that the examples mentioned above are mainly reference examples for the repair cmdlet and may not be operational as is, since many installers don't support repair as a standard functionality. For the Microsoft.GDK.2406 example, the assumption is that Microsoft.GDK.2406 supports repair capability and the author of the installer has provided the necessary repair context/switches in the Package Manifest in the Package Source referenced by the WinGet Client. .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0)] [ValidateNotNullOrEmpty()] [System.String]$Query, [Parameter(Mandatory = $false)] [ValidateSet('Equals', 'EqualsCaseInsensitive')] [System.String]$MatchOption, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Id, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Log, [Parameter(Mandatory = $false)] [ValidateSet('Silent', 'Interactive')] [System.String]$Mode, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Moniker, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Name, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Source, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Version ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } process { try { try { # Send this to the backend common function. return (& $Script:CommandTable.'Invoke-ADTWinGetDeploymentOperation' -Action Repair @PSBoundParameters) } catch { # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to repair the specified WinGet package." } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Repair-ADTWinGetPackageManager # #----------------------------------------------------------------------------- function Repair-ADTWinGetPackageManager { <# .SYNOPSIS Repairs the installation of the WinGet client on your computer. .DESCRIPTION This command repairs the installation of the WinGet client on your computer by installing the specified version or the latest version of the client. This command can also install the WinGet client if it is not already installed on your machine. It ensures that the client is installed in a working state. .INPUTS None You cannot pipe objects to this function. .OUTPUTS None This function does not return any output. .EXAMPLE Repair-ADTWinGetPackageManager This example shows how to repair they WinGet client by installing the latest version and ensuring it functions properly. .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [CmdletBinding()] param ( ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } process { try { try { # Test whether WinGet is installed and available at all. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Confirming whether [Microsoft.DesktopAppInstaller] is installed, please wait..." if (!($wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath') -or !$wingetPath.Exists) { # Throw if we're not admin. if (!$Script:ADT.RunningAsAdmin) { $naerParams = @{ Exception = [System.UnauthorizedAccessException]::new("WinGet is not installed. Please install [Microsoft.DesktopAppInstaller] and try again.") Category = [System.Management.Automation.ErrorCategory]::PermissionDenied ErrorId = 'MicrosoftDesktopAppInstallerCannotInstallFailure' RecommendedAction = "Please install [Microsoft.DesktopAppInstaller] as an admin, then try again." } throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams) } # Install Microsoft.DesktopAppInstaller. & $Script:CommandTable.'Repair-ADTWinGetDesktopAppInstaller' # Throw if the installation was successful but we still don't have WinGet. if (!($wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath') -or !$wingetPath.Exists) { $naerParams = @{ Exception = [System.InvalidOperationException]::new("Failed to get a valid WinGet path after successfully pre-provisioning the app. Please report this issue for further analysis.") Category = [System.Management.Automation.ErrorCategory]::InvalidResult ErrorId = 'MicrosoftDesktopAppInstallerMissingFailure' RecommendedAction = "Please report this issue to the project's maintainer for further analysis." } throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams) } } else { & $Script:CommandTable.'Write-ADTLogEntry' -Message "Successfully confirmed that [Microsoft.DesktopAppInstaller] is installed on system." } # Test whether we have any output from winget.exe. If this is null, it typically means the appropriate MSVC++ runtime is not installed. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Testing whether [Microsoft Visual C++ 2015-2022 Runtime] is installed, please wait..." if (!(& $wingetPath)) { # Throw if we're not admin. if (!$Script:ADT.RunningAsAdmin) { $naerParams = @{ Exception = [System.InvalidOperationException]::new("The installed version of WinGet was unable to run. Please ensure the latest [Microsoft Visual C++ 2015-2022 Runtime] is installed and try again.") Category = [System.Management.Automation.ErrorCategory]::PermissionDenied ErrorId = 'VcRedistCannotInstallFailure' RecommendedAction = "Please install the latest [Microsoft Visual C++ 2015-2022 Runtime] as an admin, then try again." } throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams) } # Install MSVCRT onto device. & $Script:CommandTable.'Repair-ADTWinGetVisualStudioRuntime' # Throw if we're still not able to run WinGet. if (!(& $wingetPath)) { $naerParams = @{ Exception = [System.InvalidOperationException]::new("The installed version of WinGet was unable to run. This is possibly related to a missing [Microsoft Visual C++ 2015-2022 Runtime] library.") Category = [System.Management.Automation.ErrorCategory]::InvalidResult ErrorId = 'MicrosoftDesktopAppInstallerExecutionFailure' RecommendedAction = "Please verify that WinGet.exe can run on this system, then try again." } throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams) } } else { & $Script:CommandTable.'Write-ADTLogEntry' -Message "Successfully confirmed that [Microsoft Visual C++ 2015-2022 Runtime] is installed on system." } # Ensure winget.exe is above the minimum version. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Testing whether the installed WinGet is version [$($Script:ADT.WinGetMinVersion)] or higher, please wait..." if (([System.Version]$wingetVer = (& $Script:CommandTable.'Get-ADTWinGetVersion' -InformationAction SilentlyContinue).Trim('v')) -lt $Script:ADT.WinGetMinVersion) { # Throw if we're not admin. if (!$Script:ADT.RunningAsAdmin) { $naerParams = @{ Exception = [System.Activities.VersionMismatchException]::new("The installed WinGet version of [$wingetVer] is less than [$($Script:ADT.WinGetMinVersion)]. Please update [Microsoft.DesktopAppInstaller] and try again.", [System.Activities.WorkflowIdentity]::new('winget.exe', $wingetVer, $wingetPath.FullName), [System.Activities.WorkflowIdentity]::new('winget.exe', $Script:ADT.WinGetMinVersion, $wingetPath.FullName)) Category = [System.Management.Automation.ErrorCategory]::PermissionDenied ErrorId = 'VcRedistCannotInstallFailure' RecommendedAction = "Please update [Microsoft.DesktopAppInstaller] as an admin, then try again." } throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams) } # Install the missing dependency and reset variables. & $Script:CommandTable.'Repair-ADTWinGetDesktopAppInstaller' $wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath' # Ensure winget.exe is above the minimum version. & $Script:CommandTable.'Assert-ADTWinGetPackageManager' # Reset WinGet sources after updating. Helps with a corner-case issue discovered. & $Script:CommandTable.'Reset-ADTWinGetSource' -All } else { & $Script:CommandTable.'Write-ADTLogEntry' -Message "Successfully confirmed WinGet version [$wingetVer] is installed on system." } } catch { # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to repair the WinGet package manager." } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Reset-ADTWinGetSource # #----------------------------------------------------------------------------- function Reset-ADTWinGetSource { <# .SYNOPSIS Resets WinGet sources. .DESCRIPTION Resets a named WinGet source by removing the source configuration. You can reset all configured sources and add the default source configurations using the All switch parameter. This command must be executed with administrator permissions. .PARAMETER Name The name of the source. .PARAMETER All Reset all sources and add the default sources. .INPUTS None You cannot pipe objects to this function. .OUTPUTS None This function does not return any output. .EXAMPLE Reset-ADTWinGetSource -Name msstore This example resets the configured source named 'msstore' by removing it. .EXAMPLE Reset-ADTWinGetSource -All This example resets all configured sources and adds the default sources. .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [CmdletBinding(DefaultParameterSetName = 'Name')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Name')] [ValidateNotNullOrEmpty()] [System.String]$Name, [Parameter(Mandatory = $true, ParameterSetName = 'All')] [System.Management.Automation.SwitchParameter]$All ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState # Try to get the path to WinGet before proceeding. try { $wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath' } catch { $PSCmdlet.ThrowTerminatingError($_) } } process { try { try { # Reset all sources if specified. if ($All) { & $Script:CommandTable.'Write-ADTLogEntry' -Message "Resetting all WinGet sources, please wait..." if (!($wgSrcRes = & $wingetPath source reset --force 2>&1).Equals('Resetting all sources...Done')) { $naerParams = @{ Exception = [System.Runtime.InteropServices.ExternalException]::new("Failed to reset all WinGet sources. $($wgSrcRes.TrimEnd('.')).", $Global:LASTEXITCODE) Category = [System.Management.Automation.ErrorCategory]::InvalidResult ErrorId = 'WinGetSourceAllResetFailure' TargetObject = $wgSrcRes RecommendedAction = "Please review the result in this error's TargetObject property and try again." } throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams) } & $Script:CommandTable.'Write-ADTLogEntry' -Message "Successfully reset all WinGet sources." return } # Reset the specified source. & $Script:CommandTable.'Write-ADTLogEntry' -Message "Resetting WinGet source [$Name], please wait..." if (!($wgSrcRes = & $wingetPath source reset $Name 2>&1).Equals("Resetting source: $Name...Done")) { $naerParams = @{ Exception = [System.Runtime.InteropServices.ExternalException]::new("Failed to WinGet source [$Name]. $($wgSrcRes.TrimEnd('.')).", $Global:LASTEXITCODE) Category = [System.Management.Automation.ErrorCategory]::InvalidResult ErrorId = "WinGetNamedSourceResetFailure" TargetObject = $wgSrcRes RecommendedAction = "Please review the result in this error's TargetObject property and try again." } throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams) } & $Script:CommandTable.'Write-ADTLogEntry' -Message "Successfully WinGet source [$Name]." } catch { # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to repair the specified WinGet source(s)." } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Uninstall-ADTWinGetPackage # #----------------------------------------------------------------------------- function Uninstall-ADTWinGetPackage { <# .SYNOPSIS Uninstalls a WinGet Package. .DESCRIPTION This command uninstalls a WinGet package from your computer. The command includes parameters to specify values used to search for installed packages. By default, all string-based searches are case-insensitive substring searches. Wildcards are not supported. .PARAMETER Query Specify one or more strings to search for. By default, the command searches all configured sources. .PARAMETER MatchOption Specify matching logic used for search. .PARAMETER Force Force the installer to run even when other checks WinGet would perform would prevent this action. .PARAMETER Id Specify the package identifier to search for. The command does a case-insensitive full text match, rather than a substring match. .PARAMETER Log Specify the location for the installer log. The value can be a fully-qualified or relative path and must include the file name. For example: `$env:TEMP\package.log`. .PARAMETER Mode Specify the output mode for the installer. .PARAMETER Moniker Specify the moniker of the WinGet package to install. For example, the moniker for the Microsoft.PowerShell package is `pwsh`. .PARAMETER Name Specify the name of the package to be installed. .PARAMETER Source Specify the name of the WinGet source from which the package should be installed. .PARAMETER Version Specify the version of the package. .INPUTS None You cannot pipe objects to this function. .OUTPUTS PSObject This function returns a PSObject containing the outcome of the operation. .EXAMPLE Uninstall-WinGetPackage -Id Microsoft.PowerShell This example shows how to uninstall a package by the specifying the package identifier. If the package identifier is available from more than one source, you must provide additional search criteria to select a specific instance of the package. .EXAMPLE Uninstall-WinGetPackage -Name "PowerToys (Preview)" This sample uninstalls the PowerToys package by the specifying the package name. .EXAMPLE Uninstall-WinGetPackage Microsoft.PowerShell -Version 7.4.4.0 This example shows how to uninstall a specific version of a package using a query. The command does a query search for packages matching `Microsoft.PowerShell`. The results of the search a limited to matches with the version of `7.4.4.0`. .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0)] [ValidateNotNullOrEmpty()] [System.String]$Query, [Parameter(Mandatory = $false)] [ValidateSet('Equals', 'EqualsCaseInsensitive')] [System.String]$MatchOption, [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$Force, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Id, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Log, [Parameter(Mandatory = $false)] [ValidateSet('Silent', 'Interactive')] [System.String]$Mode, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Moniker, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Name, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Source, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Version ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } process { try { try { # Send this to the backend common function. return (& $Script:CommandTable.'Invoke-ADTWinGetDeploymentOperation' -Action Uninstall @PSBoundParameters) } catch { # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to uninstall the specified WinGet package." } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Update-ADTWinGetPackage # #----------------------------------------------------------------------------- function Update-ADTWinGetPackage { <# .SYNOPSIS Installs a newer version of a previously installed WinGet package. .DESCRIPTION This command searches the packages installed on your system and installs a newer version of the matching WinGet package. The command includes parameters to specify values used to search for packages in the configured sources. By default, the command searches the winget source. All string-based searches are case-insensitive substring searches. Wildcards are not supported. .PARAMETER Query Specify one or more strings to search for. By default, the command searches all configured sources. .PARAMETER MatchOption Specify matching logic used for search. .PARAMETER AllowHashMismatch Allows you to download package even when the SHA256 hash for an installer or a dependency does not match the SHA256 hash in the WinGet package manifest. .PARAMETER Architecture Specify the processor architecture for the WinGet package installer. .PARAMETER Custom Use this parameter to pass additional arguments to the installer. The parameter takes a single string value. To add multiple arguments, include the arguments in the string. The arguments must be provided in the format expected by the installer. If the string contains spaces, it must be enclosed in quotes. This string is added to the arguments defined in the package manifest. .PARAMETER Force Force the installer to run even when other checks WinGet would perform would prevent this action. .PARAMETER Header Custom value to be passed via HTTP header to WinGet REST sources. .PARAMETER Id Specify the package identifier to search for. The command does a case-insensitive full text match, rather than a substring match. .PARAMETER IncludeUnknown Use this parameter to upgrade the package when the installed version is not specified in the registry. .PARAMETER InstallerType A package may contain multiple installer types. .PARAMETER Locale Specify the locale of the installer package. The locale must provided in the BCP 47 format, such as `en-US`. For more information, see Standard locale names (/globalization/locale/standard-locale-names). .PARAMETER Location Specify the file path where you want the packed to be installed. The installer must be able to support alternate install locations. .PARAMETER Log Specify the location for the installer log. The value can be a fully-qualified or relative path and must include the file name. For example: `$env:TEMP\package.log`. .PARAMETER Mode Specify the output mode for the installer. .PARAMETER Moniker Specify the moniker of the WinGet package to install. For example, the moniker for the Microsoft.PowerShell package is `pwsh`. .PARAMETER Name Specify the name of the package to be installed. .PARAMETER Override Use this parameter to override the existing arguments passed to the installer. The parameter takes a single string value. To add multiple arguments, include the arguments in the string. The arguments must be provided in the format expected by the installer. If the string contains spaces, it must be enclosed in quotes. This string overrides the arguments specified in the package manifest. .PARAMETER Scope Specify WinGet package installer scope. .PARAMETER SkipDependencies Specifies that the command shouldn't install the WinGet package dependencies. .PARAMETER Source Specify the name of the WinGet source from which the package should be installed. .PARAMETER Version Specify the version of the package. .PARAMETER DebugHashMismatch Forces the AllowHashMismatch for debugging purposes. .INPUTS None You cannot pipe objects to this function. .OUTPUTS PSObject This function returns a PSObject containing the outcome of the operation. .EXAMPLE Update-WinGetPackage -Id Microsoft.PowerShell This example shows how to update a package by the specifying the package identifier. If the package identifier is available from more than one source, you must provide additional search criteria to select a specific instance of the package. .EXAMPLE Update-WinGetPackage -Name "PowerToys (Preview)" This sample updates the PowerToys package by the specifying the package name. .EXAMPLE Update-WinGetPackage Microsoft.PowerShell -Version 7.4.4.0 This example shows how to update a specific version of a package using a query. The command does a query search for packages matching `Microsoft.PowerShell`. The results of the search a limited to matches with the version of `7.4.4.0`. .LINK https://github.com/mjr4077au/PSAppDeployToolkit.WinGet #> [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0)] [ValidateNotNullOrEmpty()] [System.String]$Query, [Parameter(Mandatory = $false)] [ValidateSet('Equals', 'EqualsCaseInsensitive')] [System.String]$MatchOption, [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$AllowHashMismatch, [Parameter(Mandatory = $false)] [ValidateSet('x86', 'x64', 'arm64')] [System.String]$Architecture, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Custom, [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$Force, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Header, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Id, [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$IncludeUnknown, [Parameter(Mandatory = $false)] [ValidateSet('Inno', 'Wix', 'Msi', 'Nullsoft', 'Zip', 'Msix', 'Exe', 'Burn', 'MSStore', 'Portable')] [System.String]$InstallerType, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Locale, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Location, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Log, [Parameter(Mandatory = $false)] [ValidateSet('Silent', 'Interactive')] [System.String]$Mode, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Moniker, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Name, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Override, [Parameter(Mandatory = $false)] [ValidateSet('Any', 'User', 'System', 'UserOrUnknown', 'SystemOrUnknown')] [System.String]$Scope, [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$SkipDependencies, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Source, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Version, [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$DebugHashMismatch ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } process { try { try { # Send this to the backend common function. return (& $Script:CommandTable.'Invoke-ADTWinGetDeploymentOperation' -Action Upgrade @PSBoundParameters) } catch { # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to update the specified WinGet package." } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Module Constants and Function Exports # #----------------------------------------------------------------------------- # Set all functions as read-only, export all public definitions and finalise the CommandTable. & $Script:CommandTable.'Set-Item' -LiteralPath $FunctionPaths -Options ReadOnly & $Script:CommandTable.'Get-Item' -LiteralPath $FunctionPaths | & { process { $CommandTable.Add($_.Name, $_) } } & $Script:CommandTable.'New-Variable' -Name CommandTable -Value ([System.Collections.ObjectModel.ReadOnlyDictionary[System.String, System.Management.Automation.CommandInfo]]::new($CommandTable)) -Option Constant -Force -Confirm:$false & $Script:CommandTable.'Export-ModuleMember' -Function $Module.Manifest.FunctionsToExport # Store module globals needed for the lifetime of the module. & $Script:CommandTable.'New-Variable' -Name ADT -Option Constant -Value ([pscustomobject]@{ WinGetMinVersion = [System.Version]::new(1, 7, 10582) RunningAsSystem = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.IsWellKnown([System.Security.Principal.WellKnownSidType]::LocalSystemSid) RunningAsAdmin = & $Script:CommandTable.'Test-ADTCallerIsAdmin' ArchLookupTable = ([ordered]@{ [PSADT.Shared.SystemArchitecture]::ARM64 = 'arm64' [PSADT.Shared.SystemArchitecture]::AMD64 = 'x64' [PSADT.Shared.SystemArchitecture]::i386 = 'x86' }).AsReadOnly() }) # Following the successful import, set the console's output encoding to UTF8 as required by WinGet's command line. [System.Console]::OutputEncoding = [System.Text.Encoding]::UTF8 |