kms-website: install the LATEST VL Office, and offer it when none is installed

- Get-LatestOfficeProduct picks the newest ProPlus/ProjectPro/VisioPro VL SKU
  from keys.json by year (data-driven: add a future LTSC to products.yaml and
  the installer follows; no hardcoded 2024). $env:KMS_OFFICE_PRODUCT still wins.
- Activate-Ospp now offers the ODT install in BOTH cases: no Office found at all
  (previously it just skipped with "not found"), and a non-VL/retail/M365 Office
  installed. ODT channel comes from the chosen product's keys.json entry.
Note: KMS can't activate Microsoft 365/retail, so "latest" = latest LTSC VL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-06-01 19:02:08 +00:00
parent e38f34f146
commit a0c100ab08

View file

@ -201,10 +201,20 @@ function Get-OfficeReleaseIds {
return @() return @()
} }
# 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
# script change). Picks the highest year among ProPlus/ProjectPro/VisioPro VL SKUs.
function Get-LatestOfficeProduct([string]$label) {
$keys = Get-Keys; if (-not $keys) { return $null }
$pat = if ($label -eq 'Project') { '^ProjectPro\d+Volume$' } elseif ($label -eq 'Visio') { '^VisioPro\d+Volume$' } else { '^ProPlus\d+Volume$' }
$keys.office | Where-Object { $_.product -match $pat } |
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. # Reinstall Office as a Volume License product via the Office Deployment Tool.
# Heavy (multi-GB download, closes Office). Only invoked after explicit consent. # Heavy (multi-GB download, closes Office). Only invoked after explicit consent.
$script:ODT_URL = 'https://download.microsoft.com/download/2/7/A/27AF1BE6-DD20-4CB4-B154-EBAB8A7D4A7E/officedeploymenttool_19127-20198.exe' $script:ODT_URL = 'https://download.microsoft.com/download/2/7/A/27AF1BE6-DD20-4CB4-B154-EBAB8A7D4A7E/officedeploymenttool_19127-20198.exe'
function Reinstall-OfficeVL([string]$product) { function Reinstall-OfficeVL([string]$product, [string]$channel) {
$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
$odt = Join-Path $tmp 'odt.exe' $odt = Join-Path $tmp 'odt.exe'
@ -214,7 +224,7 @@ function Reinstall-OfficeVL([string]$product) {
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)."; return $false } if (-not (Test-Path $setup)) { Bad "ODT extraction failed (no setup.exe)."; return $false }
$channel = if ($product -match '2021') { 'PerpetualVL2021' } elseif ($product -match '2019') { 'PerpetualVL2019' } else { 'PerpetualVL2024' } 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'
@" @"
<Configuration> <Configuration>
@ -231,6 +241,30 @@ function Reinstall-OfficeVL([string]$product) {
return $true return $true
} }
# Offer to install the LATEST Volume License product for a family (or the
# explicit $env:KMS_OFFICE_PRODUCT) via the ODT, after showing the consequences.
# Returns the installed product id, or $null if skipped/failed.
function Install-LatestOfficeVL([string]$label) {
$best = $null; $target = $env:KMS_OFFICE_PRODUCT; $channel = $null
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 }
$disp = if ($best) { "$($best.edition) ($target)" } else { $target }
$text = @"
KMS can only activate Volume License $label, and none is installed here.
The Office Deployment Tool can install the LATEST Volume License edition:
-> $disp
Consequences:
* Downloads ~3 GB and runs setup.exe /configure
* CLOSES all running Office apps (Word/Excel/Outlook/...)
* Several minutes
"@
if (-not (Approve $text ([bool]$env:KMS_OFFICE_PRODUCT))) { Warn "$label install skipped."; return $null }
if (-not (Reinstall-OfficeVL $target $channel)) { return $null }
return $target
}
# Per-product license check. ospp /dstatus lists EVERY installed product, so a # Per-product license check. ospp /dstatus lists EVERY installed product, so a
# blanket '---LICENSED---' match treats Office-licensed as Project-licensed too. # blanket '---LICENSED---' match treats Office-licensed as Project-licensed too.
# Walk the LICENSE NAME / STATUS blocks and check only THIS family. # Walk the LICENSE NAME / STATUS blocks and check only THIS family.
@ -248,47 +282,31 @@ function Test-OsppLicensed($ospp, $label) {
function Activate-Ospp([string]$label) { function Activate-Ospp([string]$label) {
$ospp = Find-Ospp $ospp = Find-Ospp
# No Office found at all -> offer to install the latest VL edition.
if (-not $ospp) { if (-not $ospp) {
Warn "$label`: ospp.vbs not found (Office not installed?). Skipping." Step "$label`: no Office found - offering the latest Volume License install"
return $t = Install-LatestOfficeVL $label
if (-not $t) { return }
$ospp = Find-Ospp; if (-not $ospp) { Bad "$label`: ospp.vbs not found after install."; return }
} }
Step "$label activation via $ospp" Step "$label activation via $ospp"
& cscript //Nologo $ospp /sethst:$KmsHost | Out-Host & cscript //Nologo $ospp /sethst:$KmsHost | Out-Host
& cscript //Nologo $ospp /setprt:$KmsPort | Out-Host & cscript //Nologo $ospp /setprt:$KmsPort | Out-Host
# Idempotent: skip /act when THIS family is already licensed. # Idempotent: skip /act when THIS family is already licensed.
if (Test-OsppLicensed $ospp $label) { OK "$label already licensed - host set, skipping /act"; return } if (Test-OsppLicensed $ospp $label) { OK "$label already licensed - host set, skipping /act"; return }
# Not licensed: install the matching GVLK for each installed VL product of # Installed VL products of this family. NB: avoid `switch ($label)` here -
# this family (Office = anything that isn't Project/Visio), fetched from the list.
# Match this family's installed VL products. NB: avoid `switch ($label)` here:
# inside a switch, $_ is the switch input (the label), not the pipeline item. # inside a switch, $_ is the switch input (the label), not the pipeline item.
$rels = Get-OfficeReleaseIds | Where-Object { $rels = Get-OfficeReleaseIds | Where-Object {
($label -eq 'Project' -and $_ -match 'Project') -or ($label -eq 'Project' -and $_ -match 'Project') -or
($label -eq 'Visio' -and $_ -match 'Visio') -or ($label -eq 'Visio' -and $_ -match 'Visio') -or
($label -eq 'Office' -and $_ -notmatch 'Project|Visio') ($label -eq 'Office' -and $_ -notmatch 'Project|Visio')
} }
# No Volume License product of this family installed (e.g. retail Office). # No VL product of this family (e.g. retail/M365 Office) -> offer latest VL.
# Offer a VL reinstall via the ODT (default product per family; override
# with $env:KMS_OFFICE_PRODUCT). Heavy + closes apps, so consent-gated.
if (-not $rels) { if (-not $rels) {
$target = if ($env:KMS_OFFICE_PRODUCT) { $env:KMS_OFFICE_PRODUCT } $t = Install-LatestOfficeVL $label
elseif ($label -eq 'Project') { 'ProjectPro2024Volume' } if (-not $t) { return }
elseif ($label -eq 'Visio') { 'VisioPro2024Volume' } $ospp = Find-Ospp; if (-not $ospp) { Bad "ospp.vbs not found after install."; return }
else { 'ProPlus2024Volume' } $rels = @($t)
$text = @"
No Volume License $label is installed, so KMS cannot activate it.
The Office Deployment Tool can reinstall it as Volume License:
-> $target
Consequences:
* Downloads ~3 GB and runs setup.exe /configure
* CLOSES all running Office apps (Word/Excel/Outlook/...)
* Several minutes; replaces the current $label install
"@
if (-not (Approve $text ([bool]$env:KMS_OFFICE_PRODUCT))) { Warn "$label reinstall skipped; no VL $label to activate."; return }
if (-not (Reinstall-OfficeVL $target)) { return }
$ospp = Find-Ospp; if (-not $ospp) { Bad "ospp.vbs not found after reinstall."; return }
$rels = @($target)
} }
$keys = Get-Keys $keys = Get-Keys
foreach ($rel in $rels) { foreach ($rel in $rels) {
@ -296,7 +314,7 @@ Consequences:
if ($k) { Write-Host " $rel -> installing GVLK $k"; & cscript //Nologo $ospp /inpkey:$k | Out-Host } if ($k) { Write-Host " $rel -> installing GVLK $k"; & cscript //Nologo $ospp /inpkey:$k | Out-Host }
} }
& cscript //Nologo $ospp /act | Out-Host & cscript //Nologo $ospp /act | Out-Host
if (Test-OsppLicensed $ospp $label) { OK "$label licensed" } else { Warn "$label status not LICENSED yet (no VL $label SKU installed? See https://kms.viktorbarzin.me/#office)" } if (Test-OsppLicensed $ospp $label) { OK "$label licensed" } else { Warn "$label status not LICENSED yet (no VL $label SKU? See https://kms.viktorbarzin.me/#office)" }
} }
if ($doOfficeAct) { Activate-Ospp 'Office' } if ($doOfficeAct) { Activate-Ospp 'Office' }
if ($doProjAct) { Activate-Ospp 'Project' } if ($doProjAct) { Activate-Ospp 'Project' }