Install winget as Local System (for scripting)

article #1606, updated 4 days ago

This works for recent builds of 10 and 11. Server 2019 sometimes has issues.

<# Winget-Install.ps1

Source for Some Functions: https://github.com/homotechsual/ninjaget/blob/dev/PS/NinjaGet.Functions.Installation.ps1

2023-11-22 - 0.0.2 - assembled and modified by David Szpunar - Initial version

DOWNLOAD: https://discord.com/channels/676451788395642880/1232432179141808228

2024-04-23 - 0.1.0 - Simplified and Modified by GraphicHealer - Initial Version

2024-10-11 - 0.1.1 - took out Ninja RMM item - JEB

DESCRIPTION: Installs or updates WinGet.exe to the latest version. Intended to run as SYSTEM.
        Use '-Force' to ignore version check.
-----
LICENSE: The MIT License (MIT)

Original Copyright 2023 Mikey O'Toole, modified by David Szpunar, Simplified and Modified by GraphicHealer.
#>

# Add -Force switch to force install
param(
    [switch]$Force = $false
)

# Start Logging
Start-Transcript -Path ".\winget-install.log" -IncludeInvocationHeader -Append -Force

# Setup Error Codes
$Script:ExitCode = 0

# Function to get OS Information
function Get-OSInfo {
    try {
        # Get registry values
        $registryValues = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
        $releaseIdValue = $registryValues.ReleaseId
        $displayVersionValue = $registryValues.DisplayVersion
        $nameValue = $registryValues.ProductName
        $editionIdValue = $registryValues.EditionId

        # Strip out "Server" from the $editionIdValue if it exists
        $editionIdValue = $editionIdValue -replace 'Server', ''

        # Get OS details using Get-CimInstance because the registry key for Name is not always correct with Windows 11
        $osDetails = Get-CimInstance -ClassName Win32_OperatingSystem
        $nameValue = $osDetails.Caption

        # Get architecture details of the OS (not the processor)
        # Get only the numbers
        $architecture = ($osDetails.OSArchitecture -replace '[^\d]').Trim()

        # If 32-bit or 64-bit replace with x32 and x64
        if ($architecture -eq '32') {
            $architecture = 'x32'
        } elseif ($architecture -eq '64') {
            $architecture = 'x64'
        }

        # Get OS version details (as version object)
        $versionValue = [System.Environment]::OSVersion.Version

        # Determine product type
        # Reference: https://learn.microsoft.com/en-us/dotnet/api/microsoft.powershell.commands.producttype?view=powershellsdk-1.1.0
        if ($osDetails.ProductType -eq 1) {
            $typeValue = 'Workstation'
        } elseif ($osDetails.ProductType -eq 2 -or $osDetails.ProductType -eq 3) {
            $typeValue = 'Server'
        } else {
            $typeValue = 'Unknown'
        }

        # Extract numerical value from Name
        $numericVersion = ($nameValue -replace '[^\d]').Trim()

        # Create and return custom object with the required properties
        $result = [PSCustomObject]@{
            ReleaseId      = $releaseIdValue
            DisplayVersion = $displayVersionValue
            Name           = $nameValue
            Type           = $typeValue
            NumericVersion = $numericVersion
            EditionId      = $editionIdValue
            Version        = $versionValue
            Architecture   = $architecture
        }

        return $result
    } catch {
        Write-Error "Unable to get OS version details.`nError: $_"
        exit 1
    }
}

# Function to Install WinGet (modified)
function Install-WinGet {
    # Install VCPPRedist Requirements
    $Visual2019 = 'Microsoft Visual C++ 2015-2019 Redistributable*'
    $Visual2022 = 'Microsoft Visual C++ 2015-2022 Redistributable*'
    $VCPPInstalled = Get-Item @('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*') | Where-Object {
        $_.GetValue('DisplayName') -like $Visual2019 -or $_.GetValue('DisplayName') -like $Visual2022
    }
    if ([System.Environment]::Is64BitOperatingSystem) {
        $OSArch = 'x64'
    } else {
        $OSArch = 'x86'
    }
    if (!($VCPPInstalled)) {
        Write-Output 'VCPPRedist Requirements Not Installed. Installing...'

        $VCPPRedistURL = ('https://aka.ms/vs/17/release/vc_redist.{0}.exe' -f $OSArch)
        $VCPPRedistFileName = [Uri]$VCPPRedistURL | Select-Object -ExpandProperty Segments | Select-Object -Last 1
        $WebClient = New-Object System.Net.WebClient
        $VCPPRedistDownloadPath = "$ENV:SystemRoot\Temp\VCPRedist"
        if (!(Test-Path -Path $VCPPRedistDownloadPath)) {
            $null = New-Item -Path $VCPPRedistDownloadPath -ItemType Directory -Force
        }
        $VCPPRedistDownloadFile = "$VCPPRedistDownloadPath\$VCPPRedistFileName"
        $WebClient.DownloadFile($VCPPRedistURL, $VCPPRedistDownloadFile)
        try {
            Start-Process -FilePath $VCPPRedistDownloadFile -ArgumentList '/quiet', '/norestart' -Wait -ErrorAction Stop | Out-Null
            Write-Output 'VCPPRedist Requirements Installed.'
        } catch {
            Write-Output 'Failed to install VCPPRedist Requirements!'
            $Script:ExitCode = 1
            return
        } finally {
            Remove-Item -Path $VCPPRedistDownloadPath -Recurse -Force -ErrorAction SilentlyContinue
        }
    } else {
        Write-Output 'VCPPRedist Requirements Installed.'
    }

    # Find Latest Download
    $LatestRelease = (Invoke-RestMethod -Uri 'https://api.github.com/repos/microsoft/winget-cli/releases/latest' -Method Get)
    $LatestAsset = ($LatestRelease).assets | Where-Object { $_.name.EndsWith('.msixbundle') }
    [version]$LatestVersion = $LatestRelease.tag_name.TrimStart('v')
    if ((Find-WinGet)) {
        [version]$CurrentVersion = (& (Find-WinGet) '-v').TrimStart('v')
    } else {
        [version]$CurrentVersion = 0
    }

    # Check if WinGet is Updated
    if (!($CurrentVersion -lt $LatestVersion) -and !$Force) {
        # Exit
        Write-Output 'WinGet is up to date. Exiting.'
        return
    }

    # Installing WinGet
    Write-Output 'WinGet not installed or out of date. Installing/updating...'

    # Download WinGet to Temp
    $WebClient = New-Object System.Net.WebClient
    $WinGetDownloadPath = Join-Path -Path "$ENV:SystemRoot\Temp" -ChildPath $LatestAsset.name
    Write-Output "Downloading WinGet to $WinGetDownloadPath..."
    $WebClient.DownloadFile($LatestAsset.browser_download_url, $WinGetDownloadPath)

    # Download WinGet Source to Temp
    $WinGetSourceDownloadPath = Join-Path -Path "$ENV:SystemRoot\Temp" -ChildPath 'source.msix'
    Write-Output "Downloading WinGet to $WinGetSourceDownloadPath..."
    Invoke-WebRequest -Uri 'https://cdn.winget.microsoft.com/cache/source.msix' -OutFile $WinGetSourceDownloadPath

    $WinArch = (Get-OSInfo).Architecture

    # Download VCLibs
    $VCLibs_Path = Join-Path -Path "$ENV:SystemRoot\Temp" -ChildPath "Microsoft.VCLibs.${WinArch}.14.00.Desktop.appx"
    $VCLibs_Url = "https://aka.ms/Microsoft.VCLibs.${WinArch}.14.00.Desktop.appx"
    Write-Output 'Downloading VCLibs...'
    Write-Debug "Downloading VCLibs from $VCLibs_Url to $VCLibs_Path`n`n"
    Invoke-WebRequest -Uri $VCLibs_Url -OutFile $VCLibs_Path

    # Download UI.Xaml
    $UIXaml_Path = Join-Path -Path "$ENV:SystemRoot\Temp" -ChildPath "Microsoft.UI.Xaml.2.8.${WinArch}.appx"
    $UIXaml_Url = "https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.6/Microsoft.UI.Xaml.2.8.${WinArch}.appx"
    Write-Output 'Downloading UI.Xaml...'
    Write-Debug "Downloading UI.Xaml from $UIXaml_Url to $UIXaml_Path`n"
    Invoke-WebRequest -Uri $UIXaml_Url -OutFile $UIXaml_Path

    # Install Downloaded WinGet
    try {
        Write-Output 'Installing WinGet...'
        Add-AppxProvisionedPackage -Online -PackagePath $WinGetDownloadPath -DependencyPackagePath $UIXaml_Path, $VCLibs_Path -SkipLicense -ErrorAction Stop
        Add-AppxProvisionedPackage -Online -PackagePath $WinGetSourceDownloadPath -SkipLicense -ErrorAction Stop
        Write-Output 'WinGet installed.'
    } catch {
        Write-Output 'Failed to install WinGet!'
        $Script:ExitCode = 1
    } finally {
        Remove-Item -Path $WinGetDownloadPath -Force -ErrorAction SilentlyContinue
    }

    return
}

# Function to Find the WinGet exe (Modified)
function Find-WinGet {
    # Get the WinGet path (for use when running in SYSTEM context).
    $WinGetPathToResolve = Join-Path -Path $ENV:ProgramFiles -ChildPath 'WindowsApps\Microsoft.DesktopAppInstaller_*_*__8wekyb3d8bbwe'
    $ResolveWinGetPath = Resolve-Path -Path $WinGetPathToResolve | Sort-Object {
        [version]($_.Path -replace '^[^\d]+_((\d+\.)*\d+)_.*', '$1')
    }
    if ($ResolveWinGetPath) {
        # If we have multiple versions - use the latest.
        $WinGetPath = $ResolveWinGetPath[-1].Path
    }

    # Get the User-Context WinGet exe location.
    $WinGetExePath = Get-Command -Name winget.exe -CommandType Application -ErrorAction SilentlyContinue

    # Select the correct WinGet exe
    if (Test-Path -Path (Join-Path $WinGetPath 'winget.exe')) {
        # Running in SYSTEM-Context.
        $WinGet = Join-Path $WinGetPath 'winget.exe'
    } elseif ($WinGetExePath) {
        # Get User-Context if SYSTEM-Context not found.
        $WinGet = $WinGetExePath.Path
    } else {
        Write-Output 'WinGet not Found!'
        Stop-Transcript
        exit 1
    }

    # Return WinGet Location
    return $WinGet
}

# Test internet connection
$MaxNet = 15
Write-Output 'Checking Internet Connection...'
for ($i = 0; ($i -lt $MaxNet) -and !$net; $i++) {
    $net = Test-Connection 1.1.1.1 -Count 1 -Delay 1 -Quiet
}
if (!$net) {
    Write-Output 'No Internet Detected. Cancelling Installation.'
    Stop-Transcript
    exit 1
}

Write-Output 'Internet Detected, Continuing Installation.'

# Test OS Compatibility
$osVersion = Get-OSInfo

# If it's a workstation, make sure it is Windows 10+
if ($osVersion.Type -eq 'Workstation' -and $osVersion.NumericVersion -lt 10) {
    Write-Error 'winget requires Windows 10 or later on workstations. Your version of Windows is not supported.'
    Stop-Transcript
    exit 0
}

# If it's a workstation with Windows 10, make sure it's version 1809 or greater
if ($osVersion.Type -eq 'Workstation' -and $osVersion.NumericVersion -eq 10 -and $osVersion.ReleaseId -lt 1809) {
    Write-Error 'winget requires Windows 10 version 1809 or later on workstations. Please update Windows to a compatible version.'
    Stop-Transcript
    exit 0
}

# Run the Install/Update
Install-WinGet

& (Find-WinGet) 'list' '--accept-source-agreements'

Stop-Transcript
exit $Script:ExitCode

Categories: