diff --git a/scripts/fetch-herdr-annotate.ps1 b/scripts/fetch-herdr-annotate.ps1 index 08cdc3f..4eaff10 100644 --- a/scripts/fetch-herdr-annotate.ps1 +++ b/scripts/fetch-herdr-annotate.ps1 @@ -35,6 +35,27 @@ if ($env:HERDR_ANNOTATE_BIN) { exit 0 } +# .NET rather than Get-FileHash: in Windows PowerShell 5.1, Get-FileHash is a script function of +# Microsoft.PowerShell.Utility, not a compiled cmdlet. A powershell.exe started by a process that +# came from PowerShell 7 inherits a PSModulePath listing PowerShell 7's modules first, loads that +# Core-only Utility manifest instead, and Get-FileHash does not exist. SHA256 from .NET is the +# same in 5.1 and 7 whatever the module path says. +function Get-Sha256Hex { + param([Parameter(Mandatory = $true)][string]$LiteralPath) + + $stream = [System.IO.File]::OpenRead($LiteralPath) + try { + $sha256 = [System.Security.Cryptography.SHA256]::Create() + try { + [System.BitConverter]::ToString($sha256.ComputeHash($stream)).Replace("-", "").ToLowerInvariant() + } finally { + $sha256.Dispose() + } + } finally { + $stream.Dispose() + } +} + $architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() $target = switch ($architecture) { "X64" { "x86_64-pc-windows-msvc" } @@ -51,7 +72,7 @@ try { $line = Get-Content -LiteralPath (Join-Path $temporary "SHA256SUMS") | Where-Object { $_ -match "\s$([regex]::Escape($asset))$" } | Select-Object -First 1 if (-not $line) { throw "$asset is not listed in $base/SHA256SUMS" } $expected = ($line -split "\s+")[0].ToLowerInvariant() - $actual = (Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path $temporary $asset)).Hash.ToLowerInvariant() + $actual = Get-Sha256Hex -LiteralPath (Join-Path $temporary $asset) if ($actual -ne $expected) { throw "sha256 mismatch for ${asset}: expected $expected, got $actual" } Copy-Item -Force -LiteralPath (Join-Path $temporary $asset) -Destination "$destination.tmp" Move-Item -Force -LiteralPath "$destination.tmp" -Destination $destination diff --git a/scripts/fetch-plannotator-tui.ps1 b/scripts/fetch-plannotator-tui.ps1 index 79a4ac8..3851834 100644 --- a/scripts/fetch-plannotator-tui.ps1 +++ b/scripts/fetch-plannotator-tui.ps1 @@ -114,6 +114,27 @@ function Install-PlannotatorTui { } } +# .NET rather than Get-FileHash: in Windows PowerShell 5.1, Get-FileHash is a script function of +# Microsoft.PowerShell.Utility, not a compiled cmdlet. A powershell.exe started by a process that +# came from PowerShell 7 inherits a PSModulePath listing PowerShell 7's modules first, loads that +# Core-only Utility manifest instead, and Get-FileHash does not exist. SHA256 from .NET is the +# same in 5.1 and 7 whatever the module path says. +function Get-Sha256Hex { + param([Parameter(Mandatory = $true)][string]$LiteralPath) + + $stream = [System.IO.File]::OpenRead($LiteralPath) + try { + $sha256 = [System.Security.Cryptography.SHA256]::Create() + try { + [System.BitConverter]::ToString($sha256.ComputeHash($stream)).Replace("-", "").ToLowerInvariant() + } finally { + $sha256.Dispose() + } + } finally { + $stream.Dispose() + } +} + if ($hasLocalOverride) { if (-not (Test-Path -LiteralPath $localOverride -PathType Leaf)) { throw "PLANNOTATOR_TUI_BIN is not a file: $localOverride" @@ -166,7 +187,7 @@ try { } $checksumFields = $matches[0].Trim() -split "\s+" $expected = $checksumFields[0].ToLowerInvariant() - $actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $downloadedAsset).Hash.ToLowerInvariant() + $actual = Get-Sha256Hex -LiteralPath $downloadedAsset if ($actual -ne $expected) { throw "sha256 mismatch for ${asset}: expected $expected, got $actual" } diff --git a/scripts/test-windows-full-manifest.py b/scripts/test-windows-full-manifest.py index 82c33c0..d13fda2 100644 --- a/scripts/test-windows-full-manifest.py +++ b/scripts/test-windows-full-manifest.py @@ -109,6 +109,11 @@ "stage-plannotator-tui.ps1", ], ] +# The Windows builds run under powershell.exe 5.1, where Get-FileHash is a script function of +# Microsoft.PowerShell.Utility. Started from a PowerShell 7 session, 5.1 inherits a PSModulePath +# that loads PowerShell 7's Core-only Utility manifest instead and Get-FileHash is missing, so the +# fetchers hash with .NET. +WINDOWS_FETCHERS = ["scripts/fetch-herdr-annotate.ps1", "scripts/fetch-plannotator-tui.ps1"] def fail(path: Path, message: str) -> None: @@ -218,6 +223,17 @@ def check_distributed(path: Path, version_path: Path, native_version_path: Path) check_no_link_capture(path, manifest) +def check_windows_fetchers(root: Path) -> None: + for relative in WINDOWS_FETCHERS: + path = root / relative + text = path.read_text(encoding="utf-8") + for number, line in enumerate(text.splitlines(), 1): + if "Get-FileHash" in line.split("#", 1)[0]: + fail(path, f"line {number} calls Get-FileHash, which 5.1 may not have") + if "Get-Sha256Hex -LiteralPath" not in text: + fail(path, "the download is not checked with Get-Sha256Hex") + + def check_development(path: Path) -> None: manifest = load(path) check_top_level_windows(path, manifest) @@ -411,6 +427,7 @@ def main() -> None: root / "windows-full" / "herdr-plugin.toml", root / "herdr-plugin.toml", ) + check_windows_fetchers(root) lite = root / "lite" / "herdr-plugin.toml" check_no_link_capture(lite, load(lite)) check_links(root / "links" / "herdr-plugin.toml", root / "herdr-plugin.toml")