Convert MSIXs to VHD or CIM for App Attach

ConversionsMSIX App Attach requires a format conversion from the standard msix file in order to attach the MSIX packages efficiently.  This efficiency is important in non-persistent image scenarios like pooled VDI, as well as semi-persistent scenarios like shared multi-user operating system connections (Server Based Computing, like RDS/RDMI as well as multiuser Windows 10) as the end-user doesn’t want to wait that long for the applications to arrive each time they log in.

Originally, MSIX App Attach was designed to work with a VHD image format.  This is not unlike many of the App Layering solutions that are out there.  When used this way, MSIX App Attach mounts the needed package vhd images and then quickly completes the necessary registrations without having to bring down the package contents from the share.

Available with the 20H2, and later, operating system versions is a different image format using the new CimFs, or CIM for short. There isn’t a lot of documentation out there currently on this, but I have been playing around.  The concept with CIM is to improve the scalability, as opposed to performance, in scenarios where there are large numbers of end-users and large numbers of packages.  One basic difference is that CIM, unlike VHD, is built to be a read-only mounting and this can lead to efficiencies in scale. Ryan Mangan (no relation) seems to confirm this in his testing. (I’ll note that in his testing he used small packages; large packages would not significantly affect VHD or CIM times but would significantly slow down applying the MSIX form to the system so the savings on App Attach will generally be greater than shown).

How to Get VHD or CIM package images

Microsoft has an open source project on GitHub called msix-packaging that includes a tool, called msixmgr, that may be used to perform a conversion from a MSIX file to either a VHD file or CIM. Microsoft’s Stefan Georgiev posed a blog about this in the Microsoft Community portal for Windows Virtual Desktop here: Simplify MSIX image creation with the MSIXMGR tool – Microsoft Tech Community

In that article is a link to a pre-built version of msixmgr so that you don’t have to build your own.  Additionally, the article contained a useful PowerShell example for converting a single package.  I have now built a PowerShell script, shown below, that will allow you to convert a folder structure of MSIX packages to either format (or both) in a single execution.  There are some small improvements in this script, such as estimating the size of the volume you’ll need from the MSIX file and ignoring optional parts of the MSIX filename that may be present depending on how that package was generated.

You just need to edit the variables at the top to define where to find the MSIX packages, where to put the outputs, and the folder where to find msixmgr (x64 subfolder).


 # Name: Convert_Msix2VhdAndOrCim.ps1
 #
 # Copyright: Tim Mangan, TMurgent Technologies LLP
 #
 # License:  Released under MIT license.
 #  
 # Purpose: This script may be used to convert an existing MSIX package file into VHD and/or CIM form for use in MSIX App Attach.
 #
 # Requirements:
 #     PassiveInstall https://github.com/TimMangan/PassiveInstall
 #     MsixMgr        https://github.com/microsoft/msix-packaging
 #
 # Usage:
 #    Modify the 5 variables below as required.  Then run via powershell.
 #    The script will create requested output folders and for each conversion type, place a single log file in its base folder.

 $MSIXContentStore = "C:\Users\Admin\Desktop\MSIX"           # Existing folder structure containing MSIX packages
 $SaveLocationCIM = "C:\Users\Admin\Desktop\CIM"             # Output folder for CIM packages, set to "" to disable CIM output
 $SaveLocationVHD = "C:\Users\Admin\Desktop\VHD"             # Output folder for VHD packages, set to "" to disable VHD output
 $MsixMgrFolder = "C:\Users\Admin\Desktop\Demo4\msixmgr"     # Folder containing the x64 version of msixmgr and dependencies
 $tracefile = ""                                             # Normally set to "" to disable tracing.  Specify a base filename for msixmgr tracing.
 #$tracefile = "C:\msixtrace"

 # ---------------------------- Only make changes above this line ----------------------------------------------------------------------

 Import-Module "C:\Program Files\WindowsPowerShell\Modules\PassiveInstall\PassiveInstall.dll"
 Approve-PassiveElevation -AsAdmin
 $executingScriptDirectory = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent

 # Function to estimate the uncompressed size needed for an uncompressed image of the MSIX file
 # We generally get between 2-to-1 and 2.5-to-1 compression ratios so assuming 3-to-1 as a worst case expectation.
 # If you get an error that the VHD/CIM wasn't big enough, you might update this.
 function EstimateSizeUncompressedInMB($MSIXpackage)
 {
     $len = (Get-item $MSIXpackage).Length
     $len = ($len * 3) / 1mb
     $iguess = [Math]::Ceiling($len) 
     if ($iguess -lt 100) {
         $iguess = 100
     }
     return $iguess
 }
 
 # Function to convert one package to Cim
 function Convert2CIM($MSIXpackage, $OutputPath, $size)
 {
     $startT = (Get-Date)
     $ArgList = "-Unpack -packagePath " + $MSIXpackage + " -destination " + $OutputPath + " -applyacls -create -vhdSize " + $size + " -filetype CIM -rootDirectory apps "
     Install-PassiveInstallFile -Installer "$($MsixMgrFolder)\msixmgr.exe" -Arguments $ArgList
     $endT = (Get-Date)
     $deltaT = $endT - $StartT
     write-output "Elapsed" $deltaT.ToString()
 }

 # Function to convert one package to Vhd
 function Convert2VHD($MSIXpackage, $OutputPath, $size)
 {
     $startT = (Get-Date)
     $ArgList = "-Unpack -packagePath " + $MSIXpackage + " -destination " + $OutputPath + " -applyacls -create -vhdSize " + $size + " -filetype VHD -rootDirectory apps "
     Install-PassiveInstallFile -Installer "$($MsixMgrFolder)\msixmgr.exe" -Arguments $ArgList
     $endT = (Get-Date)
     $deltaT = $endT - $StartT
     write-output "Elapsed" $deltaT.ToString()
 }

 Write-Output ""
 Write-Output ""
 Write-Output ""
 Write-Output ""
 Write-Output ""
 Write-Output ""
 
 # Create output folders
 Write-Output "Cleanup previous runs and create new output folder(s)."
 if ($SaveLocationVHD -ne "") {
     if ( (test-path "$($SaveLocationVHD)" ) -eq $true) {
         Remove-PassiveFolders -Folders "$($SaveLocationVHD)" 
     } 
     New-Item -Force -Type Directory "$($SaveLocationVHD)"
 }
 if ($SaveLocationCIM -ne "") {
     if ( (test-path "$($SaveLocationCIM)" ) -eq $true) {
         Remove-PassiveFolders -Folders "$($SaveLocationCIM)"
     }
     New-Item -Force -Type Directory "$($SaveLocationCIM)"
 }
 
 # Define  log files
 $ConversionMasterLogFileVHD = ([System.IO.Path]::Combine($SaveLocationVHD, "Log.txt"))
 $ConversionMasterLogFileCIM = ([System.IO.Path]::Combine($SaveLocationCIM, "Log.txt"))
 cd $MsixMgrFolder
 if ($tracefile -ne "") {
     Write-Output "Setting up trace"
     rm "$($tracefile)*"
     logman create trace MsixTrace -p "{db5b779e-2dcf-41bc-ab0e-40a6e02f1438}" -o "$($tracefile).etl"
     logman start MsixTrace
     Write-Output "trace running…"
 }
 
 $lastT = (Get-Date)
 if ($SaveLocationVHD -ne "") {
     Write-Output "Start: " $lastT >> "$($ConversionMasterLogFileVHD)" 
 }
 if ($SaveLocationCIM -ne "") {
     Write-Output "Start: " $lastT >> "$($ConversionMasterLogFileCIM)" 
 }

 Write-Output " "

 Get-ChildItem $MSIXContentStore | foreach-object {
     $MSIXpackage = $_.FullName
     if ($MSIXpackage -like "*.msix") {     
         write-host "Processing " $MSIXpackage -ForegroundColor Cyan
         $size = EstimateSizeUncompressedInMB $MSIXpackage
         $PkgNameShort = $_.BaseName
         $inx = $PkgNameShort.IndexOf('_')
         if ($inx -gt 0) {
            $PkgNameShort = $_.BaseName.SubString(0,$inx)
         }
         if ($SaveLocationVHD -ne "") {
             $OutputLocation =  ([System.IO.Path]::Combine($SaveLocationVHD, $PkgNameShort))
             if ( (test-path "$($OutputLocation)" ) -eq $false) {
                 New-PassiveFolderIfNotPresent "$($OutputLocation)"
             }
             $OutputLocation = ([System.IO.Path]::Combine($OutputLocation, "$($PkgNameShort).vhd"))
             $err = Convert2Vhd $MSIXpackage $OutputLocation $size *>&1
             Write-Output $err
             Write-Output $err >> "$($ConversionMasterLogFileVHD)"
         }
         if ($SaveLocationCIM -ne "") {
             $OutputLocation =  ([System.IO.Path]::Combine($SaveLocationCIM, $PkgNameShort))
             if ( (test-path "$($OutputLocation)" ) -eq $false) {
                 New-PassiveFolderIfNotPresent "$($OutputLocation)"
             }
             $OutputLocation = ([System.IO.Path]::Combine($OutputLocation, "$($PkgNameShort).cim"))
             $err = Convert2Cim $MSIXpackage $OutputLocation $size *>&1
             Write-Output $err
             Write-Output $err >> "$($ConversionMasterLogFileCIM)"
         }
         Write-Output " "  }
 }

 if ($SaveLocationVHD -ne "") {
     Write-Output "Logging of this VHD conversion session written to $($ConversionMasterLogFileVHD)" -ForegroundColor Cyan
 }
 if ($SaveLocationCIM -ne "") {
     Write-Output "Logging of this CIM conversion session written to $($ConversionMasterLogFileCIM)" -ForegroundColor Cyan
 }

 if ($tracefile -ne "") {
     write-Output "stopping trace"
     logman stop MsixTrace
     tracerpt.exe "$($tracefile)*.etl" -lr -o "$($tracefile).xml" -of XML
     write-Output "Stopping trace and saving to $($tracefile).xml"
 }
 
 write-Output -ForegroundColor "Green"  "Done."
 Show-PassiveTimer 600000 "End of script, Ctrl-C to end now or wait for timer."
 Start-Sleep 60

About “RootDirectory”

One oddity for both VHD and CIM is the use of a “rootDirectory” setting used as part of the script. This setting has nothing to do with how the MSIX package was created; it is not related to any captured install folder nor anything internal to the MSIX file. This is a folder that will be used inside the VHD/CIM image. MSIX App Attach for both formats require that a single folder exist at the root of the image; this single folder will be used as part of the mounting process, and your containerized file system will use a junction point from the folder where your app would normally be (i.e. “C:\Program Files\WindowsApps\packagefolder”) to this folder, such that all files will look like they are exactly where they would have been if you had just installed the MSIX file. It seems it does not matter what you call this rootDirectory, and all packages can have the same name since they mount in different locations. Thus, (for now) we are just using the name “apps” for each one.

Running the script

Notice the requirements listed at the top of the script file ( PassiveInstall and  msixmgr) . Modify the variables at the top as needed. Then run the script.

The script can be run and will self-elevate via UAC, as elevation is required. So you can just right-click on the file and “Run with PowerShell”. You may also run it in an elevated PowerShell process and it won’t re-prompt. and will above create a folder for each package type (VHD/CIM) as requested in the variables at the top. In each of those folders will be a log file for the conversion efforts on that type.

Under that will be folders for each of the packages. In the case of the VHD image, you will find a single VHD file. In the case of the CIM image, you will find a set of files. One of these files will have a .cim file type extension and it will be small (typically 1k). This is the correct form and you will need the entire folder.

Enjoy!

By Tim Mangan

Tim is a Microsoft MVP, and a Citrix CTP Fellow. He is an expert in App-V and MSIX.