-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinstall-common.ps1
439 lines (403 loc) · 15.3 KB
/
install-common.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
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
#-----------------------------------------------------------------------------
#
# Copyright (c) 2022, Thierry Lelegard
# BSD-2-Clause license, see LICENSE.txt file
#
# Common part for all install scripts.
# Not a PowerShell module for easier integration of variables.
#
# Must be included as follow by all scripts:
# . "$PSScriptRoot\install-common.ps1"
#
# Assume that the calling script defined standard installation parameters:
#
# -Destination directory
# Specify a local directory where the package will be downloaded.
# By default, use the downloads folder for the current user.
# Sometimes unused if there is no external package to download
# (Windows buildin features for instance).
#
# -ForceDownload
# Force a download even if the package is already downloaded.
# Sometimes unused if there is no external package to download.
#
# -GitHubActions
# When used in a GitHub Action workflow, make sure that the required
# environment variables are propagated to subsequent jobs. Ignored when
# the installation of the package does not need specific variables.
#
# -NoInstall
# Do not install the package. Download it only. By default, the package
# is installed.
#
# -NoPause
# Do not wait for the user to press <enter> at end of execution.
# By default, execute a "pause" instruction at the end of execution,
# which is useful when the script was run from Windows Explorer.
#
#-----------------------------------------------------------------------------
# Current user environment.
$IsAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
$CurrentUserName = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$AdminUserName = (Get-CimInstance -ClassName Win32_UserAccount -Filter "LocalAccount = TRUE and SID like 'S-1-5-%-500'").Name
$AdminGroupName = (Get-CimInstance -ClassName Win32_Group -Filter "LocalAccount = TRUE and SID like 'S-1-5-%-544'").Name
# Without this, Invoke-WebRequest is awfully slow.
$ProgressPreference = 'SilentlyContinue'
# Force TLS 1.2 as default.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# User agent for downloads.
$UserAgent = "Wget"
# Retry when downloading packages.
$DownloadRetryCount = 3
$DownloadRetrySeconds = 5
# Create the directory for external products or use default.
if (-not $Destination) {
$Destination = (New-Object -ComObject Shell.Application).NameSpace('shell:Downloads').Self.Path
}
[void](New-Item -Path $Destination -ItemType Directory -Force)
# A function to exit this script.
function Exit-Script([string]$Message = "")
{
$Code = 0
if ($Message -ne "") {
Write-Host "ERROR: $Message"
$Code = 1
}
if (-not $NoPause) {
pause
}
exit $Code
}
# Recurse the current script in administrator mode.
function Recurse-Admin([string]$CmdArgs = "")
{
Write-Output "Must be administrator to continue, trying to restart as administrator ..."
$cmd = "& '" + $MyInvocation.PSCommandPath + "' " + $CmdArgs
if ($NoPause) {
$cmd += " -NoPause"
}
Start-Process -Wait -Verb runas -FilePath PowerShell.exe -ArgumentList @("-ExecutionPolicy", "RemoteSigned", "-Command", $cmd)
}
# Convert "something" into an integer. Depending on the version of PowerShell,
# the integer data in a Web response can be in a string or in an array of strings.
function To-Int($Data, $Default = $null)
{
$Previous = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
if ($Data -eq $null) {
return $Default
}
if ($Data.GetType().IsPrimitive) {
return [int]$Data
}
if ($Data.GetType().IsArray) {
foreach ($elem in $Data) {
$i = To-Int $elem $null
if ($i -ne $null) {
return $i
}
}
return $Default
}
if ($Data.GetType().UnderlyingSystemType.Name -like "Hashtable") {
foreach ($elem in $Data.Values) {
$i = To-Int $elem $null
if ($i -ne $null) {
return $i
}
}
return $Default
}
return [int]$Data.ToString()
}
catch {
return $Default
}
finally {
$ErrorActionPreference = $Previous
}
}
# Install a Windows Capability.
function Install-Windows-Capability([string]$Name)
{
$product = (Get-WindowsCapability -Online | Where-Object Name -like "${Name}*" | Select-Object -First 1)
if ($product -eq $null) {
Write-Output "$Name not found"
}
elseif ($product.State -like "Installed") {
Write-Output "$($product.Name) already installed"
}
else {
Write-Output "Installing $($product.Name) ..."
[void](Add-WindowsCapability -Online -Name $product.Name)
}
}
# Create a file when non-existent. Restrict its protection to a list of owners.
function Create-File-Set-Owner([string]$Path, [string[]]$Owners = @())
{
# Create parent directory if non-existent.
$dir = (Split-Path $Path -Parent)
if (-not (Test-Path $dir -PathType Container)) {
Write-Output "Creating directory $dir ..."
[void](New-Item $dir -ItemType Directory -Force)
}
# Create file if non-existent.
if (-not (Test-Path $Path -PathType Leaf)) {
Write-Output "Creating file $Path ..."
[void](New-Item $Path -ItemType File -Force)
}
# Reset ownership.
Write-Output "Adjusting security of $Path ..."
$acl = Get-Acl $Path
$acl.SetAccessRuleProtection($true, $false)
foreach ($user in $Owners) {
$rule = New-Object system.security.accesscontrol.filesystemaccessrule($user, "FullControl", "Allow")
$acl.SetAccessRule($rule)
}
$acl | Set-Acl
}
# Get the local file name part of a URL.
function Get-URL-Local([string]$Url)
{
return (Split-Path -Leaf ([System.Uri]$Url).LocalPath)
}
# Get an HTML page. Return $null if not found.
function Get-HTML([string]$Url, [switch]$Fatal = $false)
{
$status = 0
$message = ""
try {
$response = Invoke-WebRequest -UseBasicParsing -UserAgent $UserAgent -Uri $Url
$status = [int][Math]::Floor($response.StatusCode / 100)
}
catch {
$message = $_.Exception.Message
}
if ($status -ne 1 -and $status -ne 2) {
# Error fetching download page.
if ($message -eq "" -and (Test-Path variable:response)) {
$message = "Status code $($response.StatusCode), $($response.StatusDescription)"
}
else {
$message = "#### Error accessing ${Url}: $message"
}
if ($Fatal) {
Exit-Script $message
}
Write-Output $message
$reponse = $null
}
return $response
}
# Get an URL matching a pattern in a web page.
function Get-URL-In-HTML([string]$Url, [string]$Pattern, [string]$FallbackURL = "")
{
$Fatal = -not $FallbackURL
$response = Get-HTML $Url -Fatal:$Fatal
if ($response -eq $null) {
$ref = $null
}
else {
$ref = $response.Links.href | Where-Object { $_ -like $Pattern } | Select-Object -First 1
}
if (-not -not $ref) {
# Build the absolute URL's from base URL (the download page) and href links.
return [string](New-Object -TypeName 'System.Uri' -ArgumentList ([System.Uri]$Url, $ref))
}
elseif ($Fatal) {
Exit-Script "No package found in $Url"
}
else {
return $FallbackURL
}
}
# Get the JSON description of the 20 latest releases of a repo in GitHub.
# With -Latest, only check the release which is labelled "latest".
function Get-Releases-In-GitHub([string]$Repo, [switch]$Latest = $false)
{
# If the environment variable GITHUB_TOKEN is not empty, use it to authenticate.
if (-not $env:GITHUB_TOKEN) {
$Headers = @{}
}
else {
$Cred = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($env:GITHUB_TOKEN)"))
$Headers = @{Authorization="Basic $Cred"}
}
$Request = if ($Latest) { "releases/latest" } else { "releases?per_page=20" }
$Response = (Invoke-WebRequest -UseBasicParsing -UserAgent $UserAgent -Headers $Headers -Uri "https://api.github.com/repos/$Repo/$Request")
$Remain = (To-Int $Response.Headers['X-RateLimit-Remaining'] 99999)
if ($Remain -lt 10) {
Write-Output "Warning: GitHub API rate limit remaining is $Remain"
}
return (ConvertFrom-Json $Response.Content)
}
# Get an URL matching a pattern in a GitHub project release.
function Get-URL-In-GitHub([string]$Repo, $Patterns, [switch]$Latest = $false)
{
$Url = (Get-Releases-In-GitHub $Repo -Latest:$Latest |
ForEach-Object { $_.assets } |
ForEach-Object { $_.browser_download_url } |
Select-String $Patterns |
Select-Object -First 1)
if (-not $Url) {
Exit-Script "No package matching '$Patterns' in GitHub repo $Repo"
}
else {
return [string]$Url
}
}
# Download a package.
function Download-Package([string]$Url, [string]$InstallerPath)
{
if (-not $ForceDownload -and (Test-Path $InstallerPath)) {
$InstallerName = (Split-Path -Leaf $InstallerPath)
Write-Output "$InstallerName already downloaded, use -ForceDownload to download again"
}
else {
# We do some retries since some sites are sometimes not responsive (seen on sourceforge).
for ($i=1; $i -le $DownloadRetryCount; $i++) {
Write-Output "Downloading $Url ..."
# If the environment had set "stop on error", suspend and restore it after download.
$Previous = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
Invoke-WebRequest -UseBasicParsing -UserAgent $UserAgent -Uri $Url -OutFile $InstallerPath
$ErrorActionPreference = $Previous
if ((Test-Path $InstallerPath) -or ($i -ge $DownloadRetryCount)) {
break
}
Write-Output "Downloaded failed, will retry in $DownloadRetrySeconds seconds"
Start-Sleep -Seconds $DownloadRetrySeconds
}
if (-not (Test-Path $InstallerPath)) {
Exit-Script "$Url download failed"
}
}
}
# Standard installation procedure for an executable installer, to be searched in a Web page.
function Install-Standard-Exe([string]$ReleasePage, [string]$Pattern, [string]$FallbackURL = "", [string[]]$InstallerParams = @())
{
$Url = Get-URL-In-HTML $ReleasePage $Pattern $FallbackURL
$InstallerName = Get-URL-Local $Url
$InstallerPath = "$Destination\$InstallerName"
Download-Package $Url $InstallerPath
if (-not $NoInstall) {
Write-Output "Installing $InstallerName"
Start-Process -Wait -FilePath $InstallerPath -ArgumentList $InstallerParams
}
}
# Standard installation procedure for an executable installer from GitHub release assets.
# With -Latest, only check the release which is labelled "latest".
function Install-GitHub-Exe([string]$Repo, [string]$Pattern, [string[]]$InstallerParams = @(), [switch]$Latest = $false)
{
$Url = Get-URL-In-GitHub $Repo $Pattern -Latest:$Latest
$InstallerName = Get-URL-Local $Url
$InstallerPath = "$Destination\$InstallerName"
Download-Package $Url $InstallerPath
if (-not $NoInstall) {
Write-Output "Installing $InstallerName"
Start-Process -Wait -FilePath $InstallerPath -ArgumentList $InstallerParams
}
}
# Installation procedure for an MSI installer, from it URL.
function Install-Msi([string]$Url)
{
$InstallerName = Get-URL-Local $Url
$InstallerPath = "$Destination\$InstallerName"
Download-Package $Url $InstallerPath
if (-not $NoInstall) {
Write-Output "Installing $InstallerName"
Start-Process -Wait -Verb runas -FilePath msiexec.exe -ArgumentList @("/i", $InstallerPath, "/quiet", "/qn", "/norestart")
}
}
# Standard installation procedure for an MSI installer, to be searched in a Web page.
function Install-Standard-Msi([string]$ReleasePage, [string]$Pattern, [string]$FallbackURL = "")
{
$Url = Get-URL-In-HTML $ReleasePage $Pattern $FallbackURL
Install-Msi $Url
}
# Get user environment variable.
function Get-UserEnvironment([string]$Name)
{
return [System.Environment]::GetEnvironmentVariable($Name, [System.EnvironmentVariableTarget]::User)
}
# Define user environment variable.
function Define-UserEnvironment([string]$Name, [string]$Value)
{
[System.Environment]::SetEnvironmentVariable($Name, $Value, [System.EnvironmentVariableTarget]::User)
}
# Get system-wide environment variable.
function Get-Environment([string]$Name)
{
return [System.Environment]::GetEnvironmentVariable($Name, [System.EnvironmentVariableTarget]::Machine)
}
# Define system-wide environment variable.
function Define-Environment([string]$Name, [string]$Value)
{
[System.Environment]::SetEnvironmentVariable($Name, $Value, [System.EnvironmentVariableTarget]::Machine)
}
# Propagate an environment variable in next jobs for GitHub Actions.
# If value is unspecified, get it from system environment.
function Propagate-Environment([string]$Name, [string]$Value = "")
{
if ($GitHubActions) {
if ($Value -eq "") {
$Value = Get-Environment $Name
}
Write-Output "${Name}=${Value}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
}
}
# Add a directory in a system path.
function Add-Directory-To-Path([string]$Dir, [string]$PathName = "Path")
{
$Value = Get-Environment $PathName
if (";$Value;" -notlike "*;$Dir;*") {
Write-Output "Adding $Dir to $PathName"
Define-Environment $PathName "$Value;$Dir"
}
}
# Add a shortcut in the startup menu.
function Add-Start-Menu-Entry([string]$Name, [string]$Target, [string]$MenuSubDir = "", $AllUsers = $false)
{
if (Test-Path "$Target") {
if ($AllUsers) {
$MenuDir = [Environment]::GetFolderPath('CommonStartMenu') + "\Programs"
}
else {
$MenuDir = [Environment]::GetFolderPath('StartMenu') + "\Programs"
}
if ($MenuSubDir -ne "") {
$MenuDir += "\$MenuSubDir"
if (-not (Test-Path -PathType Container $MenuDir)) {
[void](New-Item $MenuDir -ItemType Directory)
}
}
Remove-Item "$MenuDir\$Name.lnk" -Force -ErrorAction Ignore
$WScriptShell = New-Object -ComObject WScript.Shell
$Shortcut = $WScriptShell.CreateShortcut("$MenuDir\$Name.lnk")
$Shortcut.TargetPath = $Target
$Shortcut.Save()
}
}
# Search a file in a path.
function Search-Path([string]$Name, [string]$Path = $env:Path)
{
foreach ($dir in $env:Path.Split(';')) {
if (Test-Path "$dir\$Name") {
return "$dir\$Name"
}
}
return $null
}
# Send a WM_SETTINGCHANGE message to all applications
Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @"
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);
"@
function Send-SettingChange
{
$HWND_BROADCAST = [IntPtr]0xffff;
$WM_SETTINGCHANGE = 0x1a;
$result = [UIntPtr]::Zero
[void]([Win32.Nativemethods]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE, [UIntPtr]::Zero, "Environment", 2, 5000, [ref]$result))
}