install
Architecture: any
Architecture: any
name: install
variables:
# Optional: winget package ids installed machine-wide after winget, comma
# separated, e.g. Git.Git,7zip.7zip
- name: packages
value: ''
fodder:
- name: install-winget
type: shellscript
filename: install-winget.ps1
content: |
#ps1_sysnative
# Installs winget (App Installer) for all users.
#
# Fodder runs as Local System, and Add-AppxPackage refuses to install for
# that account. The package is therefore provisioned for the image with
# Add-AppxProvisionedPackage; every user gets it at logon. Dependencies and
# license come from the same winget release, so they always match.
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
if ([System.Net.ServicePointManager]::SecurityProtocol -ne 0) {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
}
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$build = [int]$os.BuildNumber
$isServer = $os.ProductType -ne 1
# The current App Installer package cannot be provisioned on Server 2019
# (DISM rejects its manifest); clients need Windows 10 1809 or later.
if (($isServer -and $build -lt 20348) -or $build -lt 17763) {
Write-Host "ERROR: winget is not supported on $($os.Caption) (build $build). Supported: Windows 10 1809+, Windows 11, Windows Server 2022+."
exit 1
}
$arch = switch ($env:PROCESSOR_ARCHITECTURE) {
'AMD64' { 'x64' }
'ARM64' { 'arm64' }
default { 'x86' }
}
$release = Invoke-RestMethod -Uri 'https://api.github.com/repos/microsoft/winget-cli/releases/latest' -UseBasicParsing
function Get-ReleaseAsset([string]$pattern) {
$asset = $release.assets | Where-Object { $_.name -like $pattern } | Select-Object -First 1
if (-not $asset) { throw "winget release $($release.tag_name) has no asset matching '$pattern'" }
return $asset
}
$bundleVersion = $release.tag_name -replace '^v', ''
$installed = Get-AppxProvisionedPackage -Online | Where-Object DisplayName -eq 'Microsoft.DesktopAppInstaller'
Write-Host "winget release: $bundleVersion; provisioned App Installer: $(if ($installed) { $installed.Version } else { 'none' })"
$tempDir = Join-Path $env:TEMP 'winget-install'
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
$bundlePath = Join-Path $tempDir 'Microsoft.DesktopAppInstaller.msixbundle'
$dependenciesZip = Join-Path $tempDir 'dependencies.zip'
$licensePath = Join-Path $tempDir 'license.xml'
Write-Host "Downloading winget $bundleVersion..."
Invoke-WebRequest -Uri (Get-ReleaseAsset 'Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle').browser_download_url -OutFile $bundlePath -UseBasicParsing
Invoke-WebRequest -Uri (Get-ReleaseAsset 'DesktopAppInstaller_Dependencies.zip').browser_download_url -OutFile $dependenciesZip -UseBasicParsing
Invoke-WebRequest -Uri (Get-ReleaseAsset '*_License1.xml').browser_download_url -OutFile $licensePath -UseBasicParsing
Expand-Archive -Path $dependenciesZip -DestinationPath (Join-Path $tempDir 'dependencies') -Force
$dependencies = @(Get-ChildItem (Join-Path $tempDir "dependencies\$arch") -Filter *.appx | ForEach-Object FullName)
if ($dependencies.Count -eq 0) {
throw "The winget dependencies contain no packages for $arch"
}
Write-Host "Dependencies: $(($dependencies | ForEach-Object { Split-Path $_ -Leaf }) -join ', ')"
Write-Host "Provisioning winget for all users..."
Add-AppxProvisionedPackage -Online -PackagePath $bundlePath -DependencyPackagePath $dependencies -LicensePath $licensePath | Out-Null
}
finally {
Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
$provisioned = Get-AppxProvisionedPackage -Online | Where-Object DisplayName -eq 'Microsoft.DesktopAppInstaller'
if (-not $provisioned) {
Write-Host "ERROR: App Installer is not provisioned after installation"
exit 1
}
Write-Host "winget provisioned: App Installer $($provisioned.Version). Users get it at their next logon."
# The winget CLI does not run as Local System out of the box: started outside
# a package context it can't find its dependency DLLs (0xC0000135). This
# wrapper puts them on the PATH and runs the newest winget.exe, so fodder
# running as Local System can use winget (machine scope, winget source):
# & C:\ProgramData\eryph\winget\winget-system.ps1 install --id Git.Git --exact --scope machine ...
$wrapperDir = Join-Path $env:ProgramData 'eryph\winget'
New-Item -ItemType Directory -Path $wrapperDir -Force | Out-Null
$wrapper = Join-Path $wrapperDir 'winget-system.ps1'
Set-Content -Path $wrapper -Encoding UTF8 -Value @'
# Runs winget as Local System. Written by gene:dbosoft/winget:install.
$arch = switch ($env:PROCESSOR_ARCHITECTURE) { 'AMD64' { 'x64' } 'ARM64' { 'arm64' } default { 'x86' } }
$apps = Join-Path $env:ProgramFiles 'WindowsApps'
function Get-NewestPackage([string]$name) {
Get-ChildItem $apps -Directory -Filter "$($name)_*_$($arch)__8wekyb3d8bbwe" -ErrorAction SilentlyContinue |
Sort-Object { [version](($_.Name -split '_')[1]) } | Select-Object -Last 1
}
$appInstaller = Get-NewestPackage 'Microsoft.DesktopAppInstaller'
if (-not $appInstaller) { Write-Error 'winget (App Installer) is not installed'; exit 1 }
$dependencies = @('Microsoft.VCLibs.140.00.UWPDesktop', 'Microsoft.VCLibs.140.00') + @(
Get-ChildItem $apps -Directory -Filter "Microsoft.WindowsAppRuntime.*_$($arch)__8wekyb3d8bbwe" -ErrorAction SilentlyContinue |
ForEach-Object { ($_.Name -split '_')[0] } | Sort-Object -Unique)
$paths = @($appInstaller.FullName) + @($dependencies | ForEach-Object { (Get-NewestPackage $_).FullName } | Where-Object { $_ })
$env:PATH = ($paths -join ';') + ';' + $env:PATH
& (Join-Path $appInstaller.FullName 'winget.exe') @args
exit $LASTEXITCODE
'@
Write-Host "winget wrapper for Local System: $wrapper"
& $wrapper --version
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: winget does not run as Local System (exit code $LASTEXITCODE)"
exit 1
}
$packages = @('{{ packages }}' -split '[,\s]+' | Where-Object { $_ })
$failed = @()
foreach ($package in $packages) {
Write-Host "Installing $package..."
& $wrapper install --id $package --exact --source winget --scope machine --silent `
--accept-source-agreements --accept-package-agreements --disable-interactivity
# 0x8A15002B: no newer version available, 0x8A150061: already installed
if ($LASTEXITCODE -notin 0, -1978335189, -1978335135) {
Write-Host "ERROR: installing $package failed with exit code $LASTEXITCODE"
$failed += $package
}
}
if ($failed) {
Write-Host "ERROR: failed packages: $($failed -join ', ')"
exit 1
}