From db952355f33ff9e8b1a02c624d91e21be73f7901 Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:06:24 -0700 Subject: [PATCH] Add unified FFU disk layout controls Consolidate disk size, required partition drive letters, Recovery, and data partition settings into the Hyper-V Disk Layout UI. Add capacity validation, Fill Remaining handling, Recovery remove/restore, data partition reorder/remove controls, and persist CreateRecoveryPartition through config and VHDX cache checks. Update FFU optimization so automatic partition selection targets the Fill Remaining partition, falling back to Windows when none is selected. Refresh sample config and docs for the new Disk Layout parameters. --- FFUDevelopment/BuildFFUVM.ps1 | 116 +++- FFUDevelopment/BuildFFUVM_UI.ps1 | 9 +- FFUDevelopment/BuildFFUVM_UI.xaml | 159 ++--- .../FFUUI.Core/FFUUI.Core.Config.psm1 | 563 +++++++++++++++++- .../FFUUI.Core/FFUUI.Core.Handlers.psm1 | 114 +++- .../FFUUI.Core/FFUUI.Core.Initialize.psm1 | 72 ++- .../FFUUI.Core/FFUUI.Core.Shared.psm1 | 27 +- FFUDevelopment/config/Sample_default.json | Bin 7558 -> 7634 bytes docs/hyperv_settings.md | 28 +- docs/parameters_reference.md | 17 +- 10 files changed, 923 insertions(+), 182 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index f6d69bb..12d72cf 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -111,6 +111,9 @@ Fixed size of the Windows partition in bytes. Required when AdditionalDataPartit .PARAMETER RecoveryPartitionSize Optional fixed size of the Recovery partition in bytes. Leave as 0 to calculate the Recovery partition size from winre.wim plus buffer space. +.PARAMETER CreateRecoveryPartition +When set to $false, skips creating the Windows Recovery partition. Default is $true. + .PARAMETER AdditionalDataPartitions Optional data partitions to create after the Recovery partition. Each item supports Name, Label, DriveLetter, SizeBytes or SizeGB, FillRemaining, and FileSystem. @@ -187,7 +190,7 @@ Path to a custom Office configuration XML file to use for installation. When set to $true, will optimize the FFU file. Default is $true. .PARAMETER OptimizeFFUPartitionNumber -Optional partition number to pass to DISM /Optimize-FFU /PartitionNumber. Leave as 0 to optimize with DISM defaults. +Optional partition number to pass to DISM /Optimize-FFU /PartitionNumber. Leave as 0 to optimize the FillRemaining partition, or Windows when no partition uses FillRemaining. .PARAMETER OptionalFeatures Provide a semicolon-separated list of Windows optional features you want to include in the FFU (e.g., netfx3;TFTP). @@ -366,6 +369,7 @@ param( [uint64]$Disksize = 50GB, [uint64]$OSPartitionSize = 0, [uint64]$RecoveryPartitionSize = 0, + [bool]$CreateRecoveryPartition = $true, [object[]]$AdditionalDataPartitions = @(), [int]$Processors = 4, [bool]$EnableVMNetworking, @@ -898,6 +902,7 @@ class VhdxCacheItem { [string]$VhdxFileName = "" [uint32]$LogicalSectorSizeBytes = "" [uint64]$Disksize = "" + [bool]$CreateRecoveryPartition = $true [string]$SystemPartitionDriveLetter = "" [string]$WindowsPartitionDriveLetter = "" [string]$RecoveryPartitionDriveLetter = "" @@ -3089,14 +3094,17 @@ function Get-NormalizedPartitionDriveLetters { [string]$SystemPartitionDriveLetter, [string]$WindowsPartitionDriveLetter, [string]$RecoveryPartitionDriveLetter, + [bool]$CreateRecoveryPartition = $true, [object[]]$AdditionalDataPartitions = @(), [switch]$ValidateAvailable ) $requestedLetters = [ordered]@{ - SystemPartitionDriveLetter = $SystemPartitionDriveLetter - WindowsPartitionDriveLetter = $WindowsPartitionDriveLetter - RecoveryPartitionDriveLetter = $RecoveryPartitionDriveLetter + SystemPartitionDriveLetter = $SystemPartitionDriveLetter + WindowsPartitionDriveLetter = $WindowsPartitionDriveLetter + } + if ($CreateRecoveryPartition) { + $requestedLetters['RecoveryPartitionDriveLetter'] = $RecoveryPartitionDriveLetter } foreach ($dataPartition in @($AdditionalDataPartitions)) { if ($null -eq $dataPartition) { continue } @@ -3123,7 +3131,7 @@ function Get-NormalizedPartitionDriveLetters { $duplicateLetters = @($normalizedLetters.Values | Group-Object | Where-Object { $_.Count -gt 1 }) if ($duplicateLetters.Count -gt 0) { $duplicateLetterList = ($duplicateLetters | ForEach-Object { $_.Name }) -join ', ' - throw "System, Windows, and Recovery partition drive letters must be unique. Duplicate value(s): $duplicateLetterList." + throw "Build partition drive letters must be unique. Duplicate value(s): $duplicateLetterList." } if ($ValidateAvailable) { @@ -3229,6 +3237,7 @@ function Get-PartitionLayoutSignature { param( [uint64]$OSPartitionSize, [uint64]$RecoveryPartitionSize, + [bool]$CreateRecoveryPartition = $true, [object[]]$DataPartitions = @() ) @@ -3236,7 +3245,7 @@ function Get-PartitionLayoutSignature { "$($_.Name)|$($_.Label)|$($_.DriveLetter)|$($_.FileSystem)|$($_.SizeBytes)|$($_.FillRemaining)" }) - return "OS=$OSPartitionSize;Recovery=$RecoveryPartitionSize;Data=$($dataPartitionSignatures -join ';')" + return "OS=$OSPartitionSize;CreateRecovery=$CreateRecoveryPartition;Recovery=$RecoveryPartitionSize;Data=$($dataPartitionSignatures -join ';')" } function Get-PartitionDriveLetterCacheValue { param( @@ -3451,6 +3460,56 @@ function Get-WindowsPartitionFromDisk { return $basicDataPartitions | Select-Object -First 1 } +function Get-FFUOptimizePartitionNumber { + param( + [Parameter(Mandatory = $true)] + [ciminstance]$Disk, + [int]$RequestedPartitionNumber = 0, + [string]$WindowsPartitionDriveLetter, + [object[]]$AdditionalDataPartitions = @() + ) + + if ($RequestedPartitionNumber -gt 0) { + return $RequestedPartitionNumber + } + + $fillRemainingDataPartition = $null + foreach ($dataPartition in @($AdditionalDataPartitions)) { + if ($null -eq $dataPartition) { continue } + + $fillRemaining = $false + if ($dataPartition.PSObject.Properties.Name -contains 'FillRemaining') { + $fillRemaining = [System.Convert]::ToBoolean($dataPartition.FillRemaining) + } + + if ($fillRemaining) { + $fillRemainingDataPartition = $dataPartition + break + } + } + if ($null -ne $fillRemainingDataPartition) { + $driveLetter = ([string]$fillRemainingDataPartition.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant() + if ([string]::IsNullOrWhiteSpace($driveLetter)) { + throw "FillRemaining data partition '$($fillRemainingDataPartition.Name)' does not have a drive letter for FFU optimization." + } + + $dataPartition = $Disk | Get-Partition | Where-Object { ([string]$_.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant() -eq $driveLetter } | Select-Object -First 1 + if ($null -eq $dataPartition) { + throw "Unable to resolve FillRemaining data partition '$($fillRemainingDataPartition.Name)' at drive ${driveLetter}: for FFU optimization." + } + + WriteLog "Using FillRemaining data partition '$($fillRemainingDataPartition.Name)' (partition number $($dataPartition.PartitionNumber)) for FFU optimization." + return [int]$dataPartition.PartitionNumber + } + + $windowsPartition = Get-WindowsPartitionFromDisk -Disk $Disk -DriveLetter $WindowsPartitionDriveLetter + if ($null -eq $windowsPartition) { + throw 'Unable to resolve Windows partition for FFU optimization.' + } + + WriteLog "Using Windows partition (partition number $($windowsPartition.PartitionNumber)) for FFU optimization." + return [int]$windowsPartition.PartitionNumber +} #Add boot files function Add-BootFiles { param( @@ -4400,6 +4459,10 @@ function New-FFUFileName { function New-FFU { $captureContext = Get-CaptureVhdContext -VhdxPath $VHDXPath $captureDisk = $captureContext.Disk + $resolvedFFUOptimizePartitionNumber = 0 + if ($Optimize -eq $true) { + $resolvedFFUOptimizePartitionNumber = Get-FFUOptimizePartitionNumber -Disk $captureDisk -RequestedPartitionNumber $OptimizeFFUPartitionNumber -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -AdditionalDataPartitions $normalizedAdditionalDataPartitions + } $ffuCaptureNamingInfo = Get-FFUCaptureNamingInfo -ShortenedWindowsSKU $shortenedWindowsSKU -WindowsRelease $WindowsRelease -WindowsVersion $WindowsVersion -InstallationType $installationType -IsWindows10LtscClient:$isWindows10LtscClient try { @@ -4461,9 +4524,9 @@ function New-FFU { Set-Progress -Percentage 85 -Message "Optimizing FFU..." WriteLog 'Optimizing FFU - This will take a few minutes, please be patient' #Need to use ADK version of DISM to address bug in DISM - perhaps Windows 11 24H2 will fix this - $optimizePartitionArgument = if ($OptimizeFFUPartitionNumber -gt 0) { " /PartitionNumber:$OptimizeFFUPartitionNumber" } else { '' } + $optimizePartitionArgument = if ($resolvedFFUOptimizePartitionNumber -gt 0) { " /PartitionNumber:$resolvedFFUOptimizePartitionNumber" } else { '' } if (-not [string]::IsNullOrWhiteSpace($optimizePartitionArgument)) { - WriteLog "Optimizing FFU with DISM partition number $OptimizeFFUPartitionNumber." + WriteLog "Optimizing FFU with DISM partition number $resolvedFFUOptimizePartitionNumber." } Invoke-Process cmd "/c ""$DandIEnv"" && dism /optimize-ffu /imagefile:$FFUFile$optimizePartitionArgument" | Out-Null #Invoke-Process cmd "/c dism /optimize-ffu /imagefile:$FFUFile" | Out-Null @@ -6177,16 +6240,25 @@ Set-Progress -Percentage 2 -Message "Validating parameters..." try { $normalizedAdditionalDataPartitions = ConvertTo-NormalizedDataPartitions -DataPartitions $AdditionalDataPartitions if ($normalizedAdditionalDataPartitions.Count -gt 0 -and $OSPartitionSize -le 0) { - throw 'OSPartitionSize must be set when AdditionalDataPartitions are configured so space remains for Recovery and data partitions.' + $osPartitionSizeMessage = if ($CreateRecoveryPartition) { + 'OSPartitionSize must be set when AdditionalDataPartitions are configured so space remains for Recovery and data partitions.' + } + else { + 'OSPartitionSize must be set when AdditionalDataPartitions are configured so space remains for data partitions.' + } + throw $osPartitionSizeMessage } - $partitionLayoutSignature = Get-PartitionLayoutSignature -OSPartitionSize $OSPartitionSize -RecoveryPartitionSize $RecoveryPartitionSize -DataPartitions $normalizedAdditionalDataPartitions + $partitionLayoutSignature = Get-PartitionLayoutSignature -OSPartitionSize $OSPartitionSize -RecoveryPartitionSize $RecoveryPartitionSize -CreateRecoveryPartition $CreateRecoveryPartition -DataPartitions $normalizedAdditionalDataPartitions - $partitionDriveLetters = Get-NormalizedPartitionDriveLetters -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -AdditionalDataPartitions $normalizedAdditionalDataPartitions -ValidateAvailable + $partitionDriveLetters = Get-NormalizedPartitionDriveLetters -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $normalizedAdditionalDataPartitions -ValidateAvailable $SystemPartitionDriveLetter = $partitionDriveLetters.SystemPartitionDriveLetter $WindowsPartitionDriveLetter = $partitionDriveLetters.WindowsPartitionDriveLetter - $RecoveryPartitionDriveLetter = $partitionDriveLetters.RecoveryPartitionDriveLetter + if ($CreateRecoveryPartition) { + $RecoveryPartitionDriveLetter = $partitionDriveLetters.RecoveryPartitionDriveLetter + } $dataPartitionDriveLetterLog = if ($normalizedAdditionalDataPartitions.Count -gt 0) { ', Data=' + (($normalizedAdditionalDataPartitions | ForEach-Object { "$($_.Name):$($_.DriveLetter)" }) -join ', ') } else { '' } - WriteLog "Using build partition drive letters: System=$SystemPartitionDriveLetter, Windows=$WindowsPartitionDriveLetter, Recovery=$RecoveryPartitionDriveLetter$dataPartitionDriveLetterLog" + $recoveryPartitionDriveLetterLog = if ($CreateRecoveryPartition) { ", Recovery=$RecoveryPartitionDriveLetter" } else { ', Recovery=disabled' } + WriteLog "Using build partition drive letters: System=$SystemPartitionDriveLetter, Windows=$WindowsPartitionDriveLetter$recoveryPartitionDriveLetterLog$dataPartitionDriveLetterLog" } catch { $partitionDriveLetterValidationError = "Build validation failed: $($_.Exception.Message)" @@ -7486,15 +7558,19 @@ try { [uint64]$cachedDisksize = 0 if (-not [uint64]::TryParse([string]$vhdxCacheItem.Disksize, [ref]$cachedDisksize)) { WriteLog "Disksize invalid in cached config ($($vhdxCacheItem.Disksize)), continuing"; continue } if ($cachedDisksize -ne $Disksize) { WriteLog "Disksize mismatch (cached: $cachedDisksize, current: $Disksize), continuing"; continue } + if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'CreateRecoveryPartition') { WriteLog 'CreateRecoveryPartition missing in cached config, continuing'; continue } + if ([bool]$vhdxCacheItem.CreateRecoveryPartition -ne $CreateRecoveryPartition) { WriteLog "CreateRecoveryPartition mismatch (cached: $($vhdxCacheItem.CreateRecoveryPartition), current: $CreateRecoveryPartition), continuing"; continue } if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'SystemPartitionDriveLetter') { WriteLog 'SystemPartitionDriveLetter missing in cached config, continuing'; continue } if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'WindowsPartitionDriveLetter') { WriteLog 'WindowsPartitionDriveLetter missing in cached config, continuing'; continue } - if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'RecoveryPartitionDriveLetter') { WriteLog 'RecoveryPartitionDriveLetter missing in cached config, continuing'; continue } + if ($CreateRecoveryPartition -and $vhdxCacheItem.PSObject.Properties.Name -notcontains 'RecoveryPartitionDriveLetter') { WriteLog 'RecoveryPartitionDriveLetter missing in cached config, continuing'; continue } $cachedSystemPartitionDriveLetter = Get-PartitionDriveLetterCacheValue -DriveLetterValue $vhdxCacheItem.SystemPartitionDriveLetter $cachedWindowsPartitionDriveLetter = Get-PartitionDriveLetterCacheValue -DriveLetterValue $vhdxCacheItem.WindowsPartitionDriveLetter - $cachedRecoveryPartitionDriveLetter = Get-PartitionDriveLetterCacheValue -DriveLetterValue $vhdxCacheItem.RecoveryPartitionDriveLetter if ($cachedSystemPartitionDriveLetter -ne $SystemPartitionDriveLetter) { WriteLog "SystemPartitionDriveLetter mismatch (cached: $($vhdxCacheItem.SystemPartitionDriveLetter), current: $SystemPartitionDriveLetter), continuing"; continue } if ($cachedWindowsPartitionDriveLetter -ne $WindowsPartitionDriveLetter) { WriteLog "WindowsPartitionDriveLetter mismatch (cached: $($vhdxCacheItem.WindowsPartitionDriveLetter), current: $WindowsPartitionDriveLetter), continuing"; continue } - if ($cachedRecoveryPartitionDriveLetter -ne $RecoveryPartitionDriveLetter) { WriteLog "RecoveryPartitionDriveLetter mismatch (cached: $($vhdxCacheItem.RecoveryPartitionDriveLetter), current: $RecoveryPartitionDriveLetter), continuing"; continue } + if ($CreateRecoveryPartition) { + $cachedRecoveryPartitionDriveLetter = Get-PartitionDriveLetterCacheValue -DriveLetterValue $vhdxCacheItem.RecoveryPartitionDriveLetter + if ($cachedRecoveryPartitionDriveLetter -ne $RecoveryPartitionDriveLetter) { WriteLog "RecoveryPartitionDriveLetter mismatch (cached: $($vhdxCacheItem.RecoveryPartitionDriveLetter), current: $RecoveryPartitionDriveLetter), continuing"; continue } + } if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'PartitionLayoutSignature') { WriteLog 'PartitionLayoutSignature missing in cached config, continuing'; continue } if ($vhdxCacheItem.PartitionLayoutSignature -ne $partitionLayoutSignature) { WriteLog 'PartitionLayoutSignature mismatch, continuing'; continue } @@ -7823,7 +7899,12 @@ try { $osPartitionDriveLetter = $osPartition[1].DriveLetter $WindowsPartition = $osPartitionDriveLetter + ':\' - $recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DriveLetter $RecoveryPartitionDriveLetter -OsPartitionUsesMaximumSize ($OSPartitionSize -le 0) + if ($CreateRecoveryPartition) { + $recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DriveLetter $RecoveryPartitionDriveLetter -OsPartitionUsesMaximumSize ($OSPartitionSize -le 0) + } + else { + WriteLog 'CreateRecoveryPartition is false. Skipping Windows Recovery partition creation.' + } foreach ($additionalDataPartition in $normalizedAdditionalDataPartitions) { New-DataPartition -VhdxDisk $vhdxDisk -DataPartition $additionalDataPartition | Out-Null @@ -8002,6 +8083,7 @@ try { $cachedVHDXInfo.VhdxFileName = $("$VMName.vhdx") $cachedVHDXInfo.LogicalSectorSizeBytes = $LogicalSectorSizeBytes $cachedVHDXInfo.Disksize = $Disksize + $cachedVHDXInfo.CreateRecoveryPartition = $CreateRecoveryPartition $cachedVHDXInfo.SystemPartitionDriveLetter = [string]$SystemPartitionDriveLetter $cachedVHDXInfo.WindowsPartitionDriveLetter = [string]$WindowsPartitionDriveLetter $cachedVHDXInfo.RecoveryPartitionDriveLetter = [string]$RecoveryPartitionDriveLetter diff --git a/FFUDevelopment/BuildFFUVM_UI.ps1 b/FFUDevelopment/BuildFFUVM_UI.ps1 index 7487e39..69cfb3e 100644 --- a/FFUDevelopment/BuildFFUVM_UI.ps1 +++ b/FFUDevelopment/BuildFFUVM_UI.ps1 @@ -48,7 +48,8 @@ $script:uiState = [PSCustomObject]@{ pollTimer = $null; currentBuildProcess = $null; lastConfigFilePath = $null; - loadedDeviceNamingMode = $null + loadedDeviceNamingMode = $null; + createRecoveryPartition = $true }; Flags = @{ installAppsForcedByUpdates = $false; @@ -426,6 +427,12 @@ $script:uiState.Controls.btnRun.Add_Click({ # Gather config on the UI thread before starting the job $config = Get-UIConfig -State $script:uiState + if (-not (Test-DiskLayoutConfiguration -State $script:uiState -Config $config)) { + $btnRun.IsEnabled = $true + $script:uiState.Controls.txtStatus.Text = "Build canceled: disk layout validation failed." + return + } + # Validate Additional FFU selection if enabled if ($config.BuildUSBDrive -and $config.CopyAdditionalFFUFiles -and (($null -eq $config.AdditionalFFUFiles) -or ($config.AdditionalFFUFiles.Count -eq 0))) { [System.Windows.MessageBox]::Show("Please select at least one additional FFU file to copy, or uncheck 'Copy Additional FFU Files'.", "Selection Required", "OK", "Warning") | Out-Null diff --git a/FFUDevelopment/BuildFFUVM_UI.xaml b/FFUDevelopment/BuildFFUVM_UI.xaml index 1353a0f..efdc2d2 100644 --- a/FFUDevelopment/BuildFFUVM_UI.xaml +++ b/FFUDevelopment/BuildFFUVM_UI.xaml @@ -324,9 +324,6 @@ - - - @@ -339,110 +336,35 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + + - - + + + + + + + + + + + + + + + - - + + + @@ -453,6 +375,7 @@ + @@ -478,24 +401,28 @@ +