-
Notifications
You must be signed in to change notification settings - Fork 15
/
Winget-InstallPackage.ps1
345 lines (321 loc) · 12.8 KB
/
Winget-InstallPackage.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
<#
.NOTES
===========================================================================
Created with: SAPIEN Technologies, Inc., PowerShell Studio 2021 v5.8.195
Created on: 3/7/2022 2:14 PM
Created by: Dave Just
Organization:
Filename: Winget-InstallPackage.ps1
Updated 07/15/2024 to support uninstallation, also handle scenario when WinGet is not installed
but package installation is requested during autopilot / oobe without a user account. Moved log location
to %programdata%\Microsoft\IntuneManagementExtension\Logs
Updated 08/12/2024 to download and extract DesktopAppInstaller msixbundle to %ProgramData% if WinGet is not installed
This allows WinGet to function in SYSTEM context during OOBE/Autopilot when installation under user context is not possbile.
===========================================================================
.DESCRIPTION
Installs any package within the WinGet public repository running as SYSTEM. Can be packaged and deployed as a Win32App in Intune
Use as base for any install with WinGet. Simply specify the PackageID and log variables.
If WinGet is not currently installed, a zipped copy will be extracted to the %PrograData% folder as installation in user context during OOBE/Autopilot was un-reliable.
.PARAMETER PackageID
Specify the WinGet ID. Use WinGet Search "SoftwareName" to locate the PackageID
.PARAMETER PackageName
Specify the Package Name in place of PackageID if preferred.
.PARAMETER Mode
Required parameter. Specify 'install' or 'uninstall' to perform that operation for the given package.
.PARAMETER Log
Required parameter. Specify the log file name. This will be logged to %programdata%\Microsoft\IntuneManagementExtension\Logs"
.PARAMETER AdditionalInstallArgs
Specify Additional Installation Arguments to pass to WinGet https://learn.microsoft.com/en-us/windows/package-manager/winget/install
.EXAMPLE
powershell.exe -executionpolicy bypass -file Winget-InstallPackage.ps1 -PackageID "Google.Chrome" -Log "ChromeWingetInstall.log"
.EXAMPLE
powershell.exe -executionpolicy bypass -file Winget-InstallPackage.ps1 -mode install -PackageID "Notepad++.Notepad++" -Log "NotepadPlusPlus.log"
.EXAMPLE
powershell.exe -executionpolicy bypass -file Winget-InstallPackage.ps1 -PackageID "Python.Python.3.11" -mode uninstall -Log "Python3Install.log" -AdditionalInstallArgs "--architecture x64"
.EXAMPLE
powershell.exe -executionpolicy bypass -file Winget-InstallPackage.ps1 -PackageName "Microsoft .NET Runtime 6.0" -mode install -Log "DotNetRuntime6.log"
#>
<#
TODO:
Add function to download zipped WinGet copy from blob storage in the event that WinGet is not installed
Extract to %ProgramData% folder
#>
param (
[string]$PackageID = 'Notepad++.Notepad++',
[string]$PackageName,
[string]$AdditionalInstallArgs,
[parameter()]
[ValidateSet('Install','Uninstall')]
# Set default mode to install
[string]$Mode = 'Install',
[parameter(Mandatory)]
[string]$Log
)
# Re-launch as 64bit process (source: https://z-nerd.com/blog/2020/03/31-intune-win32-apps-powershell-script-installer/)
$argsString = ""
If ($ENV:PROCESSOR_ARCHITEW6432 -eq "AMD64")
{
Try
{
foreach ($k in $MyInvocation.BoundParameters.keys)
{
switch ($MyInvocation.BoundParameters[$k].GetType().Name)
{
"SwitchParameter" { if ($MyInvocation.BoundParameters[$k].IsPresent) { $argsString += "-$k " } }
"String" { $argsString += "-$k `"$($MyInvocation.BoundParameters[$k])`" " }
"Int32" { $argsString += "-$k $($MyInvocation.BoundParameters[$k]) " }
"Boolean" { $argsString += "-$k `$$($MyInvocation.BoundParameters[$k]) " }
}
}
Start-Process -FilePath "$ENV:WINDIR\SysNative\WindowsPowershell\v1.0\PowerShell.exe" -ArgumentList "-File `"$($PSScriptRoot)\Winget-InstallPackage.ps1`" $($argsString)" -Wait -NoNewWindow
}
Catch
{
Throw "Failed to start 64-bit PowerShell"
}
Exit
}
#region HelperFunctions
function Write-Log($message) #Log script messages to temp directory
{
$LogMessage = ((Get-Date -Format "MM-dd-yy HH:MM:ss ") + $message)
if (Test-Path "$env:programdata\Microsoft\IntuneManagementExtension\Logs") {
Out-File -InputObject $LogMessage -FilePath "$env:programdata\Microsoft\IntuneManagementExtension\Logs\$Log" -Append -Encoding utf8
}
Write-Host $message
}
function Download-Winget {
<#
.SYNOPSIS
Download Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle and extract contents with 7zip cli to %ProgramData%
#>
$ProgressPreference = 'SilentlyContinue'
$7zipFolder = "${env:WinDir}\Temp\7zip"
try {
Write-Log "Downloading WinGet..."
# Create staging folder
New-Item -ItemType Directory -Path "${env:WinDir}\Temp\WinGet-Stage" -Force
# Download Desktop App Installer msixbundle
Invoke-WebRequest -UseBasicParsing -Uri https://aka.ms/getwinget -OutFile "${env:WinDir}\Temp\WinGet-Stage\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"
}
catch {
Write-Log "Failed to download WinGet!"
Write-Log $_.Exception.Message
return
}
try {
Write-Log "Downloading 7zip CLI executable..."
# Create temp 7zip CLI folder
New-Item -ItemType Directory -Path $7zipFolder -Force
Invoke-WebRequest -UseBasicParsing -Uri https://www.7-zip.org/a/7zr.exe -OutFile "$7zipFolder\7zr.exe"
Invoke-WebRequest -UseBasicParsing -Uri https://www.7-zip.org/a/7z2408-extra.7z -OutFile "$7zipFolder\7zr-extra.7z"
Write-Log "Extracting 7zip CLI executable to ${7zipFolder}..."
& "$7zipFolder\7zr.exe" x "$7zipFolder\7zr-extra.7z" -o"$7zipFolder" -y
}
catch {
Write-Log "Failed to download 7zip CLI executable!"
Write-Log $_.Exception.Message
return
}
try {
# Create Folder for DesktopAppInstaller inside %ProgramData%
New-Item -ItemType Directory -Path "${env:ProgramData}\Microsoft.DesktopAppInstaller" -Force
Write-Log "Extracting WinGet..."
& "$7zipFolder\7za.exe" x "${env:WinDir}\Temp\WinGet-Stage\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" -o"${env:WinDir}\Temp\WinGet-Stage" -y
& "$7zipFolder\7za.exe" x "${env:WinDir}\Temp\WinGet-Stage\AppInstaller_x64.msix" -o"${env:ProgramData}\Microsoft.DesktopAppInstaller" -y
}
catch {
Write-Log "Failed to extract WinGet!"
Write-Log $_.Exception.Message
return
}
if (-Not (Test-Path "${env:ProgramData}\Microsoft.DesktopAppInstaller\WinGet.exe")){
Write-Log "Failed to extract WinGet!"
exit 1
}
$Script:WinGet = "${env:ProgramData}\Microsoft.DesktopAppInstaller\WinGet.exe"
}
function Install-VisualC {
try {
$downloadurl = 'https://aka.ms/vs/17/release/vc_redist.x64.exe'
$WebClient = New-Object System.Net.WebClient
$WebClient.DownloadFile($downloadurl, "$env:Temp\vc_redist.x64.exe")
$WebClient.Dispose()
}
catch {
Write-Log "Failed to download Visual C++!"
Write-Log $_.Exception.Message
}
# Check if another installation is in progress, then wait for it to complete
$MSIExecCheck = Get-Process | Where-Object {$_.processname -eq 'msiexec'}
if ($Null -ne $MSIExecCheck){
Write-Log "another msi installation is in progress. Waiting for process to complete..."
Wait-Process msiexec
Write-Log "Continuing installation..."
}
try {
$Install = start-process "$env:temp\vc_redist.x64.exe" -argumentlist "/q /norestart" -Wait -PassThru
Write-Log "Installation completed with exit code $($Install.ExitCode)"
return $Install.ExitCode
}
catch {
Write-Log $_.Exception.Message
}
try {
remove-item "$env:Temp\vc_redist.x64.exe"
}
catch {
Write-Log "Failed to remove vc_redist.x64.exe after installation"
}
}
function Get-RegUninstallKey
{
param (
[string]$DisplayName
)
$uninstallKeys = @(
"registry::HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall"
"registry::HKLM\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
)
$LoggedOnUser = (Get-CimInstance -ClassName Win32_ComputerSystem).UserName
if ($LoggedOnUser){
$UserSID = ([System.Security.Principal.NTAccount](Get-CimInstance -ClassName Win32_ComputerSystem).UserName).Translate([System.Security.Principal.SecurityIdentifier]).Value
$UninstallKeys += @("registry::HKU\$UserSID\Software\Microsoft\Windows\CurrentVersion\Uninstall" , "registry::HKU\$UserSID\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall")
}
$softwareTable =@()
foreach ($key in $uninstallKeys){
if (-Not (Test-Path $Key)){
Write-Warning "$Key not found"
continue
}
$softwareTable += Get-Childitem $key |
ForEach-Object {
try {
Get-ItemProperty $_.pspath | Where-Object { $_.displayname } | Sort-Object -Property displayname
}
catch [System.InvalidCastException] {
# Ignore error as I was occasionally getting an invalid cast error on Get-ItemProperty
}
}
}
if ($DisplayName)
{
$softwareTable | Where-Object { $_.displayname -Like "*$DisplayName*" }
}
else
{
$softwareTable | Sort-Object -Property displayname -Unique
}
}
function WingetInstallPackage {
param (
$PackageID,
$PackageName,
$AdditionalInstallArgs
)
# Check if another msi install is in progress and wait
$MSIExecCheck = Get-Process | Where-Object {$_.processname -eq 'msiexec'}
if ($Null -ne $MSIExecCheck){
Write-Log "another msi installation is in progress. Waiting for process to complete..."
Wait-Process msiexec
Write-Log "Continuing installation..."
}
if ($PackageID){
& $Winget install --id $PackageID --source Winget --silent $AdditionalInstallArgs --accept-package-agreements --accept-source-agreements
}
elseif ($PackageName){
& $Winget install --name $PackageName --source Winget --silent $AdditionalInstallArgs --accept-package-agreements --accept-source-agreements
}
}
function Resolve-WinGetPath {
# Look for Winget install in WindowsApps folder
$WinAppFolderPath = Get-ChildItem -path "$env:ProgramFiles/WindowsApps" -recurse -filter "winget.exe" | where {$_.VersionInfo.FileVersion -ge 1.23}
if ($WinAppFolderPath){
$script:WinGet = $WinAppFolderPath | Select-Object -ExpandProperty Fullname | Sort-Object -Descending | Select-Object -First 1
Write-Log "WinGet.exe found at path $Winget"
}
else {
# Check if WinGet copy has already been extracted to ProgramData folder
if (Test-Path "${env:ProgramData}\Microsoft.DesktopAppInstaller\WinGet.exe"){
Write-Log "WinGet.exe found in ${env:ProgramData}\Microsoft.DesktopAppInstaller}"
$Script:WinGet = "${env:ProgramData}\Microsoft.DesktopAppInstaller\WinGet.exe"
}
else {
# Download WinGet MSIX bundle and extract files to ProgramData folder
Write-Log "WinGet.exe not found in ${env:ProgramData}\Microsoft.DesktopAppInstaller"
Download-Winget
}
}
}
function Test-WinGetOutput {
if (-Not (Test-Path $Winget)){
Write-Log "WinGet path not found at Test-WinGetOutput function!"
Write-Log "WinGet variable : $WinGet"
exit 1
}
$OutputTest = & $WinGet
if ([string]::IsNullOrEmpty($OutputTest)){
Write-Log "WinGet executable test failed to produce output!"
exit 1
}
}
#endregion HelperFunctions
#region Script
$VisualC = Get-RegUninstallKey -DisplayName "Microsoft Visual C++ 2015-2022 Redistributable (x64)"
# Get path for Winget executible
Resolve-WinGetPath
# If Visual C++ Redist. not installed, install it
if (-Not $VisualC){
Write-Log -message "Visual C++ X64 not found. Attempting to install"
try {
$VisualCInstall = Install-VisualC
}
catch [System.InvalidOperationException]{
Write-Log -message "Error installing visual c++ redistributable. Attempting install once more"
Start-Sleep -Seconds 30
$VisualCInstall = Install-VisualC
}
catch {
Write-Log -message "Failed to install visual c++ redistributable!"
Write-Log -message $_
exit 1
}
if ($VisualCInstall -ne 0){
Write-Log -message "Visual C++ X64 install failed. Exit code : $VisualCInstall"
exit 1
}
Test-WinGetOutput
}
try
{
switch ($Mode){
'Install'{
if ($PackageID){
Write-Log -message "executing $Mode on $PackageID"
$Install = WingetInstallPackage -PackageID $PackageID -AdditionalArgs $AdditionalInstallArgs
}
elseif ($PackageName){
Write-Log -message "executing $Mode on $PackageName"
$Install = WingeInstallPackage -PackageName $PackageName -AdditionalArgs $AdditionalInstallArgs
}
Write-Log $Install
}
'Uninstall'{
if ($PackageID){
Write-Log -message "executing $Mode on $PackageID"
$Uninstall = & $Winget uninstall --id $PackageID --source WinGet --silent
}
elseif ($PackageName){
Write-Log -message "executing $Mode on $PackageName"
$Uninstall = & $Winget uninstall --name $PackageName --source WinGet --silent
}
Write-Log $Uninstall
}
}
}
Catch
{
Write-Log $error[0]
exit 1
}
#endregion