Public/Update-MSIXTMPSF.ps1


function Update-MSIXTMPSF {
    <#
.SYNOPSIS
    Downloads and organises the latest Tim Mangan MSIX Package Support Framework release.

.DESCRIPTION
    Queries the GitHub releases for TimMangan/MSIX-PackageSupportFramework, downloads the
    latest ZipRelease asset, extracts the nested Debug and Release ZIP archives, and sorts
    every EXE and DLL into amd64\ or win32\ sub-folders based on their actual PE machine
    type (detected via Get-MSIXAppMachineType).

    The files are placed under:
        <module>\MSIXPSF\TimManganPSF\<release-name>\
            _debug\
                amd64\ <- x64 binaries
                win32\ <- x86 binaries
                         <- scripts / non-binary files
            _release\
                amd64\
                win32\

    The Visual C++ Runtime DLLs are not shipped by Tim Mangan. By default they are copied
    from the local Windows installation. A warning is issued for each file not found on the system.

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

.PARAMETER CopyVCRuntime
    Copies the Visual C++ Runtime DLLs that Tim Mangan does not ship from the local Windows
    installation into the amd64\ and win32\ sub-folders:
        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-MSIXTMPSF

.EXAMPLE
    Update-MSIXTMPSF -Force -Verbose

.EXAMPLE
    Update-MSIXTMPSF -CopyVCRuntime:$false

.NOTES
    Source: https://github.com/TimMangan/MSIX-PackageSupportFramework
    https://www.nick-it.de
    Andreas Nick, 2026
#>

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

    # Load ZIP support (required in PS 5.1 / .NET Framework 4.5+)
    Add-Type -AssemblyName System.IO.Compression.FileSystem

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

    $releasesUrl      = "https://api.github.com/repos/TimMangan/MSIX-PackageSupportFramework/releases"
    $timManganPsfPath = Join-Path $Script:MSIXPSFPath "TimManganPSF"

    # --- Query GitHub for latest release that carries a ZipRelease asset ---
    Write-Verbose "Querying GitHub for latest Tim Mangan PSF release..."
    try {
        $response = Invoke-WebRequest -Uri $releasesUrl -UseBasicParsing `
            -Headers @{ 'User-Agent' = 'MSIXForcelets' }
        $releases = $response.Content | ConvertFrom-Json
    }
    catch {
        Write-Error "Could not query GitHub releases: $_"
        return
    }

    $latestRelease = $null
    $zipAsset      = $null
    foreach ($release in $releases) {
        $asset = $release.assets |
            Where-Object { $_.name -like "ZipRelease*.zip" } |
            Select-Object -First 1
        if ($null -ne $asset) {
            $latestRelease = $release
            $zipAsset      = $asset
            break
        }
    }

    if ($null -eq $latestRelease) {
        Write-Warning "No Tim Mangan PSF release with a ZipRelease*.zip asset was found on GitHub."
        return
    }

    $assetName   = $zipAsset.name
    $downloadUrl = $zipAsset.browser_download_url

    # Extract date string from asset name, e.g. "ZipRelease.zip-v2026-2-22.zip" -> "2026-2-22"
    if ($assetName -match '-v(\d{4}-\d{1,2}-\d{1,2})') {
        $dateStr = $Matches[1]
    }
    else {
        $dateStr = $latestRelease.tag_name -replace '^[vV]', ''
    }

    $debugPath   = Join-Path $timManganPsfPath "$($dateStr)_debug"
    $releasePath = Join-Path $timManganPsfPath "$($dateStr)_release"

    Write-Verbose "Latest release : $($latestRelease.tag_name)"
    Write-Verbose "Asset : $assetName"
    Write-Verbose "Debug folder : $debugPath"
    Write-Verbose "Release folder : $releasePath"

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

    if (-not $Force) {
        if (-not $PSCmdlet.ShouldContinue(
                "Download Tim Mangan PSF '$assetName' from GitHub?",
                "Download PSF")) {
            return
        }
    }

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

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

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

        # --- Locate the inner Debug / Release ZIPs ---
        $innerZips  = Get-ChildItem -Path $tempExtract -Filter "*.zip" -Recurse
        $debugZip   = $innerZips | Where-Object { $_.Name -imatch 'debug' }   | Select-Object -First 1
        $releaseZip = $innerZips | Where-Object { $_.Name -imatch 'release' -and $_.Name -inotmatch 'debug' } |
            Select-Object -First 1

        if ($null -eq $debugZip -and $null -eq $releaseZip) {
            Write-Warning ("Could not identify Debug or Release ZIPs inside '$assetName'. " +
                "Found: $($innerZips.Name -join ', ')")
            return
        }

        # Build a lookup table: label -> @{ Path; Zip }
        $buildTypeMap = [ordered]@{}
        if ($null -ne $debugZip)   { $buildTypeMap['debug']   = @{ Path = $debugPath;   Zip = $debugZip } }
        if ($null -ne $releaseZip) { $buildTypeMap['release'] = @{ Path = $releasePath; Zip = $releaseZip } }

        # -----------------------------------------------------------------------
        # File routing tables - derived from the known release structure of
        # TimManganPSF\amd64\ and TimManganPSF\win32\.
        # -----------------------------------------------------------------------

        # Files that exist only in amd64\ (x64-exclusive natives)
        $amd64OnlyFiles = @(
            'PsfMonitorx64.exe',
            'vcruntime140_1.dll'        # x86 edition missing from Tim's package
        )

        # Files that exist only in win32\ (x86-exclusive)
        $win32OnlyFiles = @(
            'KernelTraceControl.Win61.dll',
            'PsfMonitorx86.exe'
        )

        # Native PE files that Tim ships for BOTH architectures under the same filename.
        # Use Get-MSIXAppMachineType to route each copy to the correct sub-folder.
        # amd64\: KernelTraceControl.dll (x64), msdia140.dll (x64), msvcp140.dll (x64),
        # PsfMonitor.exe (x64), ucrtbased.dll (x64), vcruntime140.dll (x64)
        # win32\: the same names but x86 builds
        $peDetectedBothFiles = @(
            'KernelTraceControl.dll',
            'msdia140.dll',
            'msvcp140.dll',
            'PsfMonitor.exe',
            'TraceReloggerLib.dll',
            'ucrtbased.dll',
            'vcruntime140.dll'
        )

        # AnyCPU .NET assemblies - PE header reports I386 but they are architecture-neutral.
        # Copy to BOTH amd64\ and win32\ so PsfMonitor works in either bitness.
        $copyBothFiles = @(
            'Microsoft.Diagnostics.FastSerialization.dll',
            'Microsoft.Diagnostics.Tracing.TraceEvent.dll',
            'OSExtensions.dll'
        )

        # PSF core files expected by Add-MSIXPsfFrameworkFiles
        $expectedRootFiles = @(
            'PsfLauncher32.exe',          'PsfLauncher64.exe',
            'PsfRunDll32.exe',            'PsfRunDll64.exe',
            'PsfRuntime32.dll',           'PsfRuntime64.dll',
            'DynamicLibraryFixup32.dll',  'DynamicLibraryFixup64.dll',
            'EnvVarFixup32.dll',          'EnvVarFixup64.dll',
            'FileRedirectionFixup32.dll', 'FileRedirectionFixup64.dll',
            'MFRFixup32.dll',             'MFRFixup64.dll',
            'RegLegacyFixups32.dll',      'RegLegacyFixups64.dll',
            'TraceFixup32.dll',           'TraceFixup64.dll',
            'StartingScriptWrapper.ps1',
            'StartMenuCmdScriptWrapper.ps1',
            'StartMenuShellLaunchWrapperScript.ps1'
        )
        # VCRuntime DLLs are NOT shipped by Tim - must be sourced from the local Windows installation
        $expectedAmd64Runtime = @('msvcp140.dll', 'msvcp140_1.dll', 'msvcp140_2.dll', 'vcruntime140.dll', 'vcruntime140_1.dll')
        $expectedWin32Runtime = @('msvcp140.dll', 'msvcp140_1.dll', 'msvcp140_2.dll', 'vcruntime140.dll')

        foreach ($buildType in $buildTypeMap.Keys) {
            $entry     = $buildTypeMap[$buildType]
            $buildPath = $entry.Path
            $innerZip  = $entry.Zip
            $innerGuid = [System.Guid]::NewGuid().ToString('N')
            $innerTemp = Join-Path $env:TEMP "TMPSFPSF_inner_$innerGuid"

            Write-Verbose "Extracting $($innerZip.Name) -> $buildPath ..."
            [System.IO.Compression.ZipFile]::ExtractToDirectory($innerZip.FullName, $innerTemp)

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

            # Route each extracted file to the correct destination:
            # amd64OnlyFiles → amd64\ only
            # win32OnlyFiles → win32\ only
            # peDetectedBothFiles → amd64\ or win32\ via Get-MSIXAppMachineType
            # copyBothFiles → both amd64\ and win32\ (AnyCPU .NET assemblies)
            # everything else → build root (_debug\ or _release\)
            $allFiles = Get-ChildItem -Path $innerTemp -File -Recurse
            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 $buildPath -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, Version.txt, etc.
                    Copy-Item $file.FullName -Destination $buildPath -Force
                }
            }

            # --- Warn about missing PSF core files (unexpected) ---
            $allExtractedNames = $allFiles | Select-Object -ExpandProperty Name
            foreach ($expected in $expectedRootFiles) {
                if ($allExtractedNames -notcontains $expected) {
                    Write-Warning "$buildType`: expected PSF file missing from package: $expected"
                }
            }

            # --- VCRuntime: copy from local Windows installation or warn if absent ---
            # msvcp140.dll, vcruntime140.dll, vcruntime140_1.dll are not shipped by Tim Mangan.
            $vcRuntimeSources = @{
                amd64 = @{ Dest = $amd64Path; Source = "$env:SystemRoot\System32"; Files = $expectedAmd64Runtime }
                win32 = @{ Dest = $win32Path; Source = "$env:SystemRoot\SysWOW64"; Files = $expectedWin32Runtime }
            }

            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."
                        }
                    }
                }
            }

            # --- Write SOURCE.txt and LICENSE.txt into the build folder ---
            @(
                "MSIX Package Support Framework by Tim Mangan",
                "=============================================",
                "",
                "Release : $($latestRelease.tag_name)",
                "Build : $buildType",
                "Asset : $assetName",
                "Downloaded: $(Get-Date -Format 'yyyy-MM-dd')",
                "GitHub : https://github.com/TimMangan/MSIX-PackageSupportFramework",
                "License : MIT (see LICENSE.txt)",
                "",
                "Files in this folder were downloaded from the GitHub repository listed above",
                "and are subject to the MIT License.",
                "",
                "Downloaded by MSIXForcelets - https://www.nick-it.de"
            ) | Set-Content -Path (Join-Path $buildPath "SOURCE.txt") -Encoding UTF8

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

            Remove-Item $innerTemp -Recurse -Force -ErrorAction SilentlyContinue
        }

        "Tim Mangan PSF downloaded to: $timManganPsfPath ($($dateStr)_debug / $($dateStr)_release)"
    }
    catch {
        Write-Error "Failed to download or extract Tim Mangan PSF: $_"
        throw
    }
    finally {
        if (Test-Path $tempZip)     { Remove-Item $tempZip     -Force -ErrorAction SilentlyContinue }
        if (Test-Path $tempExtract) { Remove-Item $tempExtract -Recurse -Force -ErrorAction SilentlyContinue }
    }
}