kms-website: uninstall incompatible (retail/M365) Office before VL install
When the Office install path runs and a non-VL Click-to-Run Office is present (ProductReleaseIds not ending in 'Volume' = retail/M365), it can't coexist with a VL install of the same suite. Now: detect it, show it in the consent prompt, ODT /configure <Remove> only those products (VL products of other families preserved), then proceed with the VL install. Refactored the ODT run into a shared Invoke-Odt used by both install (<Add>) and uninstall (<Remove>). Telemetry on the uninstall step. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
7cfcd73d83
commit
2027d8bcc0
1 changed files with 63 additions and 21 deletions
|
|
@ -228,6 +228,16 @@ function Get-OfficeReleaseIds {
|
||||||
return @()
|
return @()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ALL installed Click-to-Run products (VL + Retail/M365), unfiltered. Retail/M365
|
||||||
|
# IDs do NOT end in 'Volume' (e.g. O365ProPlusRetail, ProPlus2021Retail).
|
||||||
|
function Get-AllOfficeC2R {
|
||||||
|
$c = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' -ErrorAction SilentlyContinue
|
||||||
|
if ($c -and $c.ProductReleaseIds) {
|
||||||
|
return $c.ProductReleaseIds.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
# Newest VL product for a family from the published list (data-driven: "latest"
|
# Newest VL product for a family from the published list (data-driven: "latest"
|
||||||
# tracks products.yaml, so adding a future LTSC there makes this follow with no
|
# tracks products.yaml, so adding a future LTSC there makes this follow with no
|
||||||
# script change). Picks the highest year among ProPlus/ProjectPro/VisioPro VL SKUs.
|
# script change). Picks the highest year among ProPlus/ProjectPro/VisioPro VL SKUs.
|
||||||
|
|
@ -238,25 +248,35 @@ function Get-LatestOfficeProduct([string]$label) {
|
||||||
Sort-Object { [int]([regex]::Match($_.product, '\d+').Value) } -Descending | Select-Object -First 1
|
Sort-Object { [int]([regex]::Match($_.product, '\d+').Value) } -Descending | Select-Object -First 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Reinstall Office as a Volume License product via the Office Deployment Tool.
|
# Office Deployment Tool plumbing. Self-hosted ODT bootstrapper (Microsoft's
|
||||||
# Heavy (multi-GB download, closes Office). Only invoked after explicit consent.
|
# download.microsoft.com URL is build-numbered, rotates every release and 404s;
|
||||||
# Self-hosted ODT bootstrapper (Microsoft's download.microsoft.com URL rotates
|
# we serve a known-good copy from our own /scripts). $env:KMS_ODT_URL overrides.
|
||||||
# its build-numbered path every release and 404s; we serve a known-good copy
|
# Invoke-Odt runs ONE setup.exe /configure with the given <Configuration> XML and
|
||||||
# from our own /scripts). Override with $env:KMS_ODT_URL if ever needed.
|
# is reused for both install (<Add>) and uninstall (<Remove>).
|
||||||
$script:ODT_URL = $(if ($env:KMS_ODT_URL) { $env:KMS_ODT_URL } else { 'https://kms.viktorbarzin.me/scripts/odt-setup.exe' })
|
$script:ODT_URL = $(if ($env:KMS_ODT_URL) { $env:KMS_ODT_URL } else { 'https://kms.viktorbarzin.me/scripts/odt-setup.exe' })
|
||||||
function Reinstall-OfficeVL([string]$product, [string]$channel) {
|
function Invoke-Odt([string]$configXml, [string]$stepMsg) {
|
||||||
$tmp = Join-Path $env:TEMP "kms-odt-$(Get-Random)"
|
$tmp = Join-Path $env:TEMP "kms-odt-$(Get-Random)"
|
||||||
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
|
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
|
||||||
|
try {
|
||||||
$odt = Join-Path $tmp 'odt.exe'
|
$odt = Join-Path $tmp 'odt.exe'
|
||||||
Step "Downloading the Office Deployment Tool"
|
Step "Downloading the Office Deployment Tool"
|
||||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
try { Invoke-WebRequest -UseBasicParsing -Uri $script:ODT_URL -OutFile $odt } catch { Bad "ODT download failed: $($_.Exception.Message)"; Send-Diag 'odt-install' 'fail' "download: $($_.Exception.Message)"; return $false }
|
try { Invoke-WebRequest -UseBasicParsing -Uri $script:ODT_URL -OutFile $odt } catch { Bad "ODT download failed: $($_.Exception.Message)"; Send-Diag 'odt' 'fail' "download: $($_.Exception.Message)"; return $false }
|
||||||
Start-Process -FilePath $odt -ArgumentList "/extract:`"$tmp`"", '/quiet' -Wait
|
Start-Process -FilePath $odt -ArgumentList "/extract:`"$tmp`"", '/quiet' -Wait
|
||||||
$setup = Join-Path $tmp 'setup.exe'
|
$setup = Join-Path $tmp 'setup.exe'
|
||||||
if (-not (Test-Path $setup)) { Bad "ODT extraction failed (no setup.exe)."; Send-Diag 'odt-install' 'fail' 'extract: no setup.exe'; return $false }
|
if (-not (Test-Path $setup)) { Bad "ODT extraction failed (no setup.exe)."; Send-Diag 'odt' 'fail' 'extract: no setup.exe'; return $false }
|
||||||
if (-not $channel) { $channel = if ($product -match '2021') { 'PerpetualVL2021' } elseif ($product -match '2019') { 'PerpetualVL2019' } else { 'PerpetualVL2024' } }
|
|
||||||
$cfg = Join-Path $tmp 'config.xml'
|
$cfg = Join-Path $tmp 'config.xml'
|
||||||
@"
|
$configXml | Set-Content -Path $cfg -Encoding UTF8
|
||||||
|
Step $stepMsg
|
||||||
|
Start-Process -FilePath $setup -ArgumentList '/configure', "`"$cfg`"" -Wait
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Reinstall-OfficeVL([string]$product, [string]$channel) {
|
||||||
|
if (-not $channel) { $channel = if ($product -match '2021') { 'PerpetualVL2021' } elseif ($product -match '2019') { 'PerpetualVL2019' } else { 'PerpetualVL2024' } }
|
||||||
|
$xml = @"
|
||||||
<Configuration>
|
<Configuration>
|
||||||
<Add OfficeClientEdition="64" Channel="$channel">
|
<Add OfficeClientEdition="64" Channel="$channel">
|
||||||
<Product ID="$product"><Language ID="MatchOS" /></Product>
|
<Product ID="$product"><Language ID="MatchOS" /></Product>
|
||||||
|
|
@ -264,11 +284,25 @@ function Reinstall-OfficeVL([string]$product, [string]$channel) {
|
||||||
<Display Level="None" AcceptEULA="TRUE" />
|
<Display Level="None" AcceptEULA="TRUE" />
|
||||||
<Property Name="FORCEAPPSHUTDOWN" Value="TRUE" />
|
<Property Name="FORCEAPPSHUTDOWN" Value="TRUE" />
|
||||||
</Configuration>
|
</Configuration>
|
||||||
"@ | Set-Content -Path $cfg -Encoding UTF8
|
"@
|
||||||
Step "Running setup.exe /configure (multi-GB download + reinstall; several minutes)"
|
return (Invoke-Odt $xml "Installing $product (multi-GB download + reinstall; several minutes)")
|
||||||
Start-Process -FilePath $setup -ArgumentList '/configure', "`"$cfg`"" -Wait
|
}
|
||||||
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
|
|
||||||
return $true
|
# Uninstall specific Click-to-Run products (the incompatible retail/M365 Office
|
||||||
|
# that blocks a VL install). Only the listed products are removed; VL products of
|
||||||
|
# other families are preserved.
|
||||||
|
function Uninstall-OfficeC2R([string[]]$products) {
|
||||||
|
$items = ($products | ForEach-Object { " <Product ID=`"$_`" />" }) -join "`n"
|
||||||
|
$xml = @"
|
||||||
|
<Configuration>
|
||||||
|
<Remove>
|
||||||
|
$items
|
||||||
|
</Remove>
|
||||||
|
<Display Level="None" AcceptEULA="TRUE" />
|
||||||
|
<Property Name="FORCEAPPSHUTDOWN" Value="TRUE" />
|
||||||
|
</Configuration>
|
||||||
|
"@
|
||||||
|
return (Invoke-Odt $xml "Uninstalling incompatible Office: $($products -join ', ')")
|
||||||
}
|
}
|
||||||
|
|
||||||
# Offer to install the LATEST Volume License product for a family (or the
|
# Offer to install the LATEST Volume License product for a family (or the
|
||||||
|
|
@ -279,18 +313,26 @@ function Install-LatestOfficeVL([string]$label) {
|
||||||
if (-not $target) { $best = Get-LatestOfficeProduct $label; if ($best) { $target = $best.product; $channel = $best.channel } }
|
if (-not $target) { $best = Get-LatestOfficeProduct $label; if ($best) { $target = $best.product; $channel = $best.channel } }
|
||||||
if (-not $target) { Warn "Couldn't determine the latest VL $label product from the key list."; return $null }
|
if (-not $target) { Warn "Couldn't determine the latest VL $label product from the key list."; return $null }
|
||||||
$disp = if ($best) { "$($best.edition) ($target)" } else { $target }
|
$disp = if ($best) { "$($best.edition) ($target)" } else { $target }
|
||||||
|
# Retail/M365 Click-to-Run Office (IDs not ending in 'Volume') can't coexist
|
||||||
|
# with a VL install of the same suite and must be removed first.
|
||||||
|
$incompat = @(Get-AllOfficeC2R | Where-Object { $_ -notmatch 'Volume$' })
|
||||||
|
$rm = if ($incompat) { "`n * FIRST UNINSTALLS the incompatible (non-VL) Office found: $($incompat -join ', ')" } else { '' }
|
||||||
$text = @"
|
$text = @"
|
||||||
KMS can only activate Volume License $label, and none is installed here.
|
KMS can only activate Volume License $label, and none is installed here.
|
||||||
The Office Deployment Tool can install the LATEST Volume License edition:
|
The Office Deployment Tool can install the LATEST Volume License edition:
|
||||||
|
|
||||||
-> $disp
|
-> $disp
|
||||||
|
|
||||||
Consequences:
|
Consequences:$rm
|
||||||
* Downloads ~3 GB and runs setup.exe /configure
|
* Downloads ~3 GB and runs setup.exe /configure
|
||||||
* CLOSES all running Office apps (Word/Excel/Outlook/...)
|
* CLOSES all running Office apps (Word/Excel/Outlook/...)
|
||||||
* Several minutes
|
* Several minutes
|
||||||
"@
|
"@
|
||||||
if (-not (Approve $text ([bool]$env:KMS_OFFICE_PRODUCT))) { Warn "$label install skipped."; Send-Diag $label.ToLower() 'install-skipped' $target; return $null }
|
if (-not (Approve $text ([bool]$env:KMS_OFFICE_PRODUCT))) { Warn "$label install skipped."; Send-Diag $label.ToLower() 'install-skipped' $target; return $null }
|
||||||
|
if ($incompat) {
|
||||||
|
if (-not (Uninstall-OfficeC2R $incompat)) { Bad "Uninstall of incompatible Office failed; aborting before VL install."; Send-Diag $label.ToLower() 'uninstall-fail' ($incompat -join ','); return $null }
|
||||||
|
Send-Diag $label.ToLower() 'uninstalled-incompatible' ($incompat -join ',')
|
||||||
|
}
|
||||||
if (-not (Reinstall-OfficeVL $target $channel)) { return $null }
|
if (-not (Reinstall-OfficeVL $target $channel)) { return $null }
|
||||||
return $target
|
return $target
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue