Files
kitzy f4d7064f9a Add Rtools as a Windows FMA (#50028)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** #50020

# What this does

Adds **Rtools** as a Windows Fleet-maintained app. One of the 11 apps
split out of #48501 that failed the FMA validator; #50016 shipped the 6
that passed.

## Why it was failing

The install script hit the validator's 10-minute `executeScript` cap
exactly:

```
20:41:22  INFO  msg="Executing install script..." app=Rtools
20:51:22  ERROR msg="Error executing install script: exit status 1"   # exactly 10:00 later
20:51:22  WARN  msg="failed to remove rtools45-6768-6492.exe: ... Access is denied."
```

The locked installer in the temp dir shows a process was still alive.
`Start-Process -Wait` waits for the process *and all of its
descendants*, which is the same root cause as the other install-timeout
apps in this batch.

Rtools is also the one app in the batch where a **slow unpack** is a
plausible second cause — the installer is ~460 MB and expands a full
toolchain. So rather than assume, the script now waits on the installer
process alone with a 480s cap (under the caller's 10-minute budget) and
logs elapsed time plus Add/Remove Programs registration state on every
poll. If the cap is reached:

- **registered** → the install finished and only a lingering child
remains, so it stops that process and succeeds;
- **not registered** → the unpack genuinely didn't finish, and it fails
with that stated explicitly.

Either way the CI log now says which one happened instead of just timing
out.

## Notes

- **Identity verified against the installer**, not winget metadata. The
setup stub's PE version resource reads `CompanyName: The R Foundation`,
`ProductName: Rtools`. Inno derives `VersionInfoCompany` from
`AppPublisher`, so the ARP publisher is `The R Foundation` — which is
what the exists query uses.
- **Versioned ARP name.** The registry `DisplayName` is `Rtools 4.5
(6768-6492)`, so the input uses `fuzzy_match_name` and the exists query
is `name LIKE 'Rtools %'`.
- Installs to `C:\rtools45`, not Program Files, so the validator's "no
changes detected in `C:\Program Files`" line is an expected warning, not
a failure.
- Ships a new catalog icon and website asset.

# Checklist for submitter

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [x] Timeouts are implemented and retries are limited to avoid infinite
loops

## Testing

- [x] FMA CI validator (install → detect → uninstall) **passes** on the
SYSTEM-context Windows runner — [run
30384196159](https://github.com/fleetdm/fleet/actions/runs/30384196159)
(`All checks passed`)
- [x] Generated output verified locally: manifest SHA matches the winget
manifest, exists/patched queries checked against the installer's PE
version resource, `apps.json` is valid JSON with a description filled
in.
- [x] QA'd all new/changed functionality manually




<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Added Rtools as a supported Windows application.
* Added installation and uninstallation support with silent setup and
silent removal.
* Added Rtools version metadata, installer verification, and Developer
tools categorization.
  * Added a dedicated Rtools icon for software listings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-28 23:01:31 -05:00

76 lines
2.5 KiB
PowerShell

# Learn more about .exe install scripts:
# http://fleetdm.com/learn-more-about/exe-install-scripts
$exeFilePath = "${env:INSTALLER_PATH}"
# Rtools unpacks a large toolchain (~460 MB) to C:\rtools45, not Program Files.
# -Wait waits on descendants, so wait on the installer process alone and log
# progress to tell a slow unpack apart from a stuck one.
$installTimeoutSeconds = 480
$pollSeconds = 15
$machineKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
$machineKey32on64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
function Test-RtoolsRegistered {
$null -ne (Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |
ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |
Where-Object { $_.DisplayName -like "Rtools*" } |
Select-Object -First 1)
}
try {
$process = Start-Process -FilePath "$exeFilePath" `
-ArgumentList "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART" `
-PassThru
# Keeps .ExitCode readable after the process ends.
$null = $process.Handle
$elapsed = 0
while (-not $process.HasExited -and ($elapsed -lt $installTimeoutSeconds)) {
Start-Sleep -Seconds $pollSeconds
$elapsed += $pollSeconds
Write-Host "Installing... ($elapsed seconds, registered: $(Test-RtoolsRegistered))"
}
if (-not $process.HasExited) {
# Registered means the install finished and only a lingering child remains.
if (Test-RtoolsRegistered) {
Write-Host "Installer still running after ${installTimeoutSeconds}s but Rtools is registered; stopping the lingering process."
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
Exit 0
}
Write-Host "Installer did not finish within ${installTimeoutSeconds}s and Rtools is not registered."
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
Exit 1
}
$exitCode = $process.ExitCode
Write-Host "Install exit code: $exitCode"
# The parent can exit while a descendant is still writing the ARP entry.
$settle = 0
while (-not (Test-RtoolsRegistered) -and ($settle -lt 90)) {
Start-Sleep -Seconds $pollSeconds
$settle += $pollSeconds
Write-Host "Waiting for Rtools to register... ($settle seconds)"
}
if (-not (Test-RtoolsRegistered)) {
Write-Host "Rtools did not register in Add/Remove Programs."
Exit 1
}
# 3010 (reboot required) and 1641 (reboot initiated) are successful installs.
if ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }
Exit $exitCode
} catch {
Write-Host "Error: $_"
Exit 1
}