Public/New-MSIXAppAttachImage.ps1
|
function New-MSIXAppAttachImage { <# .SYNOPSIS Unpacks an MSIX package into a VHD, VHDX or CIM App Attach image. .DESCRIPTION Uses msixmgr.exe to unpack an MSIX package into a disk image suitable for Azure Virtual Desktop App Attach. Three parameter sets are available: Create (default) msixmgr creates a fixed-size VHD, VHDX or CIM image. Output path is specified explicitly via -VhdPath. ExistingDisk An empty, pre-formatted dynamic VHD or VHDX is provided via -DiskImage (e.g. from New-MSIXDynamicAppAttachDisk). The function mounts it, unpacks the MSIX and dismounts it. CIM is not supported here. OutputFolder Pipeline-friendly. One or more MSIX files are piped in. The image name is derived from each MSIX file name and written to -OutputFolder. Returns a FileInfo object for each created image. .PARAMETER MsixFile Path to the source MSIX package. Accepts pipeline input in all parameter sets. .PARAMETER VhdPath (Create) Full path of the image file to create, including file name and extension (.vhd, .vhdx or .cim). .PARAMETER FileType (Create, OutputFolder) Image format: VHD, VHDX or CIM. Defaults to VHDX. For CIM the -SizeMB parameter is ignored — msixmgr determines the size. .PARAMETER SizeMB (Create, OutputFolder, VHD/VHDX only) Disk size in megabytes. Defaults to 0 (auto-calculate from uncompressed MSIX content plus 20 % overhead, min 50 MB). .PARAMETER DiskImage (ExistingDisk) Path to a pre-created, empty, formatted VHD or VHDX produced by New-MSIXDynamicAppAttachDisk. Pre-created disks can grow dynamically. .PARAMETER OutputFolder (OutputFolder) Folder where the images are written. The file name is derived from the MSIX base name plus the -FileType extension. .PARAMETER AppFolderName Name of the root folder inside the image. Defaults to the MSIX file name without extension. .EXAMPLE New-MSIXAppAttachImage -MsixFile "C:\Pkg\MyApp.msix" -VhdPath "C:\VHD\MyApp.vhdx" Creates MyApp.vhdx (fixed-size, auto-calculated) with msixmgr. .EXAMPLE New-MSIXAppAttachImage -MsixFile "C:\Pkg\MyApp.msix" -VhdPath "C:\VHD\MyApp.vhd" ` -FileType VHD -SizeMB 500 Creates a fixed-size VHD of 500 MB. .EXAMPLE New-MSIXAppAttachImage -MsixFile "C:\Pkg\MyApp.msix" -VhdPath "C:\VHD\MyApp.cim" ` -FileType CIM Creates a CIM read-only image directly via msixmgr. .EXAMPLE $disk = New-MSIXDynamicAppAttachDisk -Path "C:\VHD\MyApp" -SizeMB 500 New-MSIXAppAttachImage -MsixFile "C:\Pkg\MyApp.msix" -DiskImage $disk Creates a dynamic VHDX first, then unpacks the MSIX into it. .EXAMPLE Get-ChildItem "C:\Packages\*.msix" | New-MSIXAppAttachImage -OutputFolder "C:\VHD" -FileType VHDX Converts all MSIX packages in a folder to VHDX images in one pipeline. .EXAMPLE Get-ChildItem "C:\Packages\*.msix" | ForEach-Object { $disk = New-MSIXDynamicAppAttachDisk -MsixFile $_ -OutputFolder "C:\VHD" -SizeMB 500 New-MSIXAppAttachImage -MsixFile $_ -DiskImage $disk } Creates a dynamic VHDX per package (via Hyper-V) and then unpacks each MSIX into its disk. Use this when dynamic images are required instead of fixed-size images. .NOTES Requires msixmgr.exe. Run Update-MSIXForcelets if it is missing. ExistingDisk mode requires elevation (Administrator) for Mount-DiskImage. msixmgr source: https://github.com/microsoft/msix-packaging Andreas Nick, 2024 #> [CmdletBinding(DefaultParameterSetName = 'Create')] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0, ParameterSetName = 'Create')] [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0, ParameterSetName = 'ExistingDisk')] [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0, ParameterSetName = 'OutputFolder')] [System.IO.FileInfo] $MsixFile, # --- Create --- [Parameter(Mandatory = $true, Position = 1, ParameterSetName = 'Create')] [string] $VhdPath, [Parameter(ParameterSetName = 'Create')] [Parameter(ParameterSetName = 'OutputFolder')] [ValidateSet('VHD', 'VHDX', 'CIM')] [string] $FileType = 'VHDX', [Parameter(ParameterSetName = 'Create')] [Parameter(ParameterSetName = 'OutputFolder')] [ValidateRange(0, [int]::MaxValue)] [int] $SizeMB = 0, # --- ExistingDisk --- [Parameter(Mandatory = $true, Position = 1, ParameterSetName = 'ExistingDisk')] [System.IO.FileInfo] $DiskImage, # --- OutputFolder --- [Parameter(Mandatory = $true, ParameterSetName = 'OutputFolder')] [string] $OutputFolder, # --- Shared --- [Parameter(ParameterSetName = 'Create')] [Parameter(ParameterSetName = 'ExistingDisk')] [Parameter(ParameterSetName = 'OutputFolder')] [string] $AppFolderName ) begin { $msixmgrExe = $Script:MSIXMgrPath if ([string]::IsNullOrEmpty($msixmgrExe) -or -not (Test-Path $msixmgrExe)) { throw "msixmgr.exe not found. Run Update-MSIXForcelets to download MSIX Core." } if ($PSCmdlet.ParameterSetName -eq 'OutputFolder' -and -not (Test-Path $OutputFolder)) { New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null } } process { if (-not (Test-Path $MsixFile.FullName)) { Write-Error "MSIX file not found: $($MsixFile.FullName)" return } $effectiveAppFolder = if ([string]::IsNullOrEmpty($AppFolderName)) { [System.IO.Path]::GetFileNameWithoutExtension($MsixFile.Name) } else { $AppFolderName } switch ($PSCmdlet.ParameterSetName) { 'Create' { Invoke-MSIXCreateImage -MsixFile $MsixFile -ImagePath $VhdPath ` -FileType $FileType -SizeMB $SizeMB -AppFolderName $effectiveAppFolder ` -MsixmgrExe $msixmgrExe } 'OutputFolder' { $imagePath = Join-Path $OutputFolder "$($MsixFile.BaseName).$($FileType.ToLower())" Invoke-MSIXCreateImage -MsixFile $MsixFile -ImagePath $imagePath ` -FileType $FileType -SizeMB $SizeMB -AppFolderName $effectiveAppFolder ` -MsixmgrExe $msixmgrExe } 'ExistingDisk' { if (-not (Test-Path $DiskImage.FullName)) { Write-Error "Disk image not found: $($DiskImage.FullName)" return } Write-Verbose "Mounting disk image: $($DiskImage.FullName)" $image = $null try { $image = Mount-DiskImage -ImagePath $DiskImage.FullName -PassThru $driveLetter = ($image | Get-Disk | Get-Partition | Where-Object { $_.DriveLetter } | Select-Object -First 1).DriveLetter if ([string]::IsNullOrEmpty($driveLetter)) { throw "Could not determine drive letter for mounted disk image '$($DiskImage.FullName)'." } Write-Verbose "Disk image mounted at $($driveLetter):" Write-Verbose "App folder inside image: $effectiveAppFolder" & $msixmgrExe -Unpack ` -packagePath $MsixFile.FullName ` -destination "$($driveLetter):\" ` -applyacls ` -rootDirectory $effectiveAppFolder if ($LASTEXITCODE -ne 0) { throw "msixmgr.exe exited with code $LASTEXITCODE" } } catch { Write-Error "Failed to unpack MSIX into existing disk image: $_" throw } finally { if ($null -ne $image) { Write-Verbose "Dismounting disk image..." Dismount-DiskImage -ImagePath $DiskImage.FullName | Out-Null } } Write-Host "App Attach disk populated: $($DiskImage.FullName)" -ForegroundColor Green $DiskImage } } } } function Invoke-MSIXCreateImage { # Internal helper — creates a VHD, VHDX or CIM image via msixmgr.exe. param( [System.IO.FileInfo] $MsixFile, [string] $ImagePath, [string] $FileType, [int] $SizeMB, [string] $AppFolderName, [string] $MsixmgrExe ) $imageDir = Split-Path $ImagePath -Parent if (-not [string]::IsNullOrEmpty($imageDir) -and -not (Test-Path $imageDir)) { New-Item -Path $imageDir -ItemType Directory -Force | Out-Null } if ($FileType -eq 'CIM') { Write-Verbose "Creating CIM image: $ImagePath" try { & $MsixmgrExe -Unpack ` -packagePath $MsixFile.FullName ` -destination $ImagePath ` -applyacls ` -create ` -filetype cim ` -rootDirectory $AppFolderName if ($LASTEXITCODE -ne 0) { throw "msixmgr.exe exited with code $LASTEXITCODE" } } catch { Write-Error "Failed to create CIM image '$ImagePath': $_" throw } Write-Host "App Attach CIM created: $ImagePath" -ForegroundColor Green } else { # Auto-calculate size if not provided. if ($SizeMB -eq 0) { Write-Verbose "Calculating disk size from MSIX content..." $zip = [System.IO.Compression.ZipFile]::OpenRead($MsixFile.FullName) try { $uncompressedBytes = ($zip.Entries | Measure-Object -Property Length -Sum).Sum } finally { $zip.Dispose() } $SizeMB = [Math]::Max(50, [int][Math]::Ceiling($uncompressedBytes / 1MB * 1.2)) Write-Verbose ("Uncompressed: {0:N1} MB +20% overhead -> {1} MB" -f ($uncompressedBytes / 1MB), $SizeMB) } Write-Verbose "Creating $FileType ($SizeMB MB): $ImagePath" try { & $MsixmgrExe -Unpack ` -packagePath $MsixFile.FullName ` -destination $ImagePath ` -applyacls ` -create ` -vhdsize $SizeMB ` -filetype $FileType.ToLower() ` -rootDirectory $AppFolderName if ($LASTEXITCODE -ne 0) { throw "msixmgr.exe exited with code $LASTEXITCODE" } } catch { Write-Error "Failed to create $FileType image '$ImagePath': $_" throw } Write-Host "App Attach $FileType created: $ImagePath ($SizeMB MB)" -ForegroundColor Green } Get-Item $ImagePath } |