Public/Invoke-MSIXCleanup.ps1


function Invoke-MSIXCleanup {
<#
.SYNOPSIS
    Removes unsupported or unnecessary artifacts from an expanded MSIX package.

.DESCRIPTION
    Cleans up an expanded MSIX package folder before repacking. Each cleanup action
    is controlled by a separate parameter that defaults to $true, so all actions run
    unless explicitly disabled.

    Cleanup actions:

      RemoveShortcutExtensions
          Removes desktop7:Extension Category="windows.shortcut" elements from
          AppxManifest.xml. These are not reliably supported and can cause
          packaging validation errors.

      RemoveInstallerFolder
          Deletes VFS\Windows\Installer and its contents. This folder contains
          MSI installer caches that have no function inside an MSIX container
          and can be very large.

      RemoveDebugSymbols
          Deletes all *.pdb files from the package tree. Debug symbols are not
          needed in production packages and increase package size.

      RemoveTempFiles
          Deletes *.tmp files and the VFS\Windows\Temp folder.

      RemoveLogFiles
          Deletes *.log files from the package tree. Installation logs left
          behind by setup routines have no purpose inside an MSIX container.

      RemoveEmptyDirectories
          Removes empty directories from the package tree after all other
          cleanup actions have run.

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

.PARAMETER RemoveShortcutExtensions
    Remove desktop7:Extension Category="windows.shortcut" from AppxManifest.xml.
    Default: $true.

.PARAMETER RemoveInstallerFolder
    Delete VFS\Windows\Installer. Default: $true.

.PARAMETER RemoveDebugSymbols
    Delete all *.pdb files. Default: $true.

.PARAMETER RemoveTempFiles
    Delete *.tmp files and VFS\Windows\Temp. Default: $true.

.PARAMETER RemoveLogFiles
    Delete *.log files. Default: $true.

.PARAMETER RemoveEmptyDirectories
    Remove empty directories after all other actions. Default: $true.

.EXAMPLE
    Invoke-MSIXCleanup -MSIXFolder "C:\MSIXTemp\WinRAR"

.EXAMPLE
    Invoke-MSIXCleanup -MSIXFolder "C:\MSIXTemp\App" -RemoveDebugSymbols $false

.NOTES
    https://www.nick-it.de
    Andreas Nick, 2026
#>

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

        [bool] $RemoveShortcutExtensions = $true,
        [bool] $RemoveInstallerFolder    = $true,
        [bool] $RemoveDebugSymbols       = $true,
        [bool] $RemoveTempFiles          = $true,
        [bool] $RemoveLogFiles           = $true,
        [bool] $RemoveEmptyDirectories   = $true
    )

    process {
        $manifestPath = Join-Path $MSIXFolder 'AppxManifest.xml'
        if (-not (Test-Path $manifestPath)) {
            Write-Error "AppxManifest.xml not found in: $($MSIXFolder.FullName)"
            return
        }

        # --- Remove desktop7 shortcut extensions from AppxManifest ---
        if ($RemoveShortcutExtensions) {
            $manifest = New-Object xml
            $manifest.Load($manifestPath)

            $nsBase  = 'http://schemas.microsoft.com/appx/manifest/foundation/windows10'
            $nsDesk7 = 'http://schemas.microsoft.com/appx/manifest/desktop/windows10/7'

            $nsmgr = New-Object System.Xml.XmlNamespaceManager($manifest.NameTable)
            $null = $nsmgr.AddNamespace('ns',    $nsBase)
            $null = $nsmgr.AddNamespace('desk7', $nsDesk7)

            $shortcutNodes = $manifest.SelectNodes(
                "//desk7:Extension[@Category='windows.shortcut']", $nsmgr)

            if ($shortcutNodes.Count -gt 0) {
                foreach ($node in $shortcutNodes) {
                    if ($PSCmdlet.ShouldProcess($manifestPath, "Remove desktop7:Extension windows.shortcut")) {
                        $null = $node.ParentNode.RemoveChild($node)
                    }
                }
                Write-Verbose "Removed $($shortcutNodes.Count) desktop7 shortcut extension(s) from AppxManifest.xml."

                # Remove empty Extensions elements left behind
                $emptyExtensions = $manifest.SelectNodes('//ns:Extensions[not(*)]', $nsmgr)
                foreach ($node in $emptyExtensions) {
                    $null = $node.ParentNode.RemoveChild($node)
                }

                $manifest.Save($manifestPath)
            }
            else {
                Write-Verbose "No desktop7 shortcut extensions found in AppxManifest.xml."
            }
        }

        # --- Remove VFS\Windows\Installer ---
        if ($RemoveInstallerFolder) {
            $installerPath = Join-Path $MSIXFolder.FullName 'VFS\Windows\Installer'
            if (Test-Path $installerPath) {
                if ($PSCmdlet.ShouldProcess($installerPath, 'Remove VFS\Windows\Installer')) {
                    Remove-Item $installerPath -Recurse -Force
                    Write-Verbose "Removed VFS\Windows\Installer."
                }
            }
            else {
                Write-Verbose "VFS\Windows\Installer not found — skipped."
            }
        }

        # --- Remove debug symbols ---
        if ($RemoveDebugSymbols) {
            $pdbFiles = Get-ChildItem -Path $MSIXFolder.FullName -Filter '*.pdb' -Recurse -ErrorAction SilentlyContinue
            foreach ($file in $pdbFiles) {
                if ($PSCmdlet.ShouldProcess($file.FullName, 'Remove .pdb file')) {
                    Remove-Item $file.FullName -Force
                }
            }
            if ($pdbFiles.Count -gt 0) {
                Write-Verbose "Removed $($pdbFiles.Count) .pdb file(s)."
            }
            else {
                Write-Verbose "No .pdb files found — skipped."
            }
        }

        # --- Remove temp files and VFS\Windows\Temp ---
        if ($RemoveTempFiles) {
            $tempFolder = Join-Path $MSIXFolder.FullName 'VFS\Windows\Temp'
            if (Test-Path $tempFolder) {
                if ($PSCmdlet.ShouldProcess($tempFolder, 'Remove VFS\Windows\Temp')) {
                    Remove-Item $tempFolder -Recurse -Force
                    Write-Verbose "Removed VFS\Windows\Temp."
                }
            }

            $tmpFiles = Get-ChildItem -Path $MSIXFolder.FullName -Filter '*.tmp' -Recurse -ErrorAction SilentlyContinue
            foreach ($file in $tmpFiles) {
                if ($PSCmdlet.ShouldProcess($file.FullName, 'Remove .tmp file')) {
                    Remove-Item $file.FullName -Force
                }
            }
            if ($tmpFiles.Count -gt 0) {
                Write-Verbose "Removed $($tmpFiles.Count) .tmp file(s)."
            }
            else {
                Write-Verbose "No .tmp files found — skipped."
            }
        }

        # --- Remove log files ---
        if ($RemoveLogFiles) {
            $logFiles = Get-ChildItem -Path $MSIXFolder.FullName -Filter '*.log' -Recurse -ErrorAction SilentlyContinue
            foreach ($file in $logFiles) {
                if ($PSCmdlet.ShouldProcess($file.FullName, 'Remove .log file')) {
                    Remove-Item $file.FullName -Force
                }
            }
            if ($logFiles.Count -gt 0) {
                Write-Verbose "Removed $($logFiles.Count) .log file(s)."
            }
            else {
                Write-Verbose "No .log files found — skipped."
            }
        }

        # --- Remove empty directories (bottom-up pass) ---
        if ($RemoveEmptyDirectories) {
            $removed = 0
            do {
                $emptyDirs = Get-ChildItem -Path $MSIXFolder.FullName -Recurse -Directory -ErrorAction SilentlyContinue |
                    Where-Object { (Get-ChildItem $_.FullName -Force -ErrorAction SilentlyContinue).Count -eq 0 }
                foreach ($dir in $emptyDirs) {
                    if ($PSCmdlet.ShouldProcess($dir.FullName, 'Remove empty directory')) {
                        Remove-Item $dir.FullName -Force -ErrorAction SilentlyContinue
                        $removed++
                    }
                }
            } while ($emptyDirs.Count -gt 0)

            if ($removed -gt 0) {
                Write-Verbose "Removed $removed empty director(ies)."
            }
        }
    }
}