Public/Add-MSIXPSFDynamicLibraryFixup.ps1

function Add-MSIXPSFDynamicLibraryFixup {
<#
.SYNOPSIS
    Adds a DynamicLibraryFixup configuration entry for all package DLLs to config.json.xml.

.DESCRIPTION
    Scans the expanded MSIX package for application DLLs and registers them in
    config.json.xml under a DynamicLibraryFixup.dll fixup entry. This allows
    the PSF to resolve DLL load requests to the correct package-relative path,
    preventing failures when the application searches system paths for DLLs
    that only exist inside the MSIX container.

    PSF infrastructure DLLs (fixup DLLs, launchers, VC++ runtimes, Windows API
    sets) and DLLs located in VFS\SystemX64 / VFS\SystemX86 are excluded
    automatically.

    Architecture is inferred per DLL and written only when Tim Mangan PSF is
    active. The Microsoft PSF DynamicLibraryFixup does not support the
    architecture field; it uses only name and filepath.
      - Filename contains "32" or path contains ProgramFilesX86 -> x86
      - Filename contains "64" or path contains ProgramFilesX64 -> x64
      - Otherwise the value of -DefaultArchitecture is used (default: x64)

    The filepath is stored with double backslashes so that the PSF XML-to-JSON
    converter produces valid JSON output ("VFS\\...").

.PARAMETER MSIXFolder
    Path to the expanded MSIX package folder (must contain config.json.xml).

.PARAMETER Executable
    Regex pattern for the process entry in config.json.xml.
    Default: ".*" (catch-all for all processes).

.PARAMETER ExcludeNames
    Additional DLL filenames (with .dll extension) to exclude from the scan,
    e.g. @('MyHelper.dll').

.PARAMETER ExcludeFolders
    Package-relative folder paths whose DLLs are skipped entirely.
    Default: VFS\SystemX64 and VFS\SystemX86 (OS-managed DLLs).

.PARAMETER DefaultArchitecture
    Architecture written to entries whose architecture cannot be inferred from
    the filename or path. Valid values: x64 (default), x86.

.EXAMPLE
    Add-MSIXPSFDynamicLibraryFixup -MSIXFolder "C:\MSIXTemp\WinRAR"

.EXAMPLE
    Add-MSIXPSFDynamicLibraryFixup -MSIXFolder "C:\MSIXTemp\App" `
        -ExcludeNames @('LegacyHelper.dll') `
        -DefaultArchitecture x64 `
        -Verbose

.NOTES
    Requires Tim Mangan PSF DynamicLibraryFixup.dll to be present in the package.
    Run Add-MSIXPsfFrameworkFiles before this function.
    https://www.nick-it.de
    Andreas Nick, 2026
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [System.IO.DirectoryInfo] $MSIXFolder,

        [String] $Executable = '.*',

        [String[]] $ExcludeNames = @(),

        [String[]] $ExcludeFolders = @('VFS\SystemX64', 'VFS\SystemX86'),

        [ValidateSet('x64', 'x86')]
        [String] $DefaultArchitecture = 'x64'
    )

    process {
        if (-not (Test-Path $MSIXFolder.FullName -PathType Container)) {
            Write-Error "MSIXFolder not found: $($MSIXFolder.FullName)"
            return
        }

        $configXmlPath = Join-Path $MSIXFolder.FullName 'config.json.xml'
        if (-not (Test-Path $configXmlPath)) {
            Write-Error "config.json.xml not found in '$($MSIXFolder.FullName)'. Run Add-MSXIXPSFShim or another fixup function first."
            return
        }

        # PSF infrastructure DLL basename patterns (checked without extension)
        $psfExcludePatterns = @(
            '^DynamicLibraryFixup',
            '^FileRedirectionFixup',
            '^RegLegacyFixups',
            '^EnvVarFixup',
            '^TraceFixup',
            '^MFRFixup',
            '^PsfRuntime',
            '^PsfRunDll',
            '^PsfLauncher',
            '^msvcp',
            '^vcruntime',
            '^api-ms-win-',
            '^ucrtbase',
            '^concrt140'
        )

        $msixRoot = $MSIXFolder.FullName.TrimEnd('\')

        # Build absolute excluded folder prefixes for fast comparison
        $excludedPrefixes = $ExcludeFolders | ForEach-Object {
            (Join-Path $msixRoot $_).TrimEnd('\') + '\'
        }

        $allDlls = Get-ChildItem -Path $msixRoot -Filter '*.dll' -Recurse -File

        $pathEntries = New-Object System.Collections.ArrayList

        foreach ($dll in $allDlls) {
            $fullPath  = $dll.FullName
            $fileName  = $dll.Name
            $baseName  = [System.IO.Path]::GetFileNameWithoutExtension($fileName)

            # Skip DLLs in excluded folders
            $skip = $false
            foreach ($prefix in $excludedPrefixes) {
                if ($fullPath.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
                    $skip = $true
                    break
                }
            }
            if ($skip) {
                Write-Verbose "Skipping (excluded folder): $fileName"
                continue
            }

            # Skip PSF infrastructure DLLs
            $isPsf = $false
            foreach ($pat in $psfExcludePatterns) {
                if ($baseName -match $pat) {
                    $isPsf = $true
                    break
                }
            }
            if ($isPsf) {
                Write-Verbose "Skipping (PSF infrastructure): $fileName"
                continue
            }

            # Skip user-specified names (match with or without extension)
            $userExcluded = $false
            foreach ($excl in $ExcludeNames) {
                if ($excl -eq $fileName -or $excl -eq $baseName) {
                    $userExcluded = $true
                    break
                }
            }
            if ($userExcluded) {
                Write-Verbose "Skipping (user excluded): $fileName"
                continue
            }

            # Package-relative path with double backslashes for JSON compatibility.
            # Use String.Replace (not -replace regex) so each \ becomes \\.
            $relPath  = $fullPath.Substring($msixRoot.Length + 1)
            $relPathDoubleSlash = $relPath.Replace('\', '\\')

            # Infer architecture from filename, then from path
            $arch = $DefaultArchitecture
            if ($baseName -match '32') {
                $arch = 'x86'
            }
            elseif ($baseName -match '64') {
                $arch = 'x64'
            }
            elseif ($relPath -match '\\ProgramFilesX86\\') {
                $arch = 'x86'
            }
            elseif ($relPath -match '\\ProgramFilesX64\\') {
                $arch = 'x64'
            }

            $null = $pathEntries.Add([PSCustomObject]@{
                Name     = $fileName
                FilePath = $relPathDoubleSlash
                Arch     = $arch
            })
            Write-Verbose "Found DLL ($arch): $fileName -> $relPath"
        }

        if ($pathEntries.Count -eq 0) {
            Write-Warning "No application DLLs found in '$($MSIXFolder.FullName)'. No DynamicLibraryFixup entry added."
            return
        }

        # architecture field is Tim Mangan PSF-only; Microsoft PSF only supports name + filepath
        $writeArchitecture = $Script:PsfBasePath -like '*TimMangan*'

        Write-Verbose "Adding DynamicLibraryFixup for $($pathEntries.Count) DLL(s) to process '$Executable'."

        $conxml = New-Object xml
        $conxml.Load($configXmlPath)

        Initialize-MSIXPSFProcessSection -ConXml $conxml

        # Find or create the target process node
        $procExecNode = $conxml.SelectSingleNode(
            "//processes/process/executable[text()='$Executable']")
        if ($null -eq $procExecNode) {
            $proc = $conxml.CreateElement('process')
            $exec = $conxml.CreateElement('executable')
            $exec.InnerText = $Executable
            $null = $proc.AppendChild($exec)
            $null = $conxml.SelectSingleNode('//processes').AppendChild($proc)
            $procExecNode = $conxml.SelectSingleNode(
                "//processes/process/executable[text()='$Executable']")
        }
        $procNode = $procExecNode.ParentNode

        # Ensure <fixups> element exists on the process node
        $fixupsNode = $procNode.SelectSingleNode('fixups')
        if ($null -eq $fixupsNode) {
            $fixupsNode = $conxml.CreateElement('fixups')
            $null = $procNode.AppendChild($fixupsNode)
        }

        # Find existing DynamicLibraryFixup.dll entry or create a new one
        $existingDllNode = $fixupsNode.SelectSingleNode(
            "fixup/dll[text()='DynamicLibraryFixup.dll']")

        if ($null -ne $existingDllNode) {
            $fixupParent = $existingDllNode.ParentNode

            # Repair incomplete entries left by previous runs (e.g. dll-only without config)
            $configNode = $fixupParent.SelectSingleNode('config')
            if ($null -eq $configNode) {
                $configNode = $conxml.CreateElement('config')
                $forceEl2 = $conxml.CreateElement('forcePackageDllUse')
                $forceEl2.InnerText = 'true'
                $null = $configNode.AppendChild($forceEl2)
                $null = $fixupParent.AppendChild($configNode)
                Write-Verbose "Repaired existing DynamicLibraryFixup entry: added missing <config>."
            }

            $relativeDllPathsNode = $configNode.SelectSingleNode('relativeDllPaths')
            if ($null -eq $relativeDllPathsNode) {
                $relativeDllPathsNode = $conxml.CreateElement('relativeDllPaths')
                $null = $configNode.AppendChild($relativeDllPathsNode)
                Write-Verbose "Repaired existing DynamicLibraryFixup entry: added missing <relativeDllPaths>."
            }
        }
        else {
            $fixupEl = $conxml.CreateElement('fixup')

            $dllEl = $conxml.CreateElement('dll')
            $dllEl.InnerText = 'DynamicLibraryFixup.dll'
            $null = $fixupEl.AppendChild($dllEl)

            $configEl = $conxml.CreateElement('config')

            $forceEl = $conxml.CreateElement('forcePackageDllUse')
            $forceEl.InnerText = 'true'
            $null = $configEl.AppendChild($forceEl)

            $relativeDllPathsNode = $conxml.CreateElement('relativeDllPaths')
            $null = $configEl.AppendChild($relativeDllPathsNode)

            $null = $fixupEl.AppendChild($configEl)
            $null = $fixupsNode.AppendChild($fixupEl)
        }

        foreach ($entry in $pathEntries) {
            # Idempotent: skip if this DLL name is already registered
            $existing = $relativeDllPathsNode.SelectSingleNode(
                "relativeDllPath/name[text()='$($entry.Name)']")
            if ($null -ne $existing) {
                Write-Verbose "DLL already in config: $($entry.Name)"
                continue
            }

            $pathEl = $conxml.CreateElement('relativeDllPath')

            $nameEl = $conxml.CreateElement('name')
            $nameEl.InnerText = $entry.Name
            $null = $pathEl.AppendChild($nameEl)

            $fileEl = $conxml.CreateElement('filepath')
            $fileEl.InnerText = $entry.FilePath
            $null = $pathEl.AppendChild($fileEl)

            if ($writeArchitecture) {
                $archEl = $conxml.CreateElement('architecture')
                $archEl.InnerText = $entry.Arch
                $null = $pathEl.AppendChild($archEl)
            }

            $null = $relativeDllPathsNode.AppendChild($pathEl)
        }

        $conxml.PreserveWhiteSpace = $false
        $conxml.Save($configXmlPath)

        Write-Verbose "DynamicLibraryFixup configured: $($pathEntries.Count) DLL path(s) written to config.json.xml."
    }
}