Public/New-MSIXDynamicAppAttachDisk.ps1
|
function New-MSIXDynamicAppAttachDisk { <# .SYNOPSIS Creates an empty, formatted, dynamic VHD or VHDX disk image for App Attach. .DESCRIPTION Creates a dynamically expanding VHD or VHDX file using New-VHD from the Hyper-V PowerShell module, initialises it, creates a primary NTFS partition and returns the unmounted disk image ready to be filled by New-MSIXAppAttachImage -DiskImage. Two parameter sets are available: SinglePath (default) Provide the full output path explicitly via -Path. OutputFolder Pipe one or more MSIX files. The disk image name is derived from each MSIX file name and written to -OutputFolder. The volume label defaults to the MSIX base name. The Hyper-V hypervisor role does not need to be active. Installing the Hyper-V Management Tools feature is sufficient. CIM images are not supported here — use New-MSIXAppAttachImage -FileType CIM to create a CIM image directly via msixmgr.exe. Returns a FileInfo object for each created disk image. .PARAMETER Path (SinglePath) Full path of the disk image to create, without extension. The correct extension (.vhd or .vhdx) is appended based on -FileType. .PARAMETER MsixFile (OutputFolder) One or more MSIX packages piped in. The disk image is named after each package and written to -OutputFolder. .PARAMETER OutputFolder (OutputFolder) Folder where the disk images are created. .PARAMETER SizeMB Maximum disk size in megabytes for the dynamic image. .PARAMETER FileType Disk image format: VHD or VHDX. Defaults to VHDX. .PARAMETER Label NTFS volume label. In OutputFolder mode defaults to the MSIX base name. In SinglePath mode defaults to "AppAttach". .EXAMPLE New-MSIXDynamicAppAttachDisk -Path "C:\VHD\MyApp" -SizeMB 500 Creates C:\VHD\MyApp.vhdx (dynamic, up to 500 MB). .EXAMPLE New-MSIXDynamicAppAttachDisk -Path "C:\VHD\MyApp" -SizeMB 500 -FileType VHD Creates C:\VHD\MyApp.vhd (dynamic, up to 500 MB). .EXAMPLE Get-ChildItem "C:\Packages\*.msix" | New-MSIXDynamicAppAttachDisk -OutputFolder "C:\VHD" -SizeMB 500 Creates one VHDX per MSIX package, named after the package. .NOTES Requires elevation (Administrator). Requires the Hyper-V Management Tools (PowerShell module): Windows 10/11: Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Management-PowerShell Windows Server: Install-WindowsFeature -Name Hyper-V-PowerShell Andreas Nick, 2024 #> [CmdletBinding(DefaultParameterSetName = 'SinglePath')] param( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'SinglePath')] [string] $Path, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'OutputFolder')] [System.IO.FileInfo] $MsixFile, [Parameter(Mandatory = $true, ParameterSetName = 'OutputFolder')] [string] $OutputFolder, [Parameter(Mandatory = $true)] [ValidateRange(10, [int]::MaxValue)] [int] $SizeMB, [ValidateSet('VHD', 'VHDX')] [string] $FileType = 'VHDX', [string] $Label ) begin { $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { throw "New-MSIXDynamicAppAttachDisk must be run as Administrator." } if (-not (Get-Module -ListAvailable -Name Hyper-V)) { throw "The Hyper-V PowerShell module is not available. " + "Install the Hyper-V Management Tools: " + "Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Management-PowerShell" } if ($PSCmdlet.ParameterSetName -eq 'OutputFolder' -and -not (Test-Path $OutputFolder)) { New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null } } process { # Resolve output path and label for this item. if ($PSCmdlet.ParameterSetName -eq 'OutputFolder') { if (-not (Test-Path $MsixFile.FullName)) { Write-Error "MSIX file not found: $($MsixFile.FullName)" return } $baseName = $MsixFile.BaseName $fullPath = Join-Path $OutputFolder "$baseName.$($FileType.ToLower())" $effectiveLabel = if ([string]::IsNullOrEmpty($Label)) { $baseName } else { $Label } } else { $basePath = [System.IO.Path]::ChangeExtension($Path, $null).TrimEnd('.') $fullPath = "$basePath.$($FileType.ToLower())" $effectiveLabel = if ([string]::IsNullOrEmpty($Label)) { 'AppAttach' } else { $Label } } if (Test-Path $fullPath) { Write-Error "Disk image already exists at '$fullPath'. Remove it first or choose a different path." return } $diskDir = Split-Path $fullPath -Parent if (-not [string]::IsNullOrEmpty($diskDir) -and -not (Test-Path $diskDir)) { New-Item -Path $diskDir -ItemType Directory -Force | Out-Null } Write-Verbose "Creating dynamic $FileType via New-VHD: $fullPath ($SizeMB MB)" New-VHD -Path $fullPath -SizeBytes ($SizeMB * 1MB) -Dynamic | Out-Null # Mount, initialise, partition, format, dismount. Write-Verbose "Mounting disk image for formatting..." $image = $null try { $image = Mount-DiskImage -ImagePath $fullPath -PassThru $disk = $image | Get-Disk # Safety checks — abort if the disk looks like anything other than our # freshly mounted, empty virtual disk. if ($disk.IsBoot -or $disk.IsSystem) { throw "Safety check failed: disk $($disk.Number) is flagged as boot or system disk. Aborting to prevent data loss." } if ($disk.IsReadOnly) { throw "Safety check failed: disk $($disk.Number) is read-only." } if ($disk.PartitionStyle -ne 'RAW') { throw "Safety check failed: disk $($disk.Number) already has a partition table ($($disk.PartitionStyle)). Expected a blank disk." } if ($disk.Location -and $disk.Location -ne $fullPath) { throw "Safety check failed: disk location '$($disk.Location)' does not match expected path '$fullPath'." } $partition = Initialize-Disk -Number $disk.Number -PartitionStyle MBR -PassThru | New-Partition -UseMaximumSize -AssignDriveLetter if ($partition.DiskNumber -ne $disk.Number) { throw "Safety check failed: partition disk number ($($partition.DiskNumber)) does not match expected disk ($($disk.Number))." } $systemDriveLetter = $env:SystemDrive.TrimEnd(':') if ($partition.DriveLetter -eq $systemDriveLetter) { throw "Safety check failed: assigned drive letter '$($partition.DriveLetter):' is the system drive. Aborting." } $volume = Get-Volume -Partition $partition -ErrorAction SilentlyContinue if ($volume -and $volume.FileSystemType -ne 'Unknown' -and $volume.FileSystemType -ne 'RAW') { throw "Safety check failed: partition already has file system '$($volume.FileSystemType)'. Expected unformatted." } Write-Verbose "Formatting drive $($partition.DriveLetter): on disk $($disk.Number) ($([Math]::Round($partition.Size / 1MB)) MB) label '$effectiveLabel'" Format-Volume -DriveLetter $partition.DriveLetter ` -FileSystem NTFS ` -NewFileSystemLabel $effectiveLabel ` -Confirm:$false | Out-Null } catch { Write-Error "Failed to initialise or format disk image '$fullPath': $_" throw } finally { if ($null -ne $image) { Dismount-DiskImage -ImagePath $fullPath | Out-Null } } Write-Host "Dynamic $FileType created: $fullPath ($SizeMB MB)" -ForegroundColor Green Get-Item $fullPath } } |