Adds two winget-sourced Windows FMAs:
- R (RProject.R) -> 'R for Windows', Inno Setup exe, machine scope
- Git (Git.Git) -> 'Git', Inno Setup exe, machine scope
Both use custom Inno install/uninstall scripts (no MSI ProductCode) with
registry-UninstallString-based uninstall and fuzzy name matching, since
their ARP DisplayName embeds the version ('R for Windows <ver>', 'Git
version <ver>').
test-fma-windows-pr-only.yml: add has_r/has_git detection and removal of
the runner's pre-installed R and Git. Git for Windows provides the Git
Bash 'bash' the workflow uses, so the apps.json filtering is split into
its own step that runs before Git removal, and validation runs 'go run
-buildvcs=false' (so Go does not invoke the now-removed git for VCS
stamping).
SSMS was investigated but intentionally skipped: SSMS 21/22 is a Visual
Studio online bootstrapper (no MSI ProductCode, multi-GB network
install), which is a fragile FMA candidate, and the runner has no
standalone SSMS to uninstall.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added support for Git (Windows) with automated install/uninstall and
UI icon.
* Added support for R for Windows with automated install/uninstall and
UI icon.
* Windows testing workflow now detects and conditionally prepares
environments when Git or R are present.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
100 lines
3.7 KiB
PowerShell
100 lines
3.7 KiB
PowerShell
# Locates Git for Windows' Inno Setup uninstaller from the registry and runs it
|
|
# silently. The registry DisplayName is not reliably "Git version <ver>" (e.g. the
|
|
# GitHub-hosted runner's Git is listed as just "Git"), so anchor the match on the
|
|
# publisher -- which is unique to Git for Windows -- and only loosely guard the
|
|
# DisplayName. This mirrors the generated exists query's publisher clause.
|
|
|
|
$displayNameLike = "Git*"
|
|
$publisherLike = "The Git Development Community*"
|
|
|
|
$paths = @(
|
|
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
|
|
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
|
|
)
|
|
|
|
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
|
|
|
|
$selected = $null
|
|
foreach ($p in $paths) {
|
|
$items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
|
|
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
|
|
}
|
|
if ($items) { $selected = $items | Select-Object -First 1; break }
|
|
}
|
|
|
|
if (-not $selected -or (-not $selected.UninstallString -and -not $selected.QuietUninstallString)) {
|
|
Write-Host "Uninstall entry not found"
|
|
Exit 0
|
|
}
|
|
|
|
# Best-effort: stop Git-related processes so the uninstaller doesn't fail on locked files.
|
|
foreach ($proc in @('git', 'bash', 'sh', 'ssh-agent', 'gitk', 'wish')) {
|
|
Stop-Process -Name $proc -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
$uninstallCommand = if ($selected.QuietUninstallString) { $selected.QuietUninstallString } else { $selected.UninstallString }
|
|
|
|
# Split the uninstall string into exe + args, handling quoted, unquoted-with-spaces,
|
|
# and bare-token forms.
|
|
$exePath = ""
|
|
$existingArgs = ""
|
|
if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') {
|
|
$exePath = $matches[1]; $existingArgs = $matches[2].Trim()
|
|
} elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
|
|
$exePath = $matches[1]; $existingArgs = $matches[2].Trim()
|
|
} elseif ($uninstallCommand -match '^\s*(\S+)\s*(.*)$') {
|
|
$exePath = $matches[1]; $existingArgs = $matches[2].Trim()
|
|
} else {
|
|
Throw "Could not parse uninstall string: $uninstallCommand"
|
|
}
|
|
|
|
# Inno Setup silent uninstall flags.
|
|
foreach ($flag in @('/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART')) {
|
|
if ($existingArgs -notmatch [regex]::Escape($flag)) {
|
|
$existingArgs = ("$existingArgs $flag").Trim()
|
|
}
|
|
}
|
|
|
|
Write-Host "Uninstall command: $exePath"
|
|
Write-Host "Uninstall args: $existingArgs"
|
|
|
|
try {
|
|
$processOptions = @{
|
|
FilePath = $exePath
|
|
ArgumentList = $existingArgs
|
|
NoNewWindow = $true
|
|
PassThru = $true
|
|
Wait = $true
|
|
}
|
|
$process = Start-Process @processOptions
|
|
$exitCode = $process.ExitCode
|
|
Write-Host "Uninstall exit code: $exitCode"
|
|
|
|
# Inno Setup's unins000.exe relaunches itself (it copies to a temp _iu*.tmp and
|
|
# spawns that copy), so the process we waited on returns BEFORE the uninstall
|
|
# has finished. Poll the registry until the entry is gone so the post-uninstall
|
|
# state is consistent for callers (e.g. Fleet's install/uninstall verification).
|
|
$deadline = (Get-Date).AddSeconds(120)
|
|
do {
|
|
Start-Sleep -Seconds 2
|
|
$stillPresent = $false
|
|
foreach ($p in $paths) {
|
|
$match = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
|
|
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
|
|
}
|
|
if ($match) { $stillPresent = $true; break }
|
|
}
|
|
} while ($stillPresent -and ((Get-Date) -lt $deadline))
|
|
|
|
if ($stillPresent) {
|
|
Write-Host "Uninstall entry still present after waiting; uninstall did not complete"
|
|
Exit 1
|
|
}
|
|
|
|
if ($ExpectedExitCodes -contains $exitCode) { Exit 0 }
|
|
Exit $exitCode
|
|
} catch {
|
|
Write-Host "Error running uninstaller: $_"
|
|
Exit 1
|
|
}
|