src/Engine/SelfUpdate.ps1
|
# SelfUpdate.ps1 — psmm's own version, update check and update guidance. # # Update semantics VERIFIED empirically against PSResourceGet 1.2.0 with a # local test repository (2026-07-14): # - a prerelease-label-only bump (0.1.0-beta2 -> 0.1.0-beta3) is invisible # to Update-PSResource (even with -Prerelease and an explicit -Version) # and to plain Install-PSResource; ONLY # `Install-PSResource <name> -Prerelease -Reinstall` installs it (the two # labels share the module's <base version> folder, so it replaces # in place) # - a base-version bump (0.1.0-x -> 0.2.0-x) updates normally via # `Update-PSResource <name> -Prerelease` (side-by-side folders) # - Remove-Module first is NOT needed: replacement succeeds while the # module is imported; the session keeps running the old copy until # `Import-Module <name> -Force` or a new session # # The check itself is a fire-and-forget ThreadJob writing a small JSON cache # next to the main config, throttled to once per day: profile load never # pays network cost - it only reads the cache from a previous session. # The RUNNING psmm version as a full semver string, e.g. '0.1.0-beta3'. function Get-PSMMVersionString { [CmdletBinding()] param() $m = $ExecutionContext.SessionState.Module if (-not $m) { return '' } $pre = $null try { $pre = $m.PrivateData.PSData.Prerelease } catch { } "$($m.Version)$(if ($pre) { "-$pre" })" } # The psmm version currently ON DISK in this module's install folder, e.g. # '0.1.0-beta4'. May differ from Get-PSMMVersionString after an in-session # `Install-PSResource psmm -Prerelease -Reinstall` (prerelease labels share # the base-version folder, so the files are replaced in place while the # session keeps running the old copy - see the header of this file). # Empty string when the manifest cannot be read. function Get-PSMMOnDiskVersionString { [CmdletBinding()] param() try { $d = Import-PowerShellDataFile (Join-Path $script:PSMMRoot 'psmm.psd1') $pre = $null try { $pre = $d.PrivateData.PSData.Prerelease } catch { } "$($d.ModuleVersion)$(if ($pre) { "-$pre" })" } catch { '' } } # NuGet-style semver comparison of two version strings (may carry prerelease # labels). Returns -1 / 0 / 1 for $A <, ==, > $B. Rules: base [version] # first; a stable version outranks any prerelease of the same base; # prerelease labels compare per dot-separated segment, numerically when both # segments are numeric, ordinal-case-insensitively otherwise. function Compare-PSMMVersion { [CmdletBinding()] param( [Parameter(Mandatory)][string]$A, [Parameter(Mandatory)][string]$B ) $baseA, $preA = $A -split '-', 2 $baseB, $preB = $B -split '-', 2 $c = ([version]$baseA).CompareTo([version]$baseB) if ($c -ne 0) { return [Math]::Sign($c) } if (-not $preA -and -not $preB) { return 0 } if (-not $preA) { return 1 } # stable > prerelease if (-not $preB) { return -1 } $segsA = $preA -split '\.' $segsB = $preB -split '\.' for ($i = 0; $i -lt [Math]::Max($segsA.Count, $segsB.Count); $i++) { if ($i -ge $segsA.Count) { return -1 } # fewer segments = smaller if ($i -ge $segsB.Count) { return 1 } $nA = 0; $nB = 0 $isNumA = [int]::TryParse($segsA[$i], [ref]$nA) $isNumB = [int]::TryParse($segsB[$i], [ref]$nB) $c = if ($isNumA -and $isNumB) { $nA.CompareTo($nB) } else { [string]::Compare($segsA[$i], $segsB[$i], [System.StringComparison]::OrdinalIgnoreCase) } if ($c -ne 0) { return [Math]::Sign($c) } } 0 } # Cache file for the daily background check (lives next to the main config, # so the $PSMM_MainConfigPath override redirects it too - tests rely on it). function Get-PSMMUpdateCachePath { [CmdletBinding()] param() Join-Path (Split-Path -Parent (Get-PSMMMainConfigPath)) 'psmm-update-check.json' } function Read-PSMMUpdateCache { [CmdletBinding()] param() $p = Get-PSMMUpdateCachePath if (-not (Test-Path -LiteralPath $p -ErrorAction Ignore)) { return $null } try { Get-Content -LiteralPath $p -Raw | ConvertFrom-Json } catch { $null } } # Kick the once-a-day gallery check as a fire-and-forget ThreadJob. Returns # the job when one was started, $null when throttled or unavailable. # Governed by the $PSMM_UpdateCheck knob (default $true). function Start-PSMMSelfUpdateCheck { [CmdletBinding()] param([switch]$Force) if (-not (Get-PSMMSetting -Name 'PSMM_UpdateCheck' -Default $true)) { return $null } if (-not (Get-Command Start-ThreadJob -ErrorAction SilentlyContinue)) { return $null } if (@(Get-PSMMTask | Where-Object { $_.Kind -eq 'selfupdate' -and -not $_.Done }).Count) { return $null } $cache = Read-PSMMUpdateCache if (-not $Force -and $cache -and $cache.CheckedAt) { try { # ConvertFrom-Json may have materialised CheckedAt as [datetime] $t = $cache.CheckedAt $checked = if ($t -is [datetime]) { $t.ToUniversalTime() } else { [datetime]::Parse("$t", [cultureinfo]::InvariantCulture, ([System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal)) } if (([datetime]::UtcNow - $checked) -lt [timespan]::FromHours(24)) { return $null } } catch { } } $path = Get-PSMMUpdateCachePath # via the task registry: the TUI shows it on the tasks screen and reacts # to its completion (Receive-PSMMUITask refreshes the update notice) Start-PSMMTask -Label 'check: psmm update' -Kind 'selfupdate' -ArgumentList @($path) -ScriptBlock { param($cachePath) try { $stable = Find-PSResource -Name psmm -Repository PSGallery -ErrorAction SilentlyContinue $pre = Find-PSResource -Name psmm -Repository PSGallery -Prerelease -ErrorAction SilentlyContinue $dir = Split-Path -Parent $cachePath if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } ([pscustomobject]@{ CheckedAt = [datetime]::UtcNow.ToString('o') LatestStable = $(if ($stable) { "$($stable.Version)$(if ($stable.Prerelease) { "-$($stable.Prerelease)" })" }) LatestPrerelease = $(if ($pre) { "$($pre.Version)$(if ($pre.Prerelease) { "-$($pre.Prerelease)" })" }) } | ConvertTo-Json) | Set-Content -LiteralPath $cachePath -Encoding utf8 } catch { } # best-effort: no network, no cache update } } # Is a newer psmm available, per the cached check? $null when up to date (or # no cache yet). Prerelease users are compared against the newest prerelease, # stable users only against the newest stable - and the returned Command is # the VERIFIED one for that jump (see the header of this file). function Test-PSMMUpdateAvailable { [CmdletBinding()] param() $current = Get-PSMMVersionString if (-not $current) { return $null } $cache = Read-PSMMUpdateCache if (-not $cache) { return $null } $onPrerelease = $current -match '-' $candidate = if ($onPrerelease) { $cache.LatestPrerelease } else { $cache.LatestStable } if (-not $candidate) { return $null } if ((Compare-PSMMVersion -A "$candidate" -B $current) -le 0) { return $null } $command = if ("$candidate" -match '-') { 'Install-PSResource psmm -Prerelease -Reinstall' } else { 'Update-PSResource psmm' } [pscustomobject]@{ Current = $current Latest = "$candidate" Command = $command } } |