Add Microsoft .NET Runtime 8 & 10 as Windows FMAs#46877
Conversation
Add Winget input manifests for Microsoft .NET Runtime 8 and 10, plus a shared install script and separate uninstall scripts for each major version. Update outputs: register both runtimes in ee/maintained-apps/outputs/apps.json and add per-app windows.json files containing version metadata, installer URLs, sha256 checksums and script refs. These changes enable automated install/uninstall and version detection for .NET Runtime 8 and 10 in the maintained-apps catalog.
Replace per-version uninstall scripts with a single generic microsoft_dotnet_runtime_uninstall.ps1 that targets the bundle by ProductCode (injected as PACKAGE_ID) and falls back to the Package Cache. Update winget input manifests for .NET Runtime 8 and 10 to reference the unified script, remove the old version-specific uninstall scripts, and update the output refs/hashes for both windows.json files to point to the new uninstall script implementations.
Script Diff Resultsee/maintained-apps/outputs/microsoft-dotnet-runtime-10/windows.json=== Install Script (no changes) ===
=== Uninstall // 68398bf2 -> b2eac15c ===
--- /tmp/old.ma5I1i 2026-06-05 04:58:52.324381424 +0000
+++ /tmp/new.2MAjft 2026-06-05 04:58:52.324381424 +0000
@@ -1,96 +1,73 @@
-# Locates the Microsoft .NET Runtime 10 (x64) WiX "burn" bundle in the registry
-# and runs its uninstaller silently.
+# Uninstalls the Microsoft .NET Runtime WiX "burn" bundle.
#
-# Burn bundles register both UninstallString (e.g.
-# "C:\ProgramData\Package Cache\{guid}\dotnet-runtime-10.0.x-win-x64.exe" /uninstall)
-# and, usually, QuietUninstallString (same, with /quiet appended). We prefer the
-# quiet form and otherwise append the silent switches ourselves.
-#
-# The DisplayName embeds the exact version ("Microsoft .NET Runtime - 10.0.8 (x64)"),
-# so we match on the major version + architecture.
-
-$displayNameLike = "Microsoft .NET Runtime - 10.* (x64)"
-$publisher = "Microsoft Corporation"
-
-$paths = @(
- 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
- 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+# The runtime installs as a burn bootstrapper that registers a *bundle* ARP entry
+# (keyed by the bundle ProductCode) alongside several MSI component entries that
+# share the same DisplayName. Only the bundle entry removes the whole runtime, and
+# it uninstalls by running its cached bootstrapper .exe with /uninstall -- never via
+# msiexec (see https://silentinstallhq.com/net-runtime-8-0-silent-uninstall-powershell/).
+# We target the bundle by its ProductCode (injected by the ingester) and fall back
+# to the cached bootstrapper in the Package Cache.
+
+$productCode = '{4443C461-ED45-4D3E-A6BB-3794C1B9FC6C}'
+
+function Invoke-Uninstaller {
+ param([string]$exe, [string]$exeArgs)
+ if ($exeArgs -notmatch '/uninstall') { $exeArgs = "/uninstall $exeArgs" }
+ if ($exeArgs -notmatch '/quiet') { $exeArgs = "$exeArgs /quiet" }
+ if ($exeArgs -notmatch '/norestart') { $exeArgs = "$exeArgs /norestart" }
+ $exeArgs = $exeArgs.Trim()
+ Write-Host "Uninstall command: $exe"
+ Write-Host "Uninstall args: $exeArgs"
+ $process = Start-Process -FilePath $exe -ArgumentList $exeArgs -NoNewWindow -PassThru -Wait
+ return $process.ExitCode
+}
+
+$exitCode = $null
+
+# 1) Preferred: the bundle ARP entry, looked up by the bundle ProductCode. Its
+# UninstallString/QuietUninstallString points to the cached bootstrapper .exe.
+$keys = @(
+ "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$productCode",
+ "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$productCode"
)
-$entry = $null
-foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
- $_.DisplayName -like $displayNameLike -and $_.Publisher -like "$publisher*"
+foreach ($key in $keys) {
+ if (-not (Test-Path $key)) { continue }
+ $entry = Get-ItemProperty $key -ErrorAction SilentlyContinue
+ if (-not $entry) { continue }
+
+ $raw = $entry.QuietUninstallString
+ if (-not $raw) { $raw = $entry.UninstallString }
+ if (-not $raw) { continue }
+
+ # Parse into executable + args, handling quoted/unquoted/bare shapes.
+ if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
+ $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+ } elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
+ $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+ } else {
+ $exe = $raw; $exeArgs = ""
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
-}
-if (-not $entry) {
- Write-Host "Uninstall entry not found"
- Exit 0
+ $exitCode = Invoke-Uninstaller -exe $exe -exeArgs $exeArgs
+ break
}
-# Prefer the vendor-provided quiet uninstall command when present.
-$raw = $entry.QuietUninstallString
-$needsSilentSwitches = $false
-if (-not $raw) {
- $raw = $entry.UninstallString
- $needsSilentSwitches = $true
+# 2) Fallback: run the cached bootstrapper directly from the Package Cache, which
+# burn names after the bundle ProductCode.
+if ($null -eq $exitCode) {
+ $cached = Get-ChildItem -Path "C:\ProgramData\Package Cache\$productCode" -Filter *.exe -ErrorAction SilentlyContinue | Select-Object -First 1
+ if ($cached) {
+ $exitCode = Invoke-Uninstaller -exe $cached.FullName -exeArgs ""
+ }
}
-if (-not $raw) {
- Write-Host "No uninstall string found"
+if ($null -eq $exitCode) {
+ Write-Host "Uninstall entry not found for product code: $productCode"
Exit 0
}
-# Parse the command into an executable path and its arguments. Handle the three
-# common shapes: quoted path, unquoted path that may contain spaces, bare token.
-if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
- $exe = $matches[1]
- $exeArgs = $matches[2].Trim()
-} elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
- $exe = $matches[1]
- $exeArgs = $matches[2].Trim()
-} else {
- $exe = $raw
- $exeArgs = ""
-}
-
-# Ensure silent + no-restart switches are present.
-if ($needsSilentSwitches -and $exeArgs -notmatch '/uninstall') {
- $exeArgs = "/uninstall $exeArgs"
-}
-if ($exeArgs -notmatch '/quiet') {
- $exeArgs = "$exeArgs /quiet"
-}
-if ($exeArgs -notmatch '/norestart') {
- $exeArgs = "$exeArgs /norestart"
-}
-$exeArgs = $exeArgs.Trim()
-
-Write-Host "Uninstall command: $exe"
-Write-Host "Uninstall args: $exeArgs"
-
-try {
- $processOptions = @{
- FilePath = $exe
- ArgumentList = $exeArgs
- NoNewWindow = $true
- PassThru = $true
- Wait = $true
- }
-
- $process = Start-Process @processOptions
- $exitCode = $process.ExitCode
- Write-Host "Uninstall exit code: $exitCode"
-
- # 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
- if ($exitCode -eq 3010 -or $exitCode -eq 1641) {
- Exit 0
- }
-
- Exit $exitCode
-} catch {
- Write-Host "Error running uninstaller: $_"
- Exit 1
-}
+Write-Host "Uninstall exit code: $exitCode"
+# 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
+if ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }
+Exit $exitCodeee/maintained-apps/outputs/microsoft-dotnet-runtime-8/windows.json=== Install Script (no changes) ===
=== Uninstall // b725f262 -> c507d96a ===
--- /tmp/old.RpPSfP 2026-06-05 04:58:52.362382245 +0000
+++ /tmp/new.A2ACry 2026-06-05 04:58:52.362382245 +0000
@@ -1,96 +1,73 @@
-# Locates the Microsoft .NET Runtime 8 (x64) WiX "burn" bundle in the registry
-# and runs its uninstaller silently.
+# Uninstalls the Microsoft .NET Runtime WiX "burn" bundle.
#
-# Burn bundles register both UninstallString (e.g.
-# "C:\ProgramData\Package Cache\{guid}\dotnet-runtime-8.0.x-win-x64.exe" /uninstall)
-# and, usually, QuietUninstallString (same, with /quiet appended). We prefer the
-# quiet form and otherwise append the silent switches ourselves.
-#
-# The DisplayName embeds the exact version ("Microsoft .NET Runtime - 8.0.27 (x64)"),
-# so we match on the major version + architecture.
-
-$displayNameLike = "Microsoft .NET Runtime - 8.* (x64)"
-$publisher = "Microsoft Corporation"
-
-$paths = @(
- 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
- 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+# The runtime installs as a burn bootstrapper that registers a *bundle* ARP entry
+# (keyed by the bundle ProductCode) alongside several MSI component entries that
+# share the same DisplayName. Only the bundle entry removes the whole runtime, and
+# it uninstalls by running its cached bootstrapper .exe with /uninstall -- never via
+# msiexec (see https://silentinstallhq.com/net-runtime-8-0-silent-uninstall-powershell/).
+# We target the bundle by its ProductCode (injected by the ingester) and fall back
+# to the cached bootstrapper in the Package Cache.
+
+$productCode = '{785c0558-c01e-44e3-8799-90ea5ea70ae6}'
+
+function Invoke-Uninstaller {
+ param([string]$exe, [string]$exeArgs)
+ if ($exeArgs -notmatch '/uninstall') { $exeArgs = "/uninstall $exeArgs" }
+ if ($exeArgs -notmatch '/quiet') { $exeArgs = "$exeArgs /quiet" }
+ if ($exeArgs -notmatch '/norestart') { $exeArgs = "$exeArgs /norestart" }
+ $exeArgs = $exeArgs.Trim()
+ Write-Host "Uninstall command: $exe"
+ Write-Host "Uninstall args: $exeArgs"
+ $process = Start-Process -FilePath $exe -ArgumentList $exeArgs -NoNewWindow -PassThru -Wait
+ return $process.ExitCode
+}
+
+$exitCode = $null
+
+# 1) Preferred: the bundle ARP entry, looked up by the bundle ProductCode. Its
+# UninstallString/QuietUninstallString points to the cached bootstrapper .exe.
+$keys = @(
+ "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$productCode",
+ "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$productCode"
)
-$entry = $null
-foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
- $_.DisplayName -like $displayNameLike -and $_.Publisher -like "$publisher*"
+foreach ($key in $keys) {
+ if (-not (Test-Path $key)) { continue }
+ $entry = Get-ItemProperty $key -ErrorAction SilentlyContinue
+ if (-not $entry) { continue }
+
+ $raw = $entry.QuietUninstallString
+ if (-not $raw) { $raw = $entry.UninstallString }
+ if (-not $raw) { continue }
+
+ # Parse into executable + args, handling quoted/unquoted/bare shapes.
+ if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
+ $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+ } elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
+ $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+ } else {
+ $exe = $raw; $exeArgs = ""
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
-}
-if (-not $entry) {
- Write-Host "Uninstall entry not found"
- Exit 0
+ $exitCode = Invoke-Uninstaller -exe $exe -exeArgs $exeArgs
+ break
}
-# Prefer the vendor-provided quiet uninstall command when present.
-$raw = $entry.QuietUninstallString
-$needsSilentSwitches = $false
-if (-not $raw) {
- $raw = $entry.UninstallString
- $needsSilentSwitches = $true
+# 2) Fallback: run the cached bootstrapper directly from the Package Cache, which
+# burn names after the bundle ProductCode.
+if ($null -eq $exitCode) {
+ $cached = Get-ChildItem -Path "C:\ProgramData\Package Cache\$productCode" -Filter *.exe -ErrorAction SilentlyContinue | Select-Object -First 1
+ if ($cached) {
+ $exitCode = Invoke-Uninstaller -exe $cached.FullName -exeArgs ""
+ }
}
-if (-not $raw) {
- Write-Host "No uninstall string found"
+if ($null -eq $exitCode) {
+ Write-Host "Uninstall entry not found for product code: $productCode"
Exit 0
}
-# Parse the command into an executable path and its arguments. Handle the three
-# common shapes: quoted path, unquoted path that may contain spaces, bare token.
-if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
- $exe = $matches[1]
- $exeArgs = $matches[2].Trim()
-} elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
- $exe = $matches[1]
- $exeArgs = $matches[2].Trim()
-} else {
- $exe = $raw
- $exeArgs = ""
-}
-
-# Ensure silent + no-restart switches are present.
-if ($needsSilentSwitches -and $exeArgs -notmatch '/uninstall') {
- $exeArgs = "/uninstall $exeArgs"
-}
-if ($exeArgs -notmatch '/quiet') {
- $exeArgs = "$exeArgs /quiet"
-}
-if ($exeArgs -notmatch '/norestart') {
- $exeArgs = "$exeArgs /norestart"
-}
-$exeArgs = $exeArgs.Trim()
-
-Write-Host "Uninstall command: $exe"
-Write-Host "Uninstall args: $exeArgs"
-
-try {
- $processOptions = @{
- FilePath = $exe
- ArgumentList = $exeArgs
- NoNewWindow = $true
- PassThru = $true
- Wait = $true
- }
-
- $process = Start-Process @processOptions
- $exitCode = $process.ExitCode
- Write-Host "Uninstall exit code: $exitCode"
-
- # 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
- if ($exitCode -eq 3010 -or $exitCode -eq 1641) {
- Exit 0
- }
-
- Exit $exitCode
-} catch {
- Write-Host "Error running uninstaller: $_"
- Exit 1
-}
+Write-Host "Uninstall exit code: $exitCode"
+# 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
+if ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }
+Exit $exitCode |
Introduce a new MicrosoftDotnetRuntime React SVG component that inlines the runtime icon (base64 PNG) at frontend/pages/SoftwarePage/components/icons/MicrosoftDotnetRuntime.tsx. Update icons index to import the new component and map the software name "microsoft .net runtime" to it. Add corresponding high-resolution PNG assets to website/assets/images (app-icon-microsoft-dotnet-runtime-8/10-60x60@2x.png). This adds the runtime icon to the app's icon set for display on the Software page.
Script Diff Resultsee/maintained-apps/outputs/microsoft-dotnet-runtime-10/windows.json=== Install Script (no changes) ===
=== Uninstall // 68398bf2 -> b2eac15c ===
--- /tmp/old.WeiBNl 2026-06-05 05:15:43.965314727 +0000
+++ /tmp/new.nnxSmL 2026-06-05 05:15:43.965314727 +0000
@@ -1,96 +1,73 @@
-# Locates the Microsoft .NET Runtime 10 (x64) WiX "burn" bundle in the registry
-# and runs its uninstaller silently.
+# Uninstalls the Microsoft .NET Runtime WiX "burn" bundle.
#
-# Burn bundles register both UninstallString (e.g.
-# "C:\ProgramData\Package Cache\{guid}\dotnet-runtime-10.0.x-win-x64.exe" /uninstall)
-# and, usually, QuietUninstallString (same, with /quiet appended). We prefer the
-# quiet form and otherwise append the silent switches ourselves.
-#
-# The DisplayName embeds the exact version ("Microsoft .NET Runtime - 10.0.8 (x64)"),
-# so we match on the major version + architecture.
-
-$displayNameLike = "Microsoft .NET Runtime - 10.* (x64)"
-$publisher = "Microsoft Corporation"
-
-$paths = @(
- 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
- 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+# The runtime installs as a burn bootstrapper that registers a *bundle* ARP entry
+# (keyed by the bundle ProductCode) alongside several MSI component entries that
+# share the same DisplayName. Only the bundle entry removes the whole runtime, and
+# it uninstalls by running its cached bootstrapper .exe with /uninstall -- never via
+# msiexec (see https://silentinstallhq.com/net-runtime-8-0-silent-uninstall-powershell/).
+# We target the bundle by its ProductCode (injected by the ingester) and fall back
+# to the cached bootstrapper in the Package Cache.
+
+$productCode = '{4443C461-ED45-4D3E-A6BB-3794C1B9FC6C}'
+
+function Invoke-Uninstaller {
+ param([string]$exe, [string]$exeArgs)
+ if ($exeArgs -notmatch '/uninstall') { $exeArgs = "/uninstall $exeArgs" }
+ if ($exeArgs -notmatch '/quiet') { $exeArgs = "$exeArgs /quiet" }
+ if ($exeArgs -notmatch '/norestart') { $exeArgs = "$exeArgs /norestart" }
+ $exeArgs = $exeArgs.Trim()
+ Write-Host "Uninstall command: $exe"
+ Write-Host "Uninstall args: $exeArgs"
+ $process = Start-Process -FilePath $exe -ArgumentList $exeArgs -NoNewWindow -PassThru -Wait
+ return $process.ExitCode
+}
+
+$exitCode = $null
+
+# 1) Preferred: the bundle ARP entry, looked up by the bundle ProductCode. Its
+# UninstallString/QuietUninstallString points to the cached bootstrapper .exe.
+$keys = @(
+ "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$productCode",
+ "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$productCode"
)
-$entry = $null
-foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
- $_.DisplayName -like $displayNameLike -and $_.Publisher -like "$publisher*"
+foreach ($key in $keys) {
+ if (-not (Test-Path $key)) { continue }
+ $entry = Get-ItemProperty $key -ErrorAction SilentlyContinue
+ if (-not $entry) { continue }
+
+ $raw = $entry.QuietUninstallString
+ if (-not $raw) { $raw = $entry.UninstallString }
+ if (-not $raw) { continue }
+
+ # Parse into executable + args, handling quoted/unquoted/bare shapes.
+ if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
+ $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+ } elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
+ $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+ } else {
+ $exe = $raw; $exeArgs = ""
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
-}
-if (-not $entry) {
- Write-Host "Uninstall entry not found"
- Exit 0
+ $exitCode = Invoke-Uninstaller -exe $exe -exeArgs $exeArgs
+ break
}
-# Prefer the vendor-provided quiet uninstall command when present.
-$raw = $entry.QuietUninstallString
-$needsSilentSwitches = $false
-if (-not $raw) {
- $raw = $entry.UninstallString
- $needsSilentSwitches = $true
+# 2) Fallback: run the cached bootstrapper directly from the Package Cache, which
+# burn names after the bundle ProductCode.
+if ($null -eq $exitCode) {
+ $cached = Get-ChildItem -Path "C:\ProgramData\Package Cache\$productCode" -Filter *.exe -ErrorAction SilentlyContinue | Select-Object -First 1
+ if ($cached) {
+ $exitCode = Invoke-Uninstaller -exe $cached.FullName -exeArgs ""
+ }
}
-if (-not $raw) {
- Write-Host "No uninstall string found"
+if ($null -eq $exitCode) {
+ Write-Host "Uninstall entry not found for product code: $productCode"
Exit 0
}
-# Parse the command into an executable path and its arguments. Handle the three
-# common shapes: quoted path, unquoted path that may contain spaces, bare token.
-if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
- $exe = $matches[1]
- $exeArgs = $matches[2].Trim()
-} elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
- $exe = $matches[1]
- $exeArgs = $matches[2].Trim()
-} else {
- $exe = $raw
- $exeArgs = ""
-}
-
-# Ensure silent + no-restart switches are present.
-if ($needsSilentSwitches -and $exeArgs -notmatch '/uninstall') {
- $exeArgs = "/uninstall $exeArgs"
-}
-if ($exeArgs -notmatch '/quiet') {
- $exeArgs = "$exeArgs /quiet"
-}
-if ($exeArgs -notmatch '/norestart') {
- $exeArgs = "$exeArgs /norestart"
-}
-$exeArgs = $exeArgs.Trim()
-
-Write-Host "Uninstall command: $exe"
-Write-Host "Uninstall args: $exeArgs"
-
-try {
- $processOptions = @{
- FilePath = $exe
- ArgumentList = $exeArgs
- NoNewWindow = $true
- PassThru = $true
- Wait = $true
- }
-
- $process = Start-Process @processOptions
- $exitCode = $process.ExitCode
- Write-Host "Uninstall exit code: $exitCode"
-
- # 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
- if ($exitCode -eq 3010 -or $exitCode -eq 1641) {
- Exit 0
- }
-
- Exit $exitCode
-} catch {
- Write-Host "Error running uninstaller: $_"
- Exit 1
-}
+Write-Host "Uninstall exit code: $exitCode"
+# 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
+if ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }
+Exit $exitCodeee/maintained-apps/outputs/microsoft-dotnet-runtime-8/windows.json=== Install Script (no changes) ===
=== Uninstall // b725f262 -> c507d96a ===
--- /tmp/old.6Iqhnx 2026-06-05 05:15:44.022314373 +0000
+++ /tmp/new.vOTTaO 2026-06-05 05:15:44.022314373 +0000
@@ -1,96 +1,73 @@
-# Locates the Microsoft .NET Runtime 8 (x64) WiX "burn" bundle in the registry
-# and runs its uninstaller silently.
+# Uninstalls the Microsoft .NET Runtime WiX "burn" bundle.
#
-# Burn bundles register both UninstallString (e.g.
-# "C:\ProgramData\Package Cache\{guid}\dotnet-runtime-8.0.x-win-x64.exe" /uninstall)
-# and, usually, QuietUninstallString (same, with /quiet appended). We prefer the
-# quiet form and otherwise append the silent switches ourselves.
-#
-# The DisplayName embeds the exact version ("Microsoft .NET Runtime - 8.0.27 (x64)"),
-# so we match on the major version + architecture.
-
-$displayNameLike = "Microsoft .NET Runtime - 8.* (x64)"
-$publisher = "Microsoft Corporation"
-
-$paths = @(
- 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
- 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+# The runtime installs as a burn bootstrapper that registers a *bundle* ARP entry
+# (keyed by the bundle ProductCode) alongside several MSI component entries that
+# share the same DisplayName. Only the bundle entry removes the whole runtime, and
+# it uninstalls by running its cached bootstrapper .exe with /uninstall -- never via
+# msiexec (see https://silentinstallhq.com/net-runtime-8-0-silent-uninstall-powershell/).
+# We target the bundle by its ProductCode (injected by the ingester) and fall back
+# to the cached bootstrapper in the Package Cache.
+
+$productCode = '{785c0558-c01e-44e3-8799-90ea5ea70ae6}'
+
+function Invoke-Uninstaller {
+ param([string]$exe, [string]$exeArgs)
+ if ($exeArgs -notmatch '/uninstall') { $exeArgs = "/uninstall $exeArgs" }
+ if ($exeArgs -notmatch '/quiet') { $exeArgs = "$exeArgs /quiet" }
+ if ($exeArgs -notmatch '/norestart') { $exeArgs = "$exeArgs /norestart" }
+ $exeArgs = $exeArgs.Trim()
+ Write-Host "Uninstall command: $exe"
+ Write-Host "Uninstall args: $exeArgs"
+ $process = Start-Process -FilePath $exe -ArgumentList $exeArgs -NoNewWindow -PassThru -Wait
+ return $process.ExitCode
+}
+
+$exitCode = $null
+
+# 1) Preferred: the bundle ARP entry, looked up by the bundle ProductCode. Its
+# UninstallString/QuietUninstallString points to the cached bootstrapper .exe.
+$keys = @(
+ "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$productCode",
+ "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$productCode"
)
-$entry = $null
-foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
- $_.DisplayName -like $displayNameLike -and $_.Publisher -like "$publisher*"
+foreach ($key in $keys) {
+ if (-not (Test-Path $key)) { continue }
+ $entry = Get-ItemProperty $key -ErrorAction SilentlyContinue
+ if (-not $entry) { continue }
+
+ $raw = $entry.QuietUninstallString
+ if (-not $raw) { $raw = $entry.UninstallString }
+ if (-not $raw) { continue }
+
+ # Parse into executable + args, handling quoted/unquoted/bare shapes.
+ if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
+ $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+ } elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
+ $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+ } else {
+ $exe = $raw; $exeArgs = ""
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
-}
-if (-not $entry) {
- Write-Host "Uninstall entry not found"
- Exit 0
+ $exitCode = Invoke-Uninstaller -exe $exe -exeArgs $exeArgs
+ break
}
-# Prefer the vendor-provided quiet uninstall command when present.
-$raw = $entry.QuietUninstallString
-$needsSilentSwitches = $false
-if (-not $raw) {
- $raw = $entry.UninstallString
- $needsSilentSwitches = $true
+# 2) Fallback: run the cached bootstrapper directly from the Package Cache, which
+# burn names after the bundle ProductCode.
+if ($null -eq $exitCode) {
+ $cached = Get-ChildItem -Path "C:\ProgramData\Package Cache\$productCode" -Filter *.exe -ErrorAction SilentlyContinue | Select-Object -First 1
+ if ($cached) {
+ $exitCode = Invoke-Uninstaller -exe $cached.FullName -exeArgs ""
+ }
}
-if (-not $raw) {
- Write-Host "No uninstall string found"
+if ($null -eq $exitCode) {
+ Write-Host "Uninstall entry not found for product code: $productCode"
Exit 0
}
-# Parse the command into an executable path and its arguments. Handle the three
-# common shapes: quoted path, unquoted path that may contain spaces, bare token.
-if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
- $exe = $matches[1]
- $exeArgs = $matches[2].Trim()
-} elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
- $exe = $matches[1]
- $exeArgs = $matches[2].Trim()
-} else {
- $exe = $raw
- $exeArgs = ""
-}
-
-# Ensure silent + no-restart switches are present.
-if ($needsSilentSwitches -and $exeArgs -notmatch '/uninstall') {
- $exeArgs = "/uninstall $exeArgs"
-}
-if ($exeArgs -notmatch '/quiet') {
- $exeArgs = "$exeArgs /quiet"
-}
-if ($exeArgs -notmatch '/norestart') {
- $exeArgs = "$exeArgs /norestart"
-}
-$exeArgs = $exeArgs.Trim()
-
-Write-Host "Uninstall command: $exe"
-Write-Host "Uninstall args: $exeArgs"
-
-try {
- $processOptions = @{
- FilePath = $exe
- ArgumentList = $exeArgs
- NoNewWindow = $true
- PassThru = $true
- Wait = $true
- }
-
- $process = Start-Process @processOptions
- $exitCode = $process.ExitCode
- Write-Host "Uninstall exit code: $exitCode"
-
- # 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
- if ($exitCode -eq 3010 -or $exitCode -eq 1641) {
- Exit 0
- }
-
- Exit $exitCode
-} catch {
- Write-Host "Error running uninstaller: $_"
- Exit 1
-}
+Write-Host "Uninstall exit code: $exitCode"
+# 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
+if ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }
+Exit $exitCode |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #46877 +/- ##
==========================================
- Coverage 66.96% 66.96% -0.01%
==========================================
Files 2856 2857 +1
Lines 225672 225674 +2
Branches 11592 11592
==========================================
+ Hits 151126 151127 +1
- Misses 60840 60841 +1
Partials 13706 13706
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (9)
WalkthroughThis PR adds managed deployment support for Microsoft .NET Runtime versions 8.0.27 and 10.0.8. It introduces Winget input definitions specifying package identifiers and detection queries, creates shared PowerShell scripts for silent WiX burn bundle installation and uninstallation with exit-code handling, generates output configurations with SQL predicates for version detection and installer metadata, registers both apps in the system registry, and adds a frontend icon component with UI integration for icon lookup by software name. Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR adds Windows Fleet-maintained apps (FMAs) for Microsoft .NET Runtime 8 and 10, including winget input manifests, install/uninstall PowerShell scripts, and generated output manifests/registry entries so Fleet can install/uninstall and detect versions of these runtimes.
Changes:
- Add a new Software page icon for “Microsoft .NET Runtime” and map it for fuzzy name matching.
- Add winget input manifests + shared install/uninstall scripts for .NET Runtime 8 and 10.
- Add generated maintained-app outputs (
apps.jsonregistration + per-appwindows.jsonmanifests with versions, queries, URLs, hashes, and script refs).
Reviewed changes
Copilot reviewed 9 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/pages/SoftwarePage/components/icons/MicrosoftDotnetRuntime.tsx | Adds a new icon component for .NET Runtime. |
| frontend/pages/SoftwarePage/components/icons/index.ts | Registers the new icon and maps it to “microsoft .net runtime”. |
| ee/maintained-apps/outputs/microsoft-dotnet-runtime-8/windows.json | Adds generated Windows manifest output for .NET Runtime 8 (version, queries, scripts, hashes). |
| ee/maintained-apps/outputs/microsoft-dotnet-runtime-10/windows.json | Adds generated Windows manifest output for .NET Runtime 10 (version, queries, scripts, hashes). |
| ee/maintained-apps/outputs/apps.json | Registers both runtimes in the maintained-app catalog. |
| ee/maintained-apps/inputs/winget/scripts/microsoft_dotnet_runtime_uninstall.ps1 | Adds a shared uninstall script template for burn bundles using ProductCode/Package Cache. |
| ee/maintained-apps/inputs/winget/scripts/microsoft_dotnet_runtime_install.ps1 | Adds a shared silent install script for the runtime .exe. |
| ee/maintained-apps/inputs/winget/microsoft-dotnet-runtime-8.json | Adds winget input manifest for .NET Runtime 8. |
| ee/maintained-apps/inputs/winget/microsoft-dotnet-runtime-10.json | Adds winget input manifest for .NET Runtime 10. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "exists_query": "SELECT 1 FROM programs WHERE name LIKE 'Microsoft .NET Runtime - 8.%' AND name LIKE '%(x64)' AND publisher = 'Microsoft Corporation';", | ||
| "installer_arch": "x64", | ||
| "installer_type": "exe", | ||
| "installer_scope": "", |
| "exists_query": "SELECT 1 FROM programs WHERE name LIKE 'Microsoft .NET Runtime - 10.%' AND name LIKE '%(x64)' AND publisher = 'Microsoft Corporation';", | ||
| "installer_arch": "x64", | ||
| "installer_type": "exe", | ||
| "installer_scope": "", |
Add Winget input manifests for Microsoft .NET Runtime 8 and 10, plus a shared install script and separate uninstall scripts for each major version. Update outputs: register both runtimes in ee/maintained-apps/outputs/apps.json and add per-app windows.json files containing version metadata, installer URLs, sha256 checksums and script refs. These changes enable automated install/uninstall and version detection for .NET Runtime 8 and 10 in the maintained-apps catalog.
Summary by CodeRabbit
Release Notes