568 lines
22 KiB
PowerShell
568 lines
22 KiB
PowerShell
# UpdatePackages-oxmc.ps1
|
|
#
|
|
# Fetches two manifests from cdn.oxmc.me and applies any pending updates:
|
|
#
|
|
# iso-manifest.json -- versioned zip of C:\Windows\OEM contents
|
|
# (scripts, cursors, config -- the stuff baked into the ISO)
|
|
# internal-manifest.json -- individual app packages (exe/msi/msix/ps1/zip)
|
|
#
|
|
# Update state is written to C:\Windows\OEM\update-state.json so the script
|
|
# is idempotent: already-installed versions are skipped.
|
|
#
|
|
# Run as: PowerShell -ExecutionPolicy Bypass -File UpdatePackages-oxmc.ps1
|
|
#
|
|
# ---- iso-manifest.json schema (example) --------------------------------
|
|
# {
|
|
# "schemaVersion": 1,
|
|
# "package": {
|
|
# "name": "oxmc-oem-core",
|
|
# "version": "1.2.0",
|
|
# "description": "OEM scripts, cursors, config",
|
|
# "url": "https://cdn.oxmc.me/apps/windows/oem-core-1.2.0.zip",
|
|
# "sha256": "abcdef...",
|
|
# "installPath": "C:\\Windows\\OEM",
|
|
# "postInstallScript":"post-install.ps1"
|
|
# }
|
|
# }
|
|
#
|
|
# ---- internal-manifest.json schema (example) ---------------------------
|
|
# {
|
|
# "schemaVersion": 1,
|
|
# "packages": [
|
|
# {
|
|
# "name": "myapp",
|
|
# "version": "2.0.0",
|
|
# "description": "My App",
|
|
# "displayName": "My App",
|
|
# "publishedAt": "2026-01-01T00:00:00Z",
|
|
# "enabled": true, // false = skip without error
|
|
# "url": "https://cdn.oxmc.me/apps/windows/myapp-2.0.0.exe",
|
|
# "sha256": "abcdef...",
|
|
# "type": "exe", // exe | msi | msix | ps1 | zip
|
|
# "args": ["/quiet"], // extra installer args (exe/msi)
|
|
# "installPath": null, // required for zip type
|
|
# "minBuild": 22000, // optional: skip below this Windows build
|
|
# "maxBuild": null, // optional: skip above this Windows build
|
|
# "restartRequired": false, // log restart reminder after install
|
|
# "checkInstalled": { // optional: detect existing install
|
|
# "type": "registry", // registry | file | appx
|
|
# "key": "HKLM:\\SOFTWARE\\...",
|
|
# "value": "ValueName", // registry only
|
|
# "match": "^2\\." // optional regex against value data
|
|
# }
|
|
# // file type: { "type": "file", "path": "C:\\Program Files\\..." }
|
|
# // appx type: { "type": "appx", "packageFamilyName": "Publisher.App_xyz" }
|
|
# },
|
|
# {
|
|
# "name": "my-app-bundle",
|
|
# "version": "1.0.0",
|
|
# "type": "packagelist", // download + install each item individually
|
|
# "enabled": true,
|
|
# "minBuild": null,
|
|
# "maxBuild": null,
|
|
# "items": [
|
|
# {
|
|
# "name": "firefox",
|
|
# "url": "https://cdn.oxmc.me/...",
|
|
# "sha256": "abcdef...",
|
|
# "type": "exe",
|
|
# "args": ["-ms"],
|
|
# "installPath": null,
|
|
# "conditions": { "minBuild": 10240, "editions": ["Pro","Enterprise"] },
|
|
# "checkInstalled": { "type": "registry", "key": "HKLM:\\...", "value": "CurrentVersion", "match": "." }
|
|
# }
|
|
# ]
|
|
# },
|
|
# {
|
|
# "name": "my-app", // variant: pick installer per OS/arch
|
|
# "version": "2.0.0",
|
|
# "enabled": true,
|
|
# "minBuild": null, // outer gate runs before variant selection
|
|
# "maxBuild": null,
|
|
# "variants": [
|
|
# {
|
|
# "displayName": "My App (Win11 x64)",
|
|
# "conditions": { "minBuild": 22000, "architecture": "x64" },
|
|
# "url": "https://cdn.oxmc.me/...",
|
|
# "sha256": "abcdef...",
|
|
# "type": "exe",
|
|
# "args": ["/quiet"],
|
|
# "installPath": null,
|
|
# "checkInstalled": null
|
|
# },
|
|
# {
|
|
# "displayName": "My App (Win10 x64)",
|
|
# "conditions": { "minBuild": 10240, "maxBuild": 19045, "architecture": "x64" },
|
|
# "url": "https://cdn.oxmc.me/...",
|
|
# "sha256": "abcdef...",
|
|
# "type": "exe",
|
|
# "args": ["/quiet"],
|
|
# "installPath": null,
|
|
# "checkInstalled": null
|
|
# }
|
|
# ]
|
|
# // conditions fields: minBuild, maxBuild, editions (array), architecture (x64|x86|arm64)
|
|
# // first matching variant wins; no match = skip package
|
|
# }
|
|
# ]
|
|
# }
|
|
# -------------------------------------------------------------------------
|
|
|
|
Set-StrictMode -Off
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
# -- Path setup ----------------------------------------------------------------
|
|
if ($PSVersionTable.PSVersion.Major -ge 3) {
|
|
$currentDir = $PSScriptRoot
|
|
} else {
|
|
$currentDir = (Get-Item .).FullName
|
|
}
|
|
|
|
. "$currentDir\script-helper.ps1"
|
|
|
|
$windowsDrive = Split-Path $env:SystemRoot -Qualifier
|
|
$oemRoot = "$windowsDrive\Windows\OEM"
|
|
$stateFile = "$oemRoot\update-state.json"
|
|
$tempDir = "$env:TEMP\oxmc-updates"
|
|
$logFile = "$windowsDrive\Windows\Setup\PrePostInstall\Logs\UpdatePackages-$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
|
|
|
|
# -- Logging -------------------------------------------------------------------
|
|
function Write-Log {
|
|
param([string]$Msg, [string]$Level = "INFO")
|
|
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Msg"
|
|
Write-Host $line
|
|
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
|
}
|
|
function Write-LogWarn { param([string]$Msg) Write-Log $Msg "WARN" }
|
|
function Write-LogError { param([string]$Msg) Write-Log $Msg "ERROR" }
|
|
|
|
# -- Admin check ---------------------------------------------------------------
|
|
if (-not (Test-Administrator)) {
|
|
Write-Host "Restarting as administrator..."
|
|
$argStr = "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""
|
|
Start-Process PowerShell -ArgumentList $argStr -Verb RunAs
|
|
exit
|
|
}
|
|
|
|
# -- State helpers -------------------------------------------------------------
|
|
function Read-State {
|
|
if (Test-Path $stateFile) {
|
|
try { return Get-Content $stateFile -Raw | ConvertFrom-Json } catch {}
|
|
}
|
|
return [PSCustomObject]@{ isoPackage = $null; packages = [PSCustomObject]@{} }
|
|
}
|
|
|
|
function Save-State {
|
|
param([PSCustomObject]$State)
|
|
$State | ConvertTo-Json -Depth 10 | Set-Content $stateFile -Encoding UTF8
|
|
}
|
|
|
|
function Get-InstalledVersion {
|
|
param([PSCustomObject]$State, [string]$Name, [string]$Type = "package")
|
|
if ($Type -eq "isoPackage") {
|
|
return if ($State.isoPackage) { $State.isoPackage.version } else { $null }
|
|
}
|
|
$pkg = $State.packages."$Name"
|
|
return if ($pkg) { $pkg.version } else { $null }
|
|
}
|
|
|
|
function Set-InstalledVersion {
|
|
param([PSCustomObject]$State, [string]$Name, [string]$Version, [string]$Type = "package")
|
|
if ($Type -eq "isoPackage") {
|
|
$State.isoPackage = [PSCustomObject]@{ version = $Version; installedAt = (Get-Date -Format "o") }
|
|
} else {
|
|
$State.packages | Add-Member -MemberType NoteProperty -Name $Name `
|
|
-Value ([PSCustomObject]@{ version = $Version; installedAt = (Get-Date -Format "o") }) `
|
|
-Force
|
|
}
|
|
}
|
|
|
|
# -- Version comparison --------------------------------------------------------
|
|
function Compare-Versions {
|
|
param([string]$Installed, [string]$Available)
|
|
if (-not $Installed) { return $true }
|
|
try {
|
|
$i = [Version]$Installed
|
|
$a = [Version]$Available
|
|
return $a -gt $i
|
|
} catch {
|
|
return $Installed -ne $Available
|
|
}
|
|
}
|
|
|
|
# -- Condition evaluator -------------------------------------------------------
|
|
# $Conditions object fields (all optional):
|
|
# minBuild int minimum Windows build (inclusive)
|
|
# maxBuild int maximum Windows build (inclusive)
|
|
# editions array e.g. ["Pro","Enterprise"] - null means any
|
|
# architecture string "x64" | "x86" | "arm64" - null means any
|
|
function Test-Conditions {
|
|
param([PSCustomObject]$Conditions)
|
|
if (-not $Conditions) { return $true }
|
|
|
|
$build = Get-WindowsBuildNumber
|
|
if ($Conditions.minBuild -and $build -lt [int]$Conditions.minBuild) { return $false }
|
|
if ($Conditions.maxBuild -and $build -gt [int]$Conditions.maxBuild) { return $false }
|
|
|
|
if ($Conditions.editions) {
|
|
$edition = Get-WindowsEdition
|
|
$hit = $Conditions.editions | Where-Object { $_ -ieq $edition }
|
|
if (-not $hit) { return $false }
|
|
}
|
|
|
|
if ($Conditions.architecture) {
|
|
$arch = Get-NativeArchitecture
|
|
if ($Conditions.architecture -ine $arch) { return $false }
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
# -- Check if package is already installed on the system -----------------------
|
|
function Test-PackageInstalled {
|
|
param([PSCustomObject]$Check)
|
|
if (-not $Check) { return $false }
|
|
|
|
switch ($Check.type.ToLower()) {
|
|
"registry" {
|
|
try {
|
|
$val = (Get-ItemProperty -Path $Check.key -Name $Check.value -ErrorAction Stop).$($Check.value)
|
|
if ($Check.match) { return [bool]($val -match $Check.match) }
|
|
return $true
|
|
} catch { return $false }
|
|
}
|
|
"file" {
|
|
return (Test-Path $Check.path)
|
|
}
|
|
"appx" {
|
|
try {
|
|
$pkg = Get-AppxPackage -Name "$($Check.packageFamilyName)*" -ErrorAction SilentlyContinue
|
|
return ($null -ne $pkg)
|
|
} catch { return $false }
|
|
}
|
|
default {
|
|
Write-LogWarn "Unknown checkInstalled type '$($Check.type)' - skipping check."
|
|
return $false
|
|
}
|
|
}
|
|
}
|
|
|
|
# -- Download + verify ---------------------------------------------------------
|
|
function Invoke-Download {
|
|
param([string]$Url, [string]$Dest, [string]$Sha256)
|
|
|
|
Write-Log "Downloading: $Url"
|
|
$null = New-Item -ItemType Directory -Path (Split-Path $Dest) -Force
|
|
Invoke-WebRequest -Uri $Url -OutFile $Dest -UseBasicParsing -ErrorAction Stop
|
|
|
|
if ($Sha256) {
|
|
$actual = (Get-FileHash $Dest -Algorithm SHA256).Hash
|
|
if ($actual -ne $Sha256.ToUpper()) {
|
|
Remove-Item $Dest -Force
|
|
throw "SHA256 mismatch for $Dest`nExpected : $Sha256`nActual : $actual"
|
|
}
|
|
Write-Log "SHA256 verified OK."
|
|
} else {
|
|
Write-LogWarn "No SHA256 in manifest for $(Split-Path $Dest -Leaf) - skipping hash check."
|
|
}
|
|
}
|
|
|
|
# -- ISO package installer -----------------------------------------------------
|
|
function Install-IsoPackage {
|
|
param([PSCustomObject]$Pkg, [PSCustomObject]$State)
|
|
|
|
$installPath = if ($Pkg.installPath) { $Pkg.installPath } else { $oemRoot }
|
|
|
|
Write-Log "==> ISO package: $($Pkg.name) v$($Pkg.version)"
|
|
Write-Log " Install path : $installPath"
|
|
|
|
$zipPath = "$tempDir\iso-pkg-$($Pkg.version).zip"
|
|
$extractDir = "$tempDir\iso-pkg-extract"
|
|
|
|
try {
|
|
Invoke-Download -Url $Pkg.url -Dest $zipPath -Sha256 $Pkg.sha256
|
|
|
|
# Clean previous extract
|
|
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force }
|
|
New-Item -ItemType Directory -Path $extractDir | Out-Null
|
|
|
|
Write-Log "Extracting package..."
|
|
Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force
|
|
|
|
# Copy all files from extract root into installPath, preserving structure
|
|
Write-Log "Applying files to: $installPath"
|
|
$null = New-Item -ItemType Directory -Path $installPath -Force
|
|
Copy-Item "$extractDir\*" -Destination $installPath -Recurse -Force
|
|
|
|
# Run post-install script if present
|
|
if ($Pkg.postInstallScript) {
|
|
$postScript = Join-Path $installPath $Pkg.postInstallScript
|
|
if (Test-Path $postScript) {
|
|
Write-Log "Running post-install script: $postScript"
|
|
& PowerShell -NoProfile -ExecutionPolicy Bypass -File $postScript
|
|
if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) {
|
|
Write-LogWarn "Post-install script exited with code $LASTEXITCODE"
|
|
}
|
|
} else {
|
|
Write-LogWarn "postInstallScript '$($Pkg.postInstallScript)' not found after extraction."
|
|
}
|
|
}
|
|
|
|
Set-InstalledVersion -State $State -Name $Pkg.name -Version $Pkg.version -Type "isoPackage"
|
|
Write-Log "ISO package installed successfully."
|
|
|
|
} finally {
|
|
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
|
|
Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
# -- Core download-and-run for a single installable item -----------------------
|
|
# Used by both Install-Package (single) and Install-Package (packagelist items).
|
|
# $Item must have: name, url, type, sha256 (optional), args (optional),
|
|
# installPath (required for zip), checkInstalled (optional)
|
|
function Invoke-PackageInstall {
|
|
param([PSCustomObject]$Item, [string]$Label = $Item.name)
|
|
|
|
# Per-item conditions: skip if current system doesn't match
|
|
if ($Item.conditions -and -not (Test-Conditions -Conditions $Item.conditions)) {
|
|
Write-Log " [$Label] Conditions not met - skipping."
|
|
return
|
|
}
|
|
|
|
# Per-item checkInstalled: skip if already present on system
|
|
if ($Item.checkInstalled -and (Test-PackageInstalled -Check $Item.checkInstalled)) {
|
|
Write-Log " [$Label] Already installed (check passed) - skipping."
|
|
return
|
|
}
|
|
|
|
$ext = switch ($Item.type.ToLower()) {
|
|
"exe" { ".exe" }
|
|
"msi" { ".msi" }
|
|
"msix" { ".msix" }
|
|
"ps1" { ".ps1" }
|
|
"zip" { ".zip" }
|
|
default { throw "Unknown package type: $($Item.type)" }
|
|
}
|
|
$destFile = "$tempDir\$Label$ext"
|
|
$extraArgs = if ($Item.args) { $Item.args } else { @() }
|
|
|
|
try {
|
|
Invoke-Download -Url $Item.url -Dest $destFile -Sha256 $Item.sha256
|
|
|
|
switch ($Item.type.ToLower()) {
|
|
"exe" {
|
|
Write-Log " [$Label] Running installer..."
|
|
Start-Process $destFile -ArgumentList $extraArgs -Wait -NoNewWindow
|
|
if ($LASTEXITCODE -and $LASTEXITCODE -notin @(0, 3010)) {
|
|
throw "Installer exited with code $LASTEXITCODE"
|
|
}
|
|
}
|
|
"msi" {
|
|
Write-Log " [$Label] Running msiexec..."
|
|
$msiArgs = @("/i", $destFile, "/quiet", "/norestart") + $extraArgs
|
|
Start-Process msiexec.exe -ArgumentList $msiArgs -Wait -NoNewWindow
|
|
if ($LASTEXITCODE -and $LASTEXITCODE -notin @(0, 3010)) {
|
|
throw "msiexec exited with code $LASTEXITCODE"
|
|
}
|
|
}
|
|
"msix" {
|
|
Write-Log " [$Label] Installing MSIX/Appx..."
|
|
Add-AppxPackage -Path $destFile -ErrorAction Stop
|
|
}
|
|
"ps1" {
|
|
Write-Log " [$Label] Running PowerShell script..."
|
|
& PowerShell -NoProfile -ExecutionPolicy Bypass -File $destFile @extraArgs
|
|
if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) {
|
|
throw "Script exited with code $LASTEXITCODE"
|
|
}
|
|
}
|
|
"zip" {
|
|
if (-not $Item.installPath) { throw "Package type 'zip' requires installPath." }
|
|
Write-Log " [$Label] Extracting to $($Item.installPath)..."
|
|
$null = New-Item -ItemType Directory -Path $Item.installPath -Force
|
|
Expand-Archive -Path $destFile -DestinationPath $Item.installPath -Force
|
|
}
|
|
}
|
|
Write-Log " [$Label] OK."
|
|
} finally {
|
|
Remove-Item $destFile -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
# -- Individual package / packagelist installer --------------------------------
|
|
function Install-Package {
|
|
param([PSCustomObject]$Pkg, [PSCustomObject]$State)
|
|
|
|
Write-Log "==> Package: $($Pkg.name) v$($Pkg.version) [$($Pkg.type)]"
|
|
|
|
# Outer build gate - quick reject before variant evaluation
|
|
$build = Get-WindowsBuildNumber
|
|
if ($Pkg.minBuild -and $build -lt [int]$Pkg.minBuild) {
|
|
Write-Log " Skipping: requires build >= $($Pkg.minBuild) (current: $build)"
|
|
return
|
|
}
|
|
if ($Pkg.maxBuild -and $build -gt [int]$Pkg.maxBuild) {
|
|
Write-Log " Skipping: requires build <= $($Pkg.maxBuild) (current: $build)"
|
|
return
|
|
}
|
|
|
|
# Variants: first matching variant wins; provides url/type/args/etc for this system
|
|
if ($Pkg.variants -and $Pkg.variants.Count -gt 0) {
|
|
$matched = $null
|
|
foreach ($v in $Pkg.variants) {
|
|
if (Test-Conditions -Conditions $v.conditions) {
|
|
$matched = $v
|
|
break
|
|
}
|
|
}
|
|
if (-not $matched) {
|
|
Write-Log " No variant matched current system - skipping."
|
|
return
|
|
}
|
|
$label = if ($matched.displayName) { $matched.displayName } else { $Pkg.name }
|
|
Write-Log " Variant: $label"
|
|
Invoke-PackageInstall -Item $matched -Label $Pkg.name
|
|
Set-InstalledVersion -State $State -Name $Pkg.name -Version $Pkg.version
|
|
Write-Log " Installed OK."
|
|
return
|
|
}
|
|
|
|
# packagelist: download and install each item individually
|
|
if ($Pkg.type.ToLower() -eq "packagelist") {
|
|
if (-not $Pkg.items -or $Pkg.items.Count -eq 0) {
|
|
throw "packagelist '$($Pkg.name)' has no items."
|
|
}
|
|
$listFailed = 0
|
|
foreach ($item in $Pkg.items) {
|
|
try {
|
|
Invoke-PackageInstall -Item $item -Label $item.name
|
|
} catch {
|
|
Write-LogError " Item '$($item.name)' failed: $_"
|
|
$listFailed++
|
|
}
|
|
}
|
|
if ($listFailed -gt 0) { throw "$listFailed item(s) in packagelist '$($Pkg.name)' failed." }
|
|
Set-InstalledVersion -State $State -Name $Pkg.name -Version $Pkg.version
|
|
Write-Log " PackageList installed OK."
|
|
return
|
|
}
|
|
|
|
# Single package - delegate to shared helper
|
|
Invoke-PackageInstall -Item $Pkg -Label $Pkg.name
|
|
|
|
Set-InstalledVersion -State $State -Name $Pkg.name -Version $Pkg.version
|
|
Write-Log " Installed OK."
|
|
}
|
|
|
|
# -- Entry point ---------------------------------------------------------------
|
|
|
|
Write-Log "UpdatePackages-oxmc starting."
|
|
Write-Log "OEM root : $oemRoot"
|
|
Write-Log "State : $stateFile"
|
|
|
|
$null = New-Item -ItemType Directory -Path $tempDir -Force
|
|
|
|
$state = Read-State
|
|
|
|
# -- 1. Fetch manifests --------------------------------------------------------
|
|
$internalManifestUrl = "https://cdn.oxmc.me/apps/windows/internal-manifest.json"
|
|
$isoManifestUrl = "https://cdn.oxmc.me/apps/windows/iso-manifest.json"
|
|
|
|
try {
|
|
Write-Log "Fetching internal manifest..."
|
|
$internalManifest = Get-ContentFromUrl -url $internalManifestUrl | ConvertFrom-Json
|
|
} catch {
|
|
Write-LogError "Failed to fetch internal manifest: $_"
|
|
exit 1
|
|
}
|
|
|
|
try {
|
|
Write-Log "Fetching ISO manifest..."
|
|
$isoManifest = Get-ContentFromUrl -url $isoManifestUrl | ConvertFrom-Json
|
|
} catch {
|
|
Write-LogError "Failed to fetch ISO manifest: $_"
|
|
exit 1
|
|
}
|
|
|
|
$updatesApplied = 0
|
|
$updatesFailed = 0
|
|
$restartNeeded = $false
|
|
|
|
# -- 2. ISO package (C:\Windows\OEM contents) ----------------------------------
|
|
if ($isoManifest.package) {
|
|
$pkg = $isoManifest.package
|
|
$installedVer = Get-InstalledVersion -State $state -Name $pkg.name -Type "isoPackage"
|
|
|
|
if (Compare-Versions -Installed $installedVer -Available $pkg.version) {
|
|
Write-Log "ISO package update: $(if ($installedVer) { $installedVer } else { 'not installed' }) -> $($pkg.version)"
|
|
try {
|
|
Install-IsoPackage -Pkg $pkg -State $state
|
|
Save-State -State $state
|
|
$updatesApplied++
|
|
} catch {
|
|
Write-LogError "ISO package install failed: $_"
|
|
$updatesFailed++
|
|
}
|
|
} else {
|
|
Write-Log "ISO package $($pkg.name) is up to date ($installedVer)."
|
|
}
|
|
} else {
|
|
Write-LogWarn "ISO manifest has no 'package' entry - skipping."
|
|
}
|
|
|
|
# -- 3. Internal packages ------------------------------------------------------
|
|
if ($internalManifest.packages) {
|
|
foreach ($pkg in $internalManifest.packages) {
|
|
|
|
# enabled check
|
|
if ($pkg.PSObject.Properties['enabled'] -and $pkg.enabled -eq $false) {
|
|
Write-Log "Package $($pkg.name) is disabled - skipping."
|
|
continue
|
|
}
|
|
|
|
$installedVer = Get-InstalledVersion -State $state -Name $pkg.name
|
|
|
|
if (Compare-Versions -Installed $installedVer -Available $pkg.version) {
|
|
|
|
# No state entry: probe the live system before downloading
|
|
if (-not $installedVer -and $pkg.checkInstalled -and (Test-PackageInstalled -Check $pkg.checkInstalled)) {
|
|
Write-Log "Package $($pkg.name) detected as already installed - recording as $($pkg.version), skipping download."
|
|
Set-InstalledVersion -State $state -Name $pkg.name -Version $pkg.version
|
|
Save-State -State $state
|
|
continue
|
|
}
|
|
|
|
Write-Log "Package update: $($pkg.name) $(if ($installedVer) { $installedVer } else { 'not installed' }) -> $($pkg.version)"
|
|
try {
|
|
Install-Package -Pkg $pkg -State $state
|
|
Save-State -State $state
|
|
$updatesApplied++
|
|
if ($pkg.restartRequired -eq $true) { $restartNeeded = $true }
|
|
} catch {
|
|
Write-LogError "Package '$($pkg.name)' failed: $_"
|
|
$updatesFailed++
|
|
}
|
|
} else {
|
|
Write-Log "Package $($pkg.name) is up to date ($installedVer)."
|
|
}
|
|
}
|
|
} else {
|
|
Write-LogWarn "Internal manifest has no 'packages' array - skipping."
|
|
}
|
|
|
|
# -- Summary -------------------------------------------------------------------
|
|
Write-Log "Done. Applied: $updatesApplied Failed: $updatesFailed"
|
|
|
|
if ($restartNeeded) {
|
|
Write-Log "*** One or more packages require a restart to complete installation. ***" "WARN"
|
|
}
|
|
|
|
# Clean up temp dir if empty
|
|
Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
|
|
if ($updatesFailed -gt 0) {
|
|
Write-LogError "Some updates failed - check log: $logFile"
|
|
exit 1
|
|
}
|