Public/Close-MSIXPackage.ps1


function Close-MSIXPackage {
<#
.SYNOPSIS
    Packs an expanded MSIX folder back into a signed .msix file.
 
.DESCRIPTION
    Converts config.json.xml to config.json (via XSLT), removes redundant PSF
    launcher stubs when renamed per-app copies exist, then calls MakeAppx to
    repack the folder.
 
    The temporary extraction folder is deleted after packing unless -KeepMSIXFolder
    is specified or the module configuration value KeepTempFolder is $true.
 
.PARAMETER MSIXFolder
    Path to the expanded MSIX package folder.
 
.PARAMETER MSIXFile
    Target path for the repacked .msix file.
 
.PARAMETER KeepMSIXFolder
    When specified, the temporary folder is kept after packing.
    When omitted, the module configuration value KeepTempFolder is used (default: $false).
 
.PARAMETER Force
    Removes an existing output file before packing. Without this switch the
    function relies on MakeAppx -o; use -Force when MakeAppx fails because
    the output file is locked or already present.
 
.PARAMETER PrettyPrint
    Re-formats AppxManifest.xml with indentation and line breaks before packing,
    so the manifest inside the package stays human-readable.
 
.PARAMETER RegenerateResource
    Rebuilds resources.pri with makepri before packing. Use after changing assets so Windows
    resolves the icons correctly (fixes plated/boxed Start-menu icons caused by a stale index).
 
.EXAMPLE
    Close-MSIXPackage -MSIXFolder "C:\Temp\MSIXTemp" -MSIXFile "C:\Temp\MyApp.msix"
 
.EXAMPLE
    Close-MSIXPackage -MSIXFolder "C:\Temp\MSIXTemp" -MSIXFile "C:\Temp\MyApp.msix" -Force
 
.EXAMPLE
    Close-MSIXPackage -MSIXFolder "C:\Temp\MSIXTemp" -MSIXFile "C:\Temp\MyApp.msix" -KeepMSIXFolder
 
.NOTES
    https://www.nick-it.de
    Andreas Nick, 2024
#>



    [CmdletBinding()]
    #[OutputType([int])]
    param(
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            #ValueFromPipelineByPropertyName = $true,
            Position = 0)]
        [System.IO.DirectoryInfo] $MSIXFolder,
        [Parameter(Mandatory = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 1)]
        [System.IO.FileInfo] $MSIXFile,
        [Switch] $KeepMSIXFolder,
        [Switch] $Force,
        [Switch] $PrettyPrint,
        [Switch] $RegenerateResource
    )

    process {
        if (-not (Test-Path $MSIXFolder)) {
            Write-Error "The MSIX temporary folder does not exist."
            return $null
        }
        else {
            if (Test-Path $MSIXFile.FullName) {
                if ($Force) {
                    Remove-Item $MSIXFile.FullName -Force
                    Write-Verbose "Removed existing output file: $($MSIXFile.FullName)"
                } else {
                    Write-Warning "Output file already exists: $($MSIXFile.FullName). Use -Force to overwrite."
                }
            }
            $configFile = Join-Path $MSIXFolder -ChildPath "config.json.xml"
            #Create config.json
            if(Test-Path $configFile){
                Convert-MSIXPSFXML2JSON -xml $configFile -xsl (Join-Path -path $Script:ScriptPath -ChildPath "Data\Format.xsl") -output (Join-Path $MSIXFolder -ChildPath "config.json")
            } else {
                Write-Warning "No config.json.xml found. A config.json is not created."
            }
            # Remove original PSF launcher stubs when renamed per-app copies exist.
            # Best practice renames PsfLauncher64/32.exe to <AppId>_PsfLauncherX.exe;
            # the originals are then redundant and must not be packed.
            $renamedLaunchers = @(Get-ChildItem -Path $MSIXFolder.FullName -Filter '*_PsfLauncher?.exe' -ErrorAction SilentlyContinue)
            if ($renamedLaunchers.Count -gt 0) {
                foreach ($stubName in @('PsfLauncher32.exe', 'PsfLauncher64.exe')) {
                    $stubPath = Join-Path $MSIXFolder.FullName $stubName
                    if (Test-Path $stubPath) {
                        Remove-Item $stubPath -Force -ErrorAction SilentlyContinue
                        Write-Verbose "Removed original launcher stub: $stubName"
                    }
                }
            }

            $manifestPath = Join-Path $MSIXFolder.FullName 'AppxManifest.xml'

            # Re-serialize the manifest with indentation so the packed copy is readable.
            if ($PrettyPrint -and (Test-Path $manifestPath)) {
                $fmtDoc = New-Object System.Xml.XmlDocument
                $fmtDoc.PreserveWhitespace = $false
                $fmtDoc.Load($manifestPath)

                $settings = New-Object System.Xml.XmlWriterSettings
                $settings.Indent = $true
                $settings.IndentChars = ' '
                $settings.NewLineChars = "`r`n"
                $settings.Encoding = New-Object System.Text.UTF8Encoding($false)

                $writer = [System.Xml.XmlWriter]::Create($manifestPath, $settings)
                try { $fmtDoc.Save($writer) } finally { $writer.Dispose() }
                Write-Verbose "Pretty-printed AppxManifest.xml"
            }

            if (Test-Path $manifestPath) {
                $valid = Test-MSIXManifest -ManifestPath $manifestPath -Verbose:($VerbosePreference -eq 'Continue')
                if (-not $valid) {
                    Write-Warning "AppxManifest.xml has schema validation errors (see warnings above). Packing may fail."
                }
            }

            # Rebuild resources.pri so Windows resolves the (possibly changed) assets correctly.
            if ($RegenerateResource) {
                $makepri = Join-Path $Script:MSIXPackagingPath 'makepri.exe'
                if (-not (Test-Path $makepri)) {
                    Write-Warning "makepri.exe not found at '$makepri'. Run Update-MSIXTooling. Skipping resource index regeneration."
                }
                elseif (-not (Test-Path $manifestPath)) {
                    Write-Warning "AppxManifest.xml not found - skipping resource index regeneration."
                }
                else {
                    $priConfig = Join-Path $env:TEMP ('priconfig_' + [System.Guid]::NewGuid().ToString('N') + '.xml')
                    $priFile   = Join-Path $MSIXFolder.FullName 'resources.pri'
                    try {
                        # Remove the stale index first so makepri writes a clean one.
                        if (Test-Path $priFile) { Remove-Item $priFile -Force }

                        if ($VerbosePreference -eq 'Continue') {
                            & $makepri createconfig /cf $priConfig /dq 'en-US' /o | Out-Default
                            & $makepri new /pr $MSIXFolder.FullName /cf $priConfig /mn $manifestPath /of $priFile /o | Out-Default
                        }
                        else {
                            & $makepri createconfig /cf $priConfig /dq 'en-US' /o | Out-Null
                            & $makepri new /pr $MSIXFolder.FullName /cf $priConfig /mn $manifestPath /of $priFile /o | Out-Null
                        }

                        if ($LASTEXITCODE -ne 0) {
                            Write-Warning "makepri returned exit code $LASTEXITCODE - resources.pri may be incomplete."
                        }
                        else {
                            Write-Verbose "Regenerated resources.pri via makepri."
                        }
                    }
                    finally {
                        if (Test-Path $priConfig) { Remove-Item $priConfig -Force -ErrorAction SilentlyContinue }
                    }
                }
            }

            if($VerbosePreference -eq 'Continue'){
                MakeAppx pack -o -p $MsixFile.FullName -d  $MSIXFolder.FullName | Out-Default
            } else {
                MakeAppx pack -o -p $MsixFile.FullName -d  $MSIXFolder.FullName | Out-Null
            }
            
            #-l
            if ($lastexitcode -ne 0) {
                Write-Error "ERROR: MSIX Cannot close Package"
                return $null
            }
            else {
                if ($PSBoundParameters.ContainsKey('KeepMSIXFolder')) {
                    $keepFolder = $KeepMSIXFolder.IsPresent
                } else {
                    $keepFolder = $Script:MSIXForceletsConfig.KeepTempFolder
                }

                if (-not $keepFolder) {
                    Write-Verbose "Remove MSIX temp folder $MSIXFolder"
                    Remove-Item $MSIXFolder -Recurse -Confirm:$false -ErrorAction SilentlyContinue
                    if (Test-Path $MSIXFolder) {
                        Write-Verbose "Force remove MSIX temp folder $MSIXFolder"
                        & Cmd.exe /C rmdir /S /Q "$MSIXFolder" 2>$null
                    }
                }
            }
            
        }
    }
}