diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1
index 2d9267d..5704935 100644
--- a/FFUDevelopment/BuildFFUVM.ps1
+++ b/FFUDevelopment/BuildFFUVM.ps1
@@ -147,6 +147,15 @@ Path to the Windows 10/11 ISO file.
.PARAMETER LogicalSectorSizeBytes
UInt32 value of 512 or 4096. Useful for 4Kn drives or devices shipping with UFS drives. Default is 512.
+.PARAMETER SystemPartitionDriveLetter
+Drive letter used for the System partition while building the FFU VHDX. Default is S.
+
+.PARAMETER WindowsPartitionDriveLetter
+Drive letter used for the Windows partition while building the FFU VHDX. Default is W.
+
+.PARAMETER RecoveryPartitionDriveLetter
+Drive letter used for the Recovery partition while building the FFU VHDX. Default is R.
+
.PARAMETER Make
Make of the device to download drivers. Accepted values are: 'Microsoft', 'Dell', 'HP', 'Lenovo'.
@@ -411,6 +420,9 @@ param(
[string]$MediaType = 'consumer',
[ValidateSet(512, 4096)]
[uint32]$LogicalSectorSizeBytes = 512,
+ [string]$SystemPartitionDriveLetter = 'S',
+ [string]$WindowsPartitionDriveLetter = 'W',
+ [string]$RecoveryPartitionDriveLetter = 'R',
[bool]$Optimize = $true,
[string]$DriversJsonPath,
[bool]$CompressDownloadedDriversToWim = $false,
@@ -866,6 +878,9 @@ class VhdxCacheItem {
[string]$VhdxFileName = ""
[uint32]$LogicalSectorSizeBytes = ""
[uint64]$Disksize = ""
+ [string]$SystemPartitionDriveLetter = ""
+ [string]$WindowsPartitionDriveLetter = ""
+ [string]$RecoveryPartitionDriveLetter = ""
[string]$WindowsSKU = ""
[string]$WindowsRelease = ""
[string]$WindowsVersion = ""
@@ -3048,21 +3063,80 @@ function New-ScratchVhdx {
Writelog "Done."
return $toReturn
}
+function Get-NormalizedPartitionDriveLetters {
+ param(
+ [string]$SystemPartitionDriveLetter,
+ [string]$WindowsPartitionDriveLetter,
+ [string]$RecoveryPartitionDriveLetter,
+ [switch]$ValidateAvailable
+ )
+
+ $requestedLetters = [ordered]@{
+ SystemPartitionDriveLetter = $SystemPartitionDriveLetter
+ WindowsPartitionDriveLetter = $WindowsPartitionDriveLetter
+ RecoveryPartitionDriveLetter = $RecoveryPartitionDriveLetter
+ }
+ $normalizedLetters = [ordered]@{}
+
+ foreach ($entry in $requestedLetters.GetEnumerator()) {
+ $driveLetter = ([string]$entry.Value).Trim().TrimEnd(':').ToUpperInvariant()
+ if ([string]::IsNullOrWhiteSpace($driveLetter) -or $driveLetter -notmatch '^[A-Z]$') {
+ throw "$($entry.Key) must be a single drive letter from A to Z without a colon."
+ }
+
+ $normalizedLetters[$entry.Key] = $driveLetter
+ }
+
+ $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."
+ }
+
+ if ($ValidateAvailable) {
+ foreach ($entry in $normalizedLetters.GetEnumerator()) {
+ $existingDrive = Get-PSDrive -Name $entry.Value -PSProvider FileSystem -ErrorAction SilentlyContinue
+ if ($null -ne $existingDrive) {
+ throw "$($entry.Key) uses drive letter $($entry.Value), but that letter is already assigned on this host. Choose an unused drive letter."
+ }
+ }
+ }
+
+ return [pscustomobject]$normalizedLetters
+}
+function Get-PartitionDriveLetterCacheValue {
+ param(
+ [object]$DriveLetterValue
+ )
+
+ $driveLetter = ([string]$DriveLetterValue).Trim().TrimEnd(':').ToUpperInvariant()
+ if ($driveLetter -match '^[A-Z]$') {
+ return $driveLetter
+ }
+
+ $trailingDriveLetter = [regex]::Match($driveLetter, '(?i)(?:^|[^A-Z])([A-Z])$')
+ if ($trailingDriveLetter.Success) {
+ return $trailingDriveLetter.Groups[1].Value.ToUpperInvariant()
+ }
+
+ return $driveLetter
+}
#Add System Partition
function New-SystemPartition {
param(
[Parameter(Mandatory = $true)]
[ciminstance]$VhdxDisk,
+ [string]$DriveLetter = 'S',
[uint64]$SystemPartitionSize = 260MB
)
WriteLog "Creating System partition..."
- $sysPartition = $VhdxDisk | New-Partition -DriveLetter 'S' -Size $SystemPartitionSize -GptType "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" -IsHidden
- $sysPartition | Format-Volume -FileSystem FAT32 -Force -NewFileSystemLabel "System"
+ $sysPartition = $VhdxDisk | New-Partition -DriveLetter $DriveLetter -Size $SystemPartitionSize -GptType "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" -IsHidden
+ $sysPartition | Format-Volume -FileSystem FAT32 -Force -NewFileSystemLabel "System" | Out-Null
WriteLog 'Done.'
- return $sysPartition.DriveLetter
+ return [string]$sysPartition.DriveLetter
}
#Add MSRPartition
function New-MSRPartition {
@@ -3088,16 +3162,17 @@ function New-OSPartition {
[Parameter(Mandatory = $true)]
[string]$WimPath,
[uint32]$WimIndex,
+ [string]$DriveLetter = 'W',
[uint64]$OSPartitionSize = 0
)
WriteLog "Creating OS partition..."
if ($OSPartitionSize -gt 0) {
- $osPartition = $vhdxDisk | New-Partition -DriveLetter 'W' -Size $OSPartitionSize -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}"
+ $osPartition = $vhdxDisk | New-Partition -DriveLetter $DriveLetter -Size $OSPartitionSize -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}"
}
else {
- $osPartition = $vhdxDisk | New-Partition -DriveLetter 'W' -UseMaximumSize -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}"
+ $osPartition = $vhdxDisk | New-Partition -DriveLetter $DriveLetter -UseMaximumSize -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}"
}
$osPartition | Format-Volume -FileSystem NTFS -Confirm:$false -Force -NewFileSystemLabel "Windows"
@@ -3129,6 +3204,7 @@ function New-RecoveryPartition {
[Parameter(Mandatory = $true)]
$OsPartition,
[uint64]$RecoveryPartitionSize = 0,
+ [string]$DriveLetter = 'R',
[ciminstance]$DataPartition
)
@@ -3163,7 +3239,7 @@ function New-RecoveryPartition {
WriteLog "OS partition shrunk by $calculatedRecoverySize bytes for Recovery partition."
}
- $recoveryPartition = $VhdxDisk | New-Partition -DriveLetter 'R' -UseMaximumSize -GptType "{de94bba4-06d1-4d40-a16a-bfd50179d6ac}" `
+ $recoveryPartition = $VhdxDisk | New-Partition -DriveLetter $DriveLetter -UseMaximumSize -GptType "{de94bba4-06d1-4d40-a16a-bfd50179d6ac}" `
| Format-Volume -FileSystem NTFS -Confirm:$false -Force -NewFileSystemLabel 'Recovery'
WriteLog "Done. Recovery partition at drive $($recoveryPartition.DriveLetter):"
@@ -5877,7 +5953,6 @@ if ($ExportConfigFile) {
####### End Generate Config File #######
-
#Setting long path support - this prevents issues where some applications have deep directory structures
#and oscdimg fails to create the Apps ISO
try {
@@ -5896,6 +5971,21 @@ if ($LongPathsEnabled -ne 1) {
Set-Progress -Percentage 2 -Message "Validating parameters..."
###PARAMETER VALIDATION
+#Set build partition drive letters and validate they are available for use; this is required before any build steps that require drive access to ensure the expected drive letters are reserved and to fail fast if there are conflicts.
+try {
+ $partitionDriveLetters = Get-NormalizedPartitionDriveLetters -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -ValidateAvailable
+ $SystemPartitionDriveLetter = $partitionDriveLetters.SystemPartitionDriveLetter
+ $WindowsPartitionDriveLetter = $partitionDriveLetters.WindowsPartitionDriveLetter
+ $RecoveryPartitionDriveLetter = $partitionDriveLetters.RecoveryPartitionDriveLetter
+ WriteLog "Using build partition drive letters: System=$SystemPartitionDriveLetter, Windows=$WindowsPartitionDriveLetter, Recovery=$RecoveryPartitionDriveLetter"
+}
+catch {
+ $partitionDriveLetterValidationError = "Build validation failed: $($_.Exception.Message)"
+ Set-Progress -Percentage 2 -Message $partitionDriveLetterValidationError
+ WriteLog $partitionDriveLetterValidationError
+ throw $partitionDriveLetterValidationError
+}
+
#Validate CopyDrivers dependency on BuildUSBDrive
if ($CopyDrivers -and (-not $BuildUSBDrive)) {
WriteLog "-CopyDrivers is set to `$true, but -BuildUSBDrive is not set to `$true"
@@ -7187,6 +7277,15 @@ 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 '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 }
+ $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 }
$cachedUpdateNames = @()
if ($vhdxCacheItem.IncludedUpdates -and $vhdxCacheItem.IncludedUpdates.Count -gt 0) {
@@ -7504,21 +7603,21 @@ try {
$vhdxDisk = New-ScratchVhdx -VhdxPath $VHDXPath -SizeBytes $disksize -LogicalSectorSizeBytes $LogicalSectorSizeBytes
- $systemPartitionDriveLetter = New-SystemPartition -VhdxDisk $vhdxDisk
+ $createdSystemPartitionDriveLetter = New-SystemPartition -VhdxDisk $vhdxDisk -DriveLetter $SystemPartitionDriveLetter
New-MSRPartition -VhdxDisk $vhdxDisk
Set-Progress -Percentage 16 -Message "Applying base Windows image to VHDX..."
- $osPartition = New-OSPartition -VhdxDisk $vhdxDisk -OSPartitionSize $OSPartitionSize -WimPath $WimPath -WimIndex $index
+ $osPartition = New-OSPartition -VhdxDisk $vhdxDisk -OSPartitionSize $OSPartitionSize -WimPath $WimPath -WimIndex $index -DriveLetter $WindowsPartitionDriveLetter
$osPartitionDriveLetter = $osPartition[1].DriveLetter
$WindowsPartition = $osPartitionDriveLetter + ':\'
#$recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DataPartition $dataPartition
- $recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DataPartition $dataPartition
+ $recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DriveLetter $RecoveryPartitionDriveLetter -DataPartition $dataPartition
WriteLog 'All necessary partitions created.'
- Add-BootFiles -OsPartitionDriveLetter $osPartitionDriveLetter -SystemPartitionDriveLetter $systemPartitionDriveLetter[1] -AdkPath $adkPath -WindowsArch $WindowsArch
+ Add-BootFiles -OsPartitionDriveLetter $osPartitionDriveLetter -SystemPartitionDriveLetter $createdSystemPartitionDriveLetter -AdkPath $adkPath -WindowsArch $WindowsArch
#Add Windows packages
if ($UpdateLatestCU -or $UpdateLatestNet -or $UpdatePreviewCU ) {
@@ -7689,6 +7788,9 @@ try {
$cachedVHDXInfo.VhdxFileName = $("$VMName.vhdx")
$cachedVHDXInfo.LogicalSectorSizeBytes = $LogicalSectorSizeBytes
$cachedVHDXInfo.Disksize = $Disksize
+ $cachedVHDXInfo.SystemPartitionDriveLetter = [string]$SystemPartitionDriveLetter
+ $cachedVHDXInfo.WindowsPartitionDriveLetter = [string]$WindowsPartitionDriveLetter
+ $cachedVHDXInfo.RecoveryPartitionDriveLetter = [string]$RecoveryPartitionDriveLetter
$cachedVHDXInfo.WindowsSKU = $WindowsSKU
$cachedVHDXInfo.WindowsRelease = $WindowsRelease
$cachedVHDXInfo.WindowsVersion = $WindowsVersion
diff --git a/FFUDevelopment/BuildFFUVM_UI.ps1 b/FFUDevelopment/BuildFFUVM_UI.ps1
index ac5de98..6a52864 100644
--- a/FFUDevelopment/BuildFFUVM_UI.ps1
+++ b/FFUDevelopment/BuildFFUVM_UI.ps1
@@ -338,7 +338,6 @@ $script:uiState.Controls.btnRun.Add_Click({
$script:uiState.Flags.isCleanupRunning = $true
$script:uiState.Data.pollTimer.Add_Tick({
- param($sender, $e)
$currentProcess = $script:uiState.Data.currentBuildProcess
# Read new lines from log
@@ -353,13 +352,13 @@ $script:uiState.Controls.btnRun.Add_Click({
}
if ($null -eq $currentProcess -or $null -eq $script:uiState.Data.pollTimer) {
- if ($null -ne $sender) { $sender.Stop() }
+ if ($null -ne $script:uiState.Data.pollTimer) { $script:uiState.Data.pollTimer.Stop() }
$script:uiState.Data.pollTimer = $null
return
}
if ($currentProcess.HasExited) {
- if ($null -ne $sender) { $sender.Stop() }
+ if ($null -ne $script:uiState.Data.pollTimer) { $script:uiState.Data.pollTimer.Stop() }
$script:uiState.Data.pollTimer = $null
if ($null -ne $script:uiState.Data.logStreamReader) {
@@ -621,7 +620,6 @@ $script:uiState.Controls.btnRun.Add_Click({
# Add the Tick event handler
$script:uiState.Data.pollTimer.Add_Tick({
- param($sender, $e)
# This scriptblock runs on the UI thread, so it can safely access script-scoped variables
$currentProcess = $script:uiState.Data.currentBuildProcess
@@ -649,8 +647,8 @@ $script:uiState.Controls.btnRun.Add_Click({
# If process is somehow null or the timer has been nulled out, stop the timer
if ($null -eq $currentProcess -or $null -eq $script:uiState.Data.pollTimer) {
- if ($null -ne $sender) {
- $sender.Stop()
+ if ($null -ne $script:uiState.Data.pollTimer) {
+ $script:uiState.Data.pollTimer.Stop()
}
$script:uiState.Data.pollTimer = $null
return
@@ -659,8 +657,8 @@ $script:uiState.Controls.btnRun.Add_Click({
# Check if the build process has exited
if ($currentProcess.HasExited) {
# Stop the timer, we're done polling
- if ($null -ne $sender) {
- $sender.Stop()
+ if ($null -ne $script:uiState.Data.pollTimer) {
+ $script:uiState.Data.pollTimer.Stop()
}
$script:uiState.Data.pollTimer = $null
@@ -698,9 +696,25 @@ $script:uiState.Controls.btnRun.Add_Click({
# Determine final status based on process exit code
$finalStatusText = "FFU build completed successfully."
if ($exitCode -ne 0) {
- $finalStatusText = "FFU build failed. Check FFUDevelopment.log for details."
+ $failureDetail = $null
+ if ($null -ne $script:uiState.Data.logData) {
+ for ($logIndex = $script:uiState.Data.logData.Count - 1; $logIndex -ge 0; $logIndex--) {
+ $logLine = [string]$script:uiState.Data.logData[$logIndex]
+ if ($logLine -match '(?i)(Build validation failed|Exception|ERROR|already assigned|must be a single drive letter|must be unique)') {
+ $failureDetail = $logLine
+ break
+ }
+ }
+ }
+
+ $finalStatusText = if ($failureDetail) { "FFU build failed. $failureDetail" } else { "FFU build failed. Check FFUDevelopment.log for details." }
WriteLog "BuildFFUVM.ps1 process failed with exit code: $exitCode"
- [System.Windows.MessageBox]::Show("The build process failed. Please check the $FFUDevelopmentPath\FFUDevelopment.log file for details.`n`nExit code: $exitCode", "Build Error", "OK", "Error") | Out-Null
+ $buildErrorMessage = "The build process failed. Please check the $FFUDevelopmentPath\FFUDevelopment.log file for details."
+ if ($failureDetail) {
+ $buildErrorMessage += "`n`n$failureDetail"
+ }
+ $buildErrorMessage += "`n`nExit code: $exitCode"
+ [System.Windows.MessageBox]::Show($buildErrorMessage, "Build Error", "OK", "Error") | Out-Null
$script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed'
}
else {
diff --git a/FFUDevelopment/BuildFFUVM_UI.xaml b/FFUDevelopment/BuildFFUVM_UI.xaml
index c668923..761270b 100644
--- a/FFUDevelopment/BuildFFUVM_UI.xaml
+++ b/FFUDevelopment/BuildFFUVM_UI.xaml
@@ -339,6 +339,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
index 90616a4..2c75d4e 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
@@ -62,6 +62,9 @@ function Get-UIConfig {
InstallWingetApps = $State.Controls.chkInstallWingetApps.IsChecked
ISOPath = $State.Controls.txtISOPath.Text
WindowsMediaSource = if ($null -ne $State.Controls.rbProvideISO -and $State.Controls.rbProvideISO.IsChecked) { "Provide Windows ISO" } else { "Download Windows ESD" }
+ SystemPartitionDriveLetter = $State.Controls.cmbSystemPartitionDriveLetter.SelectedItem.Content
+ WindowsPartitionDriveLetter = $State.Controls.cmbWindowsPartitionDriveLetter.SelectedItem.Content
+ RecoveryPartitionDriveLetter = $State.Controls.cmbRecoveryPartitionDriveLetter.SelectedItem.Content
LogicalSectorSizeBytes = [int]$State.Controls.cmbLogicalSectorSize.SelectedItem.Content
# Make = $null
MediaType = $State.Controls.cmbMediaType.SelectedItem
@@ -523,6 +526,9 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'txtProcessors' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Processors' -State $State
Set-UIValue -ControlName 'txtVMLocation' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'VMLocation' -State $State
Set-UIValue -ControlName 'txtVMNamePrefix' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'FFUPrefix' -State $State
+ Set-UIValue -ControlName 'cmbSystemPartitionDriveLetter' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'SystemPartitionDriveLetter' -TransformValue { param($val) ([string]$val).Trim().TrimEnd(':').ToUpperInvariant() } -State $State
+ Set-UIValue -ControlName 'cmbWindowsPartitionDriveLetter' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'WindowsPartitionDriveLetter' -TransformValue { param($val) ([string]$val).Trim().TrimEnd(':').ToUpperInvariant() } -State $State
+ Set-UIValue -ControlName 'cmbRecoveryPartitionDriveLetter' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'RecoveryPartitionDriveLetter' -TransformValue { param($val) ([string]$val).Trim().TrimEnd(':').ToUpperInvariant() } -State $State
Set-UIValue -ControlName 'cmbLogicalSectorSize' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'LogicalSectorSizeBytes' -TransformValue { param($val) $val.ToString() } -State $State
$State.Controls.spVMNetworkingSettings.IsEnabled = $true -eq $State.Controls.chkEnableVMNetworking.IsChecked
if (-not ($true -eq $State.Controls.chkEnableVMNetworking.IsChecked)) {
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
index ca8f396..73fdf74 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
@@ -255,6 +255,9 @@ function Initialize-UIControls {
$State.Controls.txtProcessors = $window.FindName('txtProcessors')
$State.Controls.txtVMLocation = $window.FindName('txtVMLocation')
$State.Controls.txtVMNamePrefix = $window.FindName('txtVMNamePrefix')
+ $State.Controls.cmbSystemPartitionDriveLetter = $window.FindName('cmbSystemPartitionDriveLetter')
+ $State.Controls.cmbWindowsPartitionDriveLetter = $window.FindName('cmbWindowsPartitionDriveLetter')
+ $State.Controls.cmbRecoveryPartitionDriveLetter = $window.FindName('cmbRecoveryPartitionDriveLetter')
$State.Controls.cmbLogicalSectorSize = $window.FindName('cmbLogicalSectorSize')
$State.Controls.txtProductKey = $window.FindName('txtProductKey')
$State.Controls.txtOfficePath = $window.FindName('txtOfficePath')
@@ -445,6 +448,9 @@ function Initialize-UIDefaults {
$State.Controls.txtProcessors.Text = $State.Defaults.generalDefaults.Processors
$State.Controls.txtVMLocation.Text = $State.Defaults.generalDefaults.VMLocation
$State.Controls.txtVMNamePrefix.Text = $State.Defaults.generalDefaults.VMNamePrefix
+ $State.Controls.cmbSystemPartitionDriveLetter.SelectedItem = ($State.Controls.cmbSystemPartitionDriveLetter.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.SystemPartitionDriveLetter })
+ $State.Controls.cmbWindowsPartitionDriveLetter.SelectedItem = ($State.Controls.cmbWindowsPartitionDriveLetter.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.WindowsPartitionDriveLetter })
+ $State.Controls.cmbRecoveryPartitionDriveLetter.SelectedItem = ($State.Controls.cmbRecoveryPartitionDriveLetter.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.RecoveryPartitionDriveLetter })
$State.Controls.cmbLogicalSectorSize.SelectedItem = ($State.Controls.cmbLogicalSectorSize.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.LogicalSectorSize.ToString() })
# Populate Windows Release, Version, and SKU comboboxes
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
index 5f41546..94178cf 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
@@ -159,6 +159,9 @@ function Get-GeneralDefaults {
Processors = 4
VMLocation = $vmLocationPath
VMNamePrefix = "_FFU"
+ SystemPartitionDriveLetter = 'S'
+ WindowsPartitionDriveLetter = 'W'
+ RecoveryPartitionDriveLetter = 'R'
LogicalSectorSize = 512
# Updates Tab Defaults
UpdateLatestCU = $true
diff --git a/FFUDevelopment/config/Sample_default.json b/FFUDevelopment/config/Sample_default.json
index 49192e4..a922b2c 100644
Binary files a/FFUDevelopment/config/Sample_default.json and b/FFUDevelopment/config/Sample_default.json differ
diff --git a/docs/appsscriptvariables.md b/docs/appsscriptvariables.md
index 79be4c1..2336c7c 100644
--- a/docs/appsscriptvariables.md
+++ b/docs/appsscriptvariables.md
@@ -96,9 +96,11 @@ s{
"Processors": 4,
"ProductKey": "",
"PromptExternalHardDiskMedia": true,
+ "RecoveryPartitionDriveLetter": "R",
"RemoveApps": false,
"RemoveFFU": false,
"RemoveUpdates": false,
+ "SystemPartitionDriveLetter": "S",
"Threads": 5,
"UpdateADK": true,
"UpdateEdge": true,
@@ -116,6 +118,7 @@ s{
"VMLocation": "C:\\FFUDevelopment\\VM",
"VMSwitchName": "External",
"WindowsArch": "x64",
+ "WindowsPartitionDriveLetter": "W",
"WindowsLang": "en-us",
"WindowsRelease": 11,
"WindowsSKU": "Pro",
diff --git a/docs/hyperv_settings.md b/docs/hyperv_settings.md
index bdc8b90..8ec6cb5 100644
--- a/docs/hyperv_settings.md
+++ b/docs/hyperv_settings.md
@@ -43,6 +43,20 @@ Default is `$FFUDevelopmentPath\VM`. This is the location of the VHDX that gets
Prefix for the generated VM. Default is _FFU.
+## System Partition Drive Letter
+
+Drive letter used for the System partition while building the FFU VHDX. Default is `S`.
+
+## Windows Partition Drive Letter
+
+Drive letter used for the Windows partition while building the FFU VHDX. Default is `W`.
+
+## Recovery Partition Drive Letter
+
+Drive letter used for the Recovery partition while building the FFU VHDX. Default is `R`.
+
+These settings only affect FFU creation. They do not change the hard-coded drive letters used by `ApplyFFU.ps1` during deployment.
+
## Logical Sector Size
Uint32 value of 512 or 4096. Useful for 4Kn drives or devices shipping with UFS drives. Default is 512.
diff --git a/docs/parameters_reference.md b/docs/parameters_reference.md
index 4ab0eed..e35dcc0 100644
--- a/docs/parameters_reference.md
+++ b/docs/parameters_reference.md
@@ -75,10 +75,12 @@ This table lists all top-level parameters in BuildFFUVM.ps1.
| -Processors | int | Processors | Number of virtual processors for the virtual machine. Recommended to use at least 4. |
| -ProductKey | string | Product Key | Product key for the Windows edition specified in WindowsSKU. This will overwrite whatever SKU is entered for WindowsSKU. Recommended to use if you want to use a MAK or KMS key to activate Enterprise or Education. If using VL media instead of consumer media, you'll want to enter a MAK or KMS key here. |
| -PromptExternalHardDiskMedia | bool | Prompt for External Hard Disk Media | When set to $true, will prompt the user to confirm the use of media identified as External Hard Disk media via WMI class Win32_DiskDrive. Default is $true. |
+| -RecoveryPartitionDriveLetter | string | Recovery Partition Drive Letter | Drive letter used for the Recovery partition while building the FFU VHDX. Default is R. |
| -RemoveApps | bool | Remove Apps Folder Content | When set to $true, will remove the application content in the Apps folder after the FFU has been captured. Default is $true. |
| -RemoveDownloadedESD | bool | Remove Downloaded ESD file(s) | When set to $true, downloaded Windows ESD files are automatically deleted after they have been applied. Default is $true. |
| -RemoveFFU | bool | Remove FFU | When set to $true, will remove the FFU file from the $FFUDevelopmentPath\FFU folder after it has been copied to the USB drive. Default is $false. |
| -RemoveUpdates | bool | Remove Downloaded Update Files | When set to $true, will remove the downloaded CU, MSRT, Defender, Edge, OneDrive, and .NET files downloaded. Default is $true. |
+| -SystemPartitionDriveLetter | string | System Partition Drive Letter | Drive letter used for the System partition while building the FFU VHDX. Default is S. |
| -Threads | int | Threads | Controls the throttle applied to parallel tasks inside the script. Default is 5, matching the UI Threads field, and applies to driver downloads invoked through Invoke-ParallelProcessing. |
| -UnattendArm64FilePath | string | arm64 Unattend File Path | Path to the arm64 unattend XML source file used by Copy Unattend.xml and Inject Unattend.xml. Default is $FFUDevelopmentPath\Unattend\unattend_arm64.xml. |
| -UnattendX64FilePath | string | x64 Unattend File Path | Path to the x64 unattend XML source file used by Copy Unattend.xml and Inject Unattend.xml. Default is $FFUDevelopmentPath\Unattend\unattend_x64.xml. |
@@ -98,6 +100,7 @@ This table lists all top-level parameters in BuildFFUVM.ps1.
| -VMLocation | string | VM Location | Default is $FFUDevelopmentPath\VM. This is the location of the VHDX that gets created where Windows will be installed to. |
| -VMSwitchName | string | VM Switch Name + Custom VM Switch Name (when Other selected) | Name of the Hyper-V virtual switch used when -EnableVMNetworking is set to $true. |
| -WindowsArch | string | Windows Architecture | String value of 'x86', 'x64', or 'arm64'. This is used to identify which architecture of Windows to download. Default is 'x64'. |
+| -WindowsPartitionDriveLetter | string | Windows Partition Drive Letter | Drive letter used for the Windows partition while building the FFU VHDX. Default is W. |
| -WindowsLang | string | Windows Language | String value in language-region format (e.g., 'en-us'). This is used to identify which language of media to download. Default is 'en-us'. |
| -WindowsRelease | int | Windows Release | Integer value of 10, 11, 2016, 2019, 2021, 2022, 2024, or 2025. This is used to identify which Windows client/LTSC/server release to use. Default is 11. |
| -WindowsSKU | string | Windows SKU | Edition/SKU to install. Accepted values are: 'Home', 'Home N', 'Home Single Language', 'Education', 'Education N', 'Pro', 'Pro N', 'Pro Education', 'Pro Education N', 'Pro for Workstations', 'Pro N for Workstations', 'Enterprise', 'Enterprise N', 'Enterprise 2016 LTSB', 'Enterprise N 2016 LTSB', 'Enterprise LTSC', 'Enterprise N LTSC', 'IoT Enterprise LTSC', 'IoT Enterprise N LTSC', 'Standard', 'Standard (Desktop Experience)', 'Datacenter', 'Datacenter (Desktop Experience)'. |