Public/Update-MSIXMicrosoftPSF.ps1

function Update-MSIXMicrosoftPSF {
<#
.SYNOPSIS
    Downloads and organises the latest Microsoft MSIX Package Support Framework from NuGet.

.DESCRIPTION
    Queries the NuGet v3 API for the latest version of the Microsoft.PackageSupportFramework
    package, downloads the .nupkg (which is a ZIP archive), and sorts all EXE and DLL files
    into the correct sub-folders, matching the same layout used by Update-MSIXTMPSF.

    Files are placed under:
        <module>\MSIXPSF\MicrosoftPSF\<version>\
            amd64\ <- x64-specific binaries
            win32\ <- x86-specific binaries
                     <- shared binaries (32/64 suffix pairs) and scripts

    If StartingScriptWrapper.ps1 is not included in the NuGet package it is copied from the
    module's Libs folder (src\Libs\StartingScriptWrapper.ps1).

    SOURCE.txt and LICENSE.txt are written into each version folder on download.

    The Visual C++ Runtime DLLs are not included in the NuGet package. By default they are
    copied from the local Windows installation. A warning is issued for each file not found.

.PARAMETER Force
    Skips confirmation prompts and overwrites an existing version folder.

.PARAMETER CopyVCRuntime
    Copies the Visual C++ Runtime DLLs from the local Windows installation into amd64\ and win32\:
        amd64\: msvcp140.dll, msvcp140_1.dll, msvcp140_2.dll, vcruntime140.dll, vcruntime140_1.dll (from System32)
        win32\: msvcp140.dll, msvcp140_1.dll, msvcp140_2.dll, vcruntime140.dll (from SysWOW64)
    A warning is issued for each file not found on the local machine.
    When omitted, the module configuration value CopyVCRuntime is used (default: $true).
    Pass -CopyVCRuntime:$false to skip copying; a warning is still issued for each missing file.

.EXAMPLE
    Update-MSIXMicrosoftPSF

.EXAMPLE
    Update-MSIXMicrosoftPSF -Force -Verbose

.EXAMPLE
    Update-MSIXMicrosoftPSF -CopyVCRuntime:$false

.NOTES
    NuGet source : https://www.nuget.org/packages/Microsoft.PackageSupportFramework
    GitHub source: https://github.com/microsoft/MSIX-PackageSupportFramework
    https://www.nick-it.de
    Andreas Nick, 2026
#>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Switch] $Force,
        [System.Nullable[bool]] $CopyVCRuntime
    )

    Add-Type -AssemblyName System.IO.Compression.FileSystem

    if ($PSBoundParameters.ContainsKey('CopyVCRuntime')) {
        $resolvedCopyVC = [bool]$CopyVCRuntime
    } else {
        $resolvedCopyVC = $Script:MSIXForceletsConfig.CopyVCRuntime
    }

    $nugetBaseUrl     = $Script:MicrosoftPSFNuGetUrl
    $microsoftPsfPath = Join-Path $Script:MSIXPSFPath "MicrosoftPSF"
    $libsPath         = Join-Path $Script:ScriptPath "Libs"

    # --- Query NuGet v3 for the latest version ---
    Write-Verbose "Querying NuGet for latest Microsoft PSF version..."
    try {
        $indexResponse = Invoke-WebRequest -Uri "$nugetBaseUrl/index.json" -UseBasicParsing `
            -Headers @{ 'User-Agent' = 'MSIXForcelets' }
        $versionList   = ($indexResponse.Content | ConvertFrom-Json).versions
    }
    catch {
        Write-Error "Could not query NuGet index: $_"
        return
    }

    if ($null -eq $versionList -or $versionList.Count -eq 0) {
        Write-Error "NuGet returned an empty version list for Microsoft.PackageSupportFramework."
        return
    }

    $latestVersion = $versionList[-1]
    $downloadUrl   = "$nugetBaseUrl/$latestVersion/microsoft.packagesupportframework.$latestVersion.nupkg"
    $versionPath   = Join-Path $microsoftPsfPath $latestVersion

    Write-Verbose "Latest version : $latestVersion"
    Write-Verbose "Download URL : $downloadUrl"
    Write-Verbose "Target folder : $versionPath"

    # --- Skip if already present ---
    if ((Test-Path $versionPath) -and -not $Force) {
        Write-Verbose "Microsoft PSF version '$latestVersion' is already present. Use -Force to re-download."
        return
    }

    if (-not $Force) {
        if (-not $PSCmdlet.ShouldContinue(
                "Download Microsoft PSF $latestVersion from NuGet?",
                "Download PSF")) {
            return
        }
    }

    # --- Download .nupkg to %TEMP% ---
    $guid        = [System.Guid]::NewGuid().ToString('N')
    $tempZip     = Join-Path $env:TEMP "MSPsf_$guid.nupkg"
    $tempExtract = Join-Path $env:TEMP "MSPsf_$guid"

    try {
        Write-Verbose "Downloading $downloadUrl ..."
        Invoke-WebRequest -Uri $downloadUrl -OutFile $tempZip -UseBasicParsing

        Write-Verbose "Extracting NuGet package..."
        [System.IO.Compression.ZipFile]::ExtractToDirectory($tempZip, $tempExtract)

        # -----------------------------------------------------------------------
        # File routing tables - derived from the known MicrosoftPSF\OLD structure.
        # -----------------------------------------------------------------------

        # Files that belong exclusively in amd64\
        $amd64OnlyFiles = @(
            'PsfMonitorx64.exe',
            'vcruntime140_1.dll'
        )

        # Files that belong exclusively in win32\
        $win32OnlyFiles = @(
            'KernelTraceControl.Win61.dll',
            'PsfMonitorx86.exe'
        )

        # PE binaries shipped for both architectures under the same filename.
        # Get-MSIXAppMachineType determines whether each copy goes to amd64\ or win32\.
        $peDetectedBothFiles = @(
            'KernelTraceControl.dll',
            'msdia140.dll',
            'msvcp140.dll',
            'PsfMonitor.exe',
            'TraceReloggerLib.dll',
            'ucrtbased.dll',
            'vcruntime140.dll'
        )

        # AnyCPU .NET assemblies - copy to both amd64\ and win32\
        $copyBothFiles = @(
            'Microsoft.Diagnostics.FastSerialization.dll',
            'Microsoft.Diagnostics.Tracing.TraceEvent.dll',
            'OSExtensions.dll'
        )

        # Ignore NuGet metadata and C++ SDK files (headers, linker libs, build targets, package signature)
        $ignoreExtensions = @('.nuspec', '.psmdcp', '.rels', '.xml', '.h', '.lib', '.targets', '.p7s')

        $amd64Path = Join-Path $versionPath "amd64"
        $win32Path = Join-Path $versionPath "win32"
        New-Item -Path $amd64Path -ItemType Directory -Force | Out-Null
        New-Item -Path $win32Path -ItemType Directory -Force | Out-Null

        $allFiles = Get-ChildItem -Path $tempExtract -File -Recurse | Where-Object {
            $ignoreExtensions -notcontains $_.Extension
        }

        foreach ($file in $allFiles) {
            if ($amd64OnlyFiles -contains $file.Name) {
                Copy-Item $file.FullName -Destination $amd64Path -Force
            }
            elseif ($win32OnlyFiles -contains $file.Name) {
                Copy-Item $file.FullName -Destination $win32Path -Force
            }
            elseif ($peDetectedBothFiles -contains $file.Name) {
                $arch = Get-MSIXAppMachineType -FilePathName $file
                switch ($arch) {
                    'x64'  { Copy-Item $file.FullName -Destination $amd64Path -Force }
                    'I386' { Copy-Item $file.FullName -Destination $win32Path -Force }
                    default { Copy-Item $file.FullName -Destination $versionPath -Force }
                }
            }
            elseif ($copyBothFiles -contains $file.Name) {
                Copy-Item $file.FullName -Destination $amd64Path -Force
                Copy-Item $file.FullName -Destination $win32Path -Force
            }
            else {
                # PSF core binaries (PsfLauncher32/64, Fixup DLLs, scripts, etc.)
                Copy-Item $file.FullName -Destination $versionPath -Force
            }
        }

        # --- VCRuntime: copy from local Windows installation or warn if absent ---
        # msvcp140.dll, vcruntime140.dll, vcruntime140_1.dll are not included in the NuGet package.
        $vcRuntimeSources = @{
            amd64 = @{
                Dest   = $amd64Path
                Source = "$env:SystemRoot\System32"
                Files  = @('msvcp140.dll', 'msvcp140_1.dll', 'msvcp140_2.dll', 'vcruntime140.dll', 'vcruntime140_1.dll')
            }
            win32 = @{
                Dest   = $win32Path
                Source = "$env:SystemRoot\SysWOW64"
                Files  = @('msvcp140.dll', 'msvcp140_1.dll', 'msvcp140_2.dll', 'vcruntime140.dll')
            }
        }

        foreach ($archKey in $vcRuntimeSources.Keys) {
            $vcEntry = $vcRuntimeSources[$archKey]
            $present = Get-ChildItem -Path $vcEntry.Dest -File | Select-Object -ExpandProperty Name

            foreach ($vcFile in $vcEntry.Files) {
                if ($present -notcontains $vcFile) {
                    $sourcePath = Join-Path $vcEntry.Source $vcFile
                    if ($resolvedCopyVC) {
                        if (Test-Path $sourcePath) {
                            Copy-Item $sourcePath -Destination $vcEntry.Dest -Force
                            Write-Verbose "Copied $vcFile -> $archKey\"
                        }
                        else {
                            Write-Warning "$vcFile not found on this system ($sourcePath). Install the Visual C++ Redistributable."
                        }
                    }
                    else {
                        Write-Warning "$vcFile is missing from $archKey\. Use -CopyVCRuntime or set CopyVCRuntime via Set-MSIXForceletsConfiguration."
                    }
                }
            }
        }

        # --- Copy StartingScriptWrapper.ps1 from Libs if not already present ---
        $startingScript    = Join-Path $versionPath "StartingScriptWrapper.ps1"
        $startingScriptLib = Join-Path $libsPath "StartingScriptWrapper.ps1"

        if (-not (Test-Path $startingScript)) {
            if (Test-Path $startingScriptLib) {
                Copy-Item $startingScriptLib -Destination $versionPath -Force
                Write-Verbose "Copied StartingScriptWrapper.ps1 from Libs."
            }
            else {
                Write-Warning "StartingScriptWrapper.ps1 not found in the NuGet package or in $libsPath."
            }
        }

        # --- Write SOURCE.txt and LICENSE.txt into the version folder ---
        @(
            "Microsoft MSIX Package Support Framework",
            "========================================",
            "",
            "Version : $latestVersion",
            "Downloaded: $(Get-Date -Format 'yyyy-MM-dd')",
            "NuGet : $downloadUrl",
            "GitHub : https://github.com/microsoft/MSIX-PackageSupportFramework",
            "License : MIT (see LICENSE.txt)",
            "",
            "Files in this folder were downloaded from NuGet and are subject to the MIT License.",
            "",
            "Downloaded by MSIXForcelets - https://www.nick-it.de"
        ) | Set-Content -Path (Join-Path $versionPath "SOURCE.txt") -Encoding UTF8

        try {
            $licenseUrl = "https://raw.githubusercontent.com/microsoft/MSIX-PackageSupportFramework/main/LICENSE"
            Write-Verbose "Downloading LICENSE from $licenseUrl ..."
            Invoke-WebRequest -Uri $licenseUrl -OutFile (Join-Path $versionPath "LICENSE.txt") -UseBasicParsing
        }
        catch {
            Write-Warning "Could not download LICENSE file: $_"
        }

        "Microsoft PSF $latestVersion downloaded to: $versionPath"
    }
    catch {
        Write-Error "Failed to download or extract Microsoft PSF: $_"
        throw
    }
    finally {
        if (Test-Path $tempZip)     { Remove-Item $tempZip     -Force -ErrorAction SilentlyContinue }
        if (Test-Path $tempExtract) { Remove-Item $tempExtract -Recurse -Force -ErrorAction SilentlyContinue }
    }
}