Private/Get-FoPluginBundleMetadata.ps1
|
# Plugin bundle metadata — ps-file-optimizer-aux GitHub Release (plain .zip). # CI cache keys in .github/workflows/ci.yml must be updated when Sha256 values below change. $script:FoPluginBundleVersion = '1.0.0' $script:FoPluginBundleReleaseTag = 'plugins-v1.0.0' $script:FoPluginBundleFormat = 'zip' $script:FoPluginBundles = @{ '64' = @{ Url = 'https://github.com/Aurocosh/ps-file-optimizer-aux/releases/download/plugins-v1.0.0/fo-plugins-win-x64-1.0.0.zip' FileName = 'fo-plugins-win-x64-1.0.0.zip' Sha256 = 'd2889306b31c3bb8b356e2d6de0d5f893f26e2e334812c0e541b9c0fe65a04a0' Folder = 'Plugins64' } '32' = @{ Url = 'https://github.com/Aurocosh/ps-file-optimizer-aux/releases/download/plugins-v1.0.0/fo-plugins-win-x86-1.0.0.zip' FileName = 'fo-plugins-win-x86-1.0.0.zip' Sha256 = '96bce923ca76a95db522eeea269a031a2b2a648fc0b44b45ef2a1fec202bc5b6' Folder = 'Plugins32' } } # Legacy single-bundle aliases (x64) $script:FoPluginBundleUrl = $script:FoPluginBundles['64'].Url $script:FoPluginBundleFileName = $script:FoPluginBundles['64'].FileName $script:FoPluginBundleSha256 = $script:FoPluginBundles['64'].Sha256 function Resolve-FoPluginBundleArchitecture { [CmdletBinding()] [OutputType([string])] param( [ValidateSet('Auto', '32', '64')] [string]$Architecture = 'Auto' ) if ($Architecture -eq 'Auto') { if ([Environment]::Is64BitProcess) { return '64' } return '32' } return $Architecture } function Resolve-FoPluginArchitectureFromPath { [CmdletBinding()] [OutputType([string])] param( [string]$PluginPath ) if ($PluginPath) { $leaf = Split-Path -Leaf ($PluginPath.TrimEnd('\', '/')) if ($leaf -ieq 'Plugins32') { return '32' } if ($leaf -ieq 'Plugins64') { return '64' } } return Resolve-FoPluginBundleArchitecture -Architecture Auto } function Get-FoPluginBundleFolderName { [CmdletBinding()] [OutputType([string])] param( [ValidateSet('32', '64')] [Parameter(Mandatory)] [string]$Architecture ) if ($Architecture -eq '64') { return 'Plugins64' } return 'Plugins32' } function Get-FoPluginInstallRootPath { [CmdletBinding()] [OutputType([string])] param( [string]$ModuleRoot = $script:FoModuleRoot ) if (-not $ModuleRoot) { return $null } return [System.IO.Path]::GetFullPath($ModuleRoot) } function Get-FoInstalledPluginArchitecturePaths { [CmdletBinding()] [OutputType([object[]])] param( [string]$ModuleRoot = $script:FoModuleRoot ) $root = Get-FoPluginInstallRootPath -ModuleRoot $ModuleRoot if (-not $root) { return @() } $paths = @() foreach ($name in @('Plugins64', 'Plugins32', 'plugins')) { $candidate = Join-Path $root $name if (Test-Path -LiteralPath $candidate) { $paths += [PSCustomObject]@{ Name = $name Path = $candidate } } } return $paths } function Remove-FoInstalledPluginArchitectures { [CmdletBinding(SupportsShouldProcess)] [OutputType([object[]])] param( [string]$ModuleRoot = $script:FoModuleRoot, [ValidateSet('32', '64', 'All')] [string]$Scope = 'All', [string[]]$ExcludeFolderNames = @() ) $root = Get-FoPluginInstallRootPath -ModuleRoot $ModuleRoot if (-not $root) { return @() } $targets = switch ($Scope) { 'All' { @('Plugins64', 'Plugins32', 'plugins') } '64' { @('Plugins32', 'plugins') } '32' { @('Plugins64', 'plugins') } } $removed = @() foreach ($name in $targets) { if ($name -in $ExcludeFolderNames) { continue } $path = Join-Path $root $name if (-not (Test-Path -LiteralPath $path)) { continue } if ($PSCmdlet.ShouldProcess($path, 'Remove plugin directory')) { Remove-Item -LiteralPath $path -Recurse -Force $removed += $path } } return $removed } function Assert-FoBundleSha256Policy { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Url, [string]$Sha256, [switch]$AllowUnverifiedDownload, [Parameter(Mandatory)] [string]$BundleLabel ) if ($Sha256) { return } if ($AllowUnverifiedDownload) { Write-Warning "Downloading $BundleLabel from '$Url' without SHA256 verification." return } throw "Custom $BundleLabel URL requires SHA256 verification. Supply -ArchiveSha256, set the matching FO_*_BUNDLE_SHA256 environment variable, or pass -AllowUnverifiedDownload to skip verification." } function Get-FoPluginBundleSettings { [CmdletBinding()] param( [ValidateSet('Auto', '32', '64')] [string]$Architecture = 'Auto', [string]$ArchiveUrl, [string]$ArchiveSha256, [switch]$AllowUnverifiedDownload ) if ($env:FO_PLUGIN_BUNDLE_URL) { $fileName = if ($env:FO_PLUGIN_BUNDLE_FILENAME) { $env:FO_PLUGIN_BUNDLE_FILENAME } else { [System.IO.Path]::GetFileName(($env:FO_PLUGIN_BUNDLE_URL -split '\?')[0]) } $sha256 = if ($env:FO_PLUGIN_BUNDLE_SHA256) { $env:FO_PLUGIN_BUNDLE_SHA256 } else { $ArchiveSha256 } Assert-FoBundleSha256Policy -Url $env:FO_PLUGIN_BUNDLE_URL -Sha256 $sha256 ` -AllowUnverifiedDownload:$AllowUnverifiedDownload -BundleLabel 'plugin bundle' $arch = Resolve-FoPluginBundleArchitecture -Architecture $(if ($env:FO_PLUGIN_BUNDLE_ARCH) { $env:FO_PLUGIN_BUNDLE_ARCH } else { $Architecture }) return [PSCustomObject]@{ Architecture = $arch Url = $env:FO_PLUGIN_BUNDLE_URL FileName = $fileName Sha256 = $sha256 Format = if ($env:FO_PLUGIN_BUNDLE_FORMAT) { $env:FO_PLUGIN_BUNDLE_FORMAT } else { 'zip' } Folder = if ($env:FO_PLUGIN_BUNDLE_FOLDER) { $env:FO_PLUGIN_BUNDLE_FOLDER } else { Get-FoPluginBundleFolderName -Architecture $arch } } } if ($ArchiveUrl) { Assert-FoBundleSha256Policy -Url $ArchiveUrl -Sha256 $ArchiveSha256 ` -AllowUnverifiedDownload:$AllowUnverifiedDownload -BundleLabel 'plugin bundle' $arch = Resolve-FoPluginBundleArchitecture -Architecture $Architecture return [PSCustomObject]@{ Architecture = $arch Url = $ArchiveUrl FileName = [System.IO.Path]::GetFileName(($ArchiveUrl -split '\?')[0]) Sha256 = $ArchiveSha256 Format = 'zip' Folder = Get-FoPluginBundleFolderName -Architecture $arch } } $resolvedArch = Resolve-FoPluginBundleArchitecture -Architecture $Architecture $entry = $script:FoPluginBundles[$resolvedArch] return [PSCustomObject]@{ Architecture = $resolvedArch Url = $entry.Url FileName = $entry.FileName Sha256 = $entry.Sha256 Format = $script:FoPluginBundleFormat Folder = $entry.Folder } } function Test-FoDownloadedFileSha256 { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path, [string]$ExpectedSha256, [switch]$AllowUnverifiedDownload ) if (-not $ExpectedSha256) { if ($AllowUnverifiedDownload) { return } throw 'Downloaded bundle SHA256 is required but was not provided.' } $actual = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() $expected = $ExpectedSha256.ToLowerInvariant() if ($actual -ne $expected) { throw "Downloaded bundle SHA256 mismatch. Expected $expected, got $actual." } } # Executables present in Plugins64 but absent from FO Plugins32 (not required for x86 bundle install). $script:FoPlugin64OnlyExecutables = @( 'minify.exe' 'optivorbis.exe' 'tinydng-cli.exe' ) # Ghostscript is chosen at runtime in PDF.ps1 via -Executable $gs (not a string literal). function Get-FoGhostscriptExecutableName { [CmdletBinding()] [OutputType([string])] param( [ValidateSet('32', '64')] [string]$Architecture = $(if ([Environment]::Is64BitProcess) { '64' } else { '32' }) ) if ($Architecture -eq '64') { return 'gswin64c.exe' } return 'gswin32c.exe' } function Get-FoRequiredPluginExecutables { [CmdletBinding()] [OutputType([object[]])] param( [ValidateSet('32', '64')] [string]$Architecture = $(if ([Environment]::Is64BitProcess) { '64' } else { '32' }) ) $exes = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) foreach ($e in (Get-FoPipelineDeclaredExecutables -Architecture $Architecture)) { [void]$exes.Add($e) } if ($Architecture -eq '32') { foreach ($only64 in $script:FoPlugin64OnlyExecutables) { [void]$exes.Remove($only64) } } return @($exes | Sort-Object) } function Test-FoPluginFilePresent { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [string]$PluginPath, [Parameter(Mandatory)] [string]$FileName ) if (-not $PluginPath -or -not (Test-Path -LiteralPath $PluginPath)) { return $false } $nameLower = $FileName.ToLowerInvariant() return [bool]( Get-ChildItem -LiteralPath $PluginPath -File -ErrorAction SilentlyContinue | Where-Object { $_.Name.ToLowerInvariant() -eq $nameLower } | Select-Object -First 1 ) } function Get-FoPluginSupportFilesForExecutables { [CmdletBinding()] [OutputType([object[]])] param( [Parameter(Mandatory)] [string[]]$Executables, [Parameter(Mandatory)] [string]$SourcePluginDir ) $support = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $sourceFiles = @() if (Test-Path -LiteralPath $SourcePluginDir) { $sourceFiles = Get-ChildItem -LiteralPath $SourcePluginDir -File -ErrorAction SilentlyContinue } function Add-IfExists([string]$Name) { if ($sourceFiles | Where-Object { $_.Name -ieq $Name }) { [void]$support.Add($Name) } } function Add-Glob([string]$Pattern) { foreach ($f in ($sourceFiles | Where-Object { $_.Name -like $Pattern })) { [void]$support.Add($f.Name) } } foreach ($exe in $Executables) { switch -Regex ($exe) { '^flac\.exe$' { Add-IfExists 'libFLAC.dll' } '^gzip\.exe$' { Add-IfExists 'zlib.dll' } '^gswin64c\.exe$' { Add-IfExists 'gsdll64.dll' } '^gswin32c\.exe$' { Add-IfExists 'gsdll32.dll' } '^qpdf\.exe$' { Add-Glob 'qpdf*.dll' } '^m7zrepacker\.exe$' { Add-IfExists 'm7zRepacker.ini' Add-IfExists '7z.exe' Add-IfExists '7z.dll' } '^tidy\.exe$' { Add-IfExists 'tidy.config' } '^magick\.exe$' { foreach ($f in ($sourceFiles | Where-Object { $_.Extension -ieq '.dll' })) { [void]$support.Add($f.Name) } } '^cwebp\.exe$' { Add-Glob 'libwebp*.dll' Add-Glob 'libsharpyuv*.dll' } '^mutool\.exe$' { Add-Glob 'mupdf*.dll' } '^strip\.exe$' { Add-Glob 'libwinpthread*.dll' Add-Glob 'libgcc*.dll' Add-Glob 'libstdc*.dll' } } } return @($support | Sort-Object) } function Get-FoPluginInstallFilePlan { [CmdletBinding()] [OutputType([object[]])] param( [Parameter(Mandatory)] [string[]]$Executables, [Parameter(Mandatory)] [string]$SourcePluginDir ) $files = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) foreach ($exe in $Executables) { [void]$files.Add($exe) } foreach ($f in (Get-FoPluginSupportFilesForExecutables -Executables $Executables -SourcePluginDir $SourcePluginDir)) { [void]$files.Add($f) } return @($files | Sort-Object) } function Get-FoMissingPluginExecutables { [CmdletBinding()] [OutputType([object[]])] param( [Parameter(Mandatory)] [string]$PluginPath, [string[]]$RequiredExecutables ) if (-not $RequiredExecutables) { $RequiredExecutables = Get-FoRequiredPluginExecutables } $missing = [System.Collections.Generic.List[string]]::new() foreach ($exe in $RequiredExecutables) { if (-not (Test-FoPluginFilePresent -PluginPath $PluginPath -FileName $exe)) { $missing.Add($exe) } } return @($missing) } function Resolve-FoBundledPluginDirectory { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ExtractRoot, [string]$ExpectedFolder ) $candidates = @() if ($ExpectedFolder) { $candidates += Join-Path $ExtractRoot $ExpectedFolder } $candidates += @( $ExtractRoot (Join-Path $ExtractRoot 'plugins') (Join-Path $ExtractRoot 'Plugins64') (Join-Path $ExtractRoot 'Plugins32') ) foreach ($dir in $candidates) { if (-not (Test-Path -LiteralPath $dir)) { continue } if (Test-FoPluginFilePresent -PluginPath $dir -FileName 'magick.exe') { return ([System.IO.Path]::GetFullPath($dir)) } } throw "Could not find plugin executables in extracted bundle at '$ExtractRoot'." } |