mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-08-12 21:53:05 -06:00
Enforce persistent FFU data-partition drive letters
Add partition-layout versioning, drive-letter configuration, deployment handling, and documentation for recovery and additional data partitions.
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ManifestPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('Audit', 'Specialize')]
|
||||
[string]$Phase
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$logPath = 'C:\Windows\Temp\FFUDataPartitionDriveLetters.log'
|
||||
$runtimeDirectory = Split-Path -Path $ManifestPath -Parent
|
||||
$successMarkerPath = Join-Path -Path $runtimeDirectory -ChildPath "$Phase.success"
|
||||
$failureMarkerPath = Join-Path -Path $runtimeDirectory -ChildPath "$Phase.failure"
|
||||
|
||||
function Write-FFUDataPartitionDriveLetterLog {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Message
|
||||
)
|
||||
|
||||
$logDirectory = Split-Path -Path $logPath -Parent
|
||||
if (-not (Test-Path -LiteralPath $logDirectory -PathType Container)) {
|
||||
New-Item -Path $logDirectory -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff'
|
||||
Add-Content -LiteralPath $logPath -Value "[$timestamp] [$Phase] $Message" -Encoding UTF8
|
||||
}
|
||||
|
||||
function ConvertTo-NormalizedPartitionGuid {
|
||||
param(
|
||||
[object]$Value
|
||||
)
|
||||
|
||||
return ([string]$Value).Trim().Trim('{', '}').ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Resolve-FFUDataPartition {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object]$ManifestEntry,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object[]]$DataPartitions
|
||||
)
|
||||
|
||||
$partition = $null
|
||||
$manifestGuid = ConvertTo-NormalizedPartitionGuid -Value $ManifestEntry.PartitionGuid
|
||||
if (-not [string]::IsNullOrWhiteSpace($manifestGuid)) {
|
||||
$guidMatches = @($DataPartitions | Where-Object { (ConvertTo-NormalizedPartitionGuid -Value $_.Guid) -eq $manifestGuid })
|
||||
if ($guidMatches.Count -gt 1) {
|
||||
throw "Partition '$($ManifestEntry.Name)' matched more than one GPT partition GUID."
|
||||
}
|
||||
if ($guidMatches.Count -eq 1) {
|
||||
$partition = $guidMatches[0]
|
||||
Write-FFUDataPartitionDriveLetterLog "Resolved '$($ManifestEntry.Name)' by GPT partition GUID."
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -eq $partition) {
|
||||
$dataOrdinal = [int]$ManifestEntry.DataOrdinal
|
||||
if ($dataOrdinal -lt 1 -or $dataOrdinal -gt $DataPartitions.Count) {
|
||||
throw "Partition '$($ManifestEntry.Name)' data ordinal $dataOrdinal is outside the applied disk layout."
|
||||
}
|
||||
$partition = $DataPartitions[$dataOrdinal - 1]
|
||||
Write-FFUDataPartitionDriveLetterLog "Resolved '$($ManifestEntry.Name)' by strict data partition order fallback."
|
||||
}
|
||||
|
||||
if ([int]$partition.PartitionNumber -ne [int]$ManifestEntry.PartitionNumber) {
|
||||
throw "Partition '$($ManifestEntry.Name)' partition number mismatch. Expected $($ManifestEntry.PartitionNumber), found $($partition.PartitionNumber)."
|
||||
}
|
||||
$allowSizeChange = $false
|
||||
if ($ManifestEntry.PSObject.Properties.Name -contains 'AllowSizeChange') {
|
||||
$allowSizeChange = [System.Convert]::ToBoolean($ManifestEntry.AllowSizeChange)
|
||||
}
|
||||
if (-not $allowSizeChange -and [uint64]$partition.Size -ne [uint64]$ManifestEntry.SizeBytes) {
|
||||
throw "Partition '$($ManifestEntry.Name)' size mismatch. Expected $($ManifestEntry.SizeBytes), found $($partition.Size)."
|
||||
}
|
||||
if ($allowSizeChange -and [uint64]$partition.Size -ne [uint64]$ManifestEntry.SizeBytes) {
|
||||
Write-FFUDataPartitionDriveLetterLog "Accepted optimized FFU size change for '$($ManifestEntry.Name)'. Captured $($ManifestEntry.SizeBytes), deployed $($partition.Size)."
|
||||
}
|
||||
|
||||
$volumes = @($partition | Get-Volume -ErrorAction SilentlyContinue)
|
||||
if ($volumes.Count -ne 1) {
|
||||
throw "Partition '$($ManifestEntry.Name)' does not resolve to exactly one volume."
|
||||
}
|
||||
$volume = $volumes[0]
|
||||
if (([string]$volume.FileSystemLabel).Trim() -ine ([string]$ManifestEntry.Label).Trim()) {
|
||||
throw "Partition '$($ManifestEntry.Name)' volume label mismatch. Expected '$($ManifestEntry.Label)', found '$($volume.FileSystemLabel)'."
|
||||
}
|
||||
if (([string]$volume.FileSystem).Trim() -ine ([string]$ManifestEntry.FileSystem).Trim()) {
|
||||
throw "Partition '$($ManifestEntry.Name)' file system mismatch. Expected '$($ManifestEntry.FileSystem)', found '$($volume.FileSystem)'."
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
ManifestEntry = $ManifestEntry
|
||||
Partition = $partition
|
||||
Volume = $volume
|
||||
}
|
||||
}
|
||||
|
||||
function Set-FFUDataPartitionDriveLetters {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object]$Manifest
|
||||
)
|
||||
|
||||
if ($env:SystemDrive -ine 'C:') {
|
||||
throw "Windows must be running from C:. Current SystemDrive is '$env:SystemDrive'."
|
||||
}
|
||||
|
||||
$windowsPartitions = @(Get-Partition -DriveLetter C -ErrorAction SilentlyContinue)
|
||||
if ($windowsPartitions.Count -ne 1) {
|
||||
throw "Unable to resolve exactly one Windows C: partition. Found $($windowsPartitions.Count)."
|
||||
}
|
||||
$windowsPartition = $windowsPartitions[0]
|
||||
$diskNumber = [int]$windowsPartition.DiskNumber
|
||||
$basicDataGptType = '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}'
|
||||
$dataPartitions = @(Get-Partition -DiskNumber $diskNumber -ErrorAction Stop |
|
||||
Where-Object { ([string]$_.GptType).ToLowerInvariant() -eq $basicDataGptType -and $_.PartitionNumber -ne $windowsPartition.PartitionNumber } |
|
||||
Sort-Object -Property PartitionNumber)
|
||||
|
||||
$resolvedEntries = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||
foreach ($manifestEntry in @($Manifest.Partitions)) {
|
||||
$requestedDriveLetter = ([string]$manifestEntry.RequestedDriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
|
||||
if ($requestedDriveLetter -notmatch '^[D-Z]$') {
|
||||
throw "Partition '$($manifestEntry.Name)' requests invalid drive letter '$requestedDriveLetter'."
|
||||
}
|
||||
$manifestEntry.RequestedDriveLetter = $requestedDriveLetter
|
||||
$resolvedEntries.Add((Resolve-FFUDataPartition -ManifestEntry $manifestEntry -DataPartitions $dataPartitions))
|
||||
}
|
||||
|
||||
foreach ($resolvedEntry in $resolvedEntries) {
|
||||
$requestedDriveLetter = [string]$resolvedEntry.ManifestEntry.RequestedDriveLetter
|
||||
$currentDriveLetter = ([string]$resolvedEntry.Partition.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
|
||||
if ($currentDriveLetter -eq $requestedDriveLetter) {
|
||||
continue
|
||||
}
|
||||
|
||||
$requestedVolumes = @(Get-Volume -DriveLetter $requestedDriveLetter -ErrorAction SilentlyContinue)
|
||||
if ($requestedVolumes.Count -gt 0) {
|
||||
$requestedOwner = @(Get-Partition -DriveLetter $requestedDriveLetter -ErrorAction SilentlyContinue | Select-Object -First 1)
|
||||
$ownerDescription = if ($requestedOwner.Count -gt 0) {
|
||||
"disk $($requestedOwner[0].DiskNumber), partition $($requestedOwner[0].PartitionNumber)"
|
||||
}
|
||||
else {
|
||||
"volume '$($requestedVolumes[0].FileSystemLabel)'"
|
||||
}
|
||||
throw "Cannot assign drive ${requestedDriveLetter}: to partition '$($resolvedEntry.ManifestEntry.Name)' because the letter is owned by $ownerDescription."
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($resolvedEntry in $resolvedEntries) {
|
||||
$requestedDriveLetter = [string]$resolvedEntry.ManifestEntry.RequestedDriveLetter
|
||||
$currentDriveLetter = ([string]$resolvedEntry.Partition.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
|
||||
if ($currentDriveLetter -ne $requestedDriveLetter) {
|
||||
$currentDriveLetterText = if ([string]::IsNullOrWhiteSpace($currentDriveLetter)) { 'no drive letter' } else { "drive ${currentDriveLetter}:" }
|
||||
Write-FFUDataPartitionDriveLetterLog "Assigning drive ${requestedDriveLetter}: to '$($resolvedEntry.ManifestEntry.Name)', currently $currentDriveLetterText."
|
||||
Set-Partition -DiskNumber $resolvedEntry.Partition.DiskNumber -PartitionNumber $resolvedEntry.Partition.PartitionNumber -NewDriveLetter $requestedDriveLetter -ErrorAction Stop
|
||||
}
|
||||
|
||||
$verifiedPartitions = @(Get-Partition -DiskNumber $resolvedEntry.Partition.DiskNumber -PartitionNumber $resolvedEntry.Partition.PartitionNumber -ErrorAction Stop)
|
||||
if ($verifiedPartitions.Count -ne 1 -or ([string]$verifiedPartitions[0].DriveLetter).Trim().TrimEnd(':').ToUpperInvariant() -ne $requestedDriveLetter) {
|
||||
throw "Drive letter verification failed for partition '$($resolvedEntry.ManifestEntry.Name)'."
|
||||
}
|
||||
Write-FFUDataPartitionDriveLetterLog "Verified '$($resolvedEntry.ManifestEntry.Name)' at drive ${requestedDriveLetter}:."
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-FFUDataPartitionDriveLetterArtifacts {
|
||||
if (-not (Test-Path -LiteralPath $runtimeDirectory -PathType Container)) {
|
||||
return
|
||||
}
|
||||
|
||||
Remove-Item -LiteralPath $runtimeDirectory -Recurse -Force -ErrorAction Stop
|
||||
}
|
||||
|
||||
try {
|
||||
Write-FFUDataPartitionDriveLetterLog 'Starting data partition drive-letter enforcement.'
|
||||
if (-not (Test-Path -LiteralPath $ManifestPath -PathType Leaf)) {
|
||||
throw "Drive-letter manifest was not found at '$ManifestPath'."
|
||||
}
|
||||
|
||||
$manifest = Get-Content -LiteralPath $ManifestPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
||||
if ([int]$manifest.SchemaVersion -ne 1) {
|
||||
throw "Unsupported drive-letter manifest schema version '$($manifest.SchemaVersion)'."
|
||||
}
|
||||
if (@($manifest.Partitions).Count -eq 0) {
|
||||
throw 'Drive-letter manifest does not contain any partitions.'
|
||||
}
|
||||
|
||||
Remove-Item -LiteralPath $successMarkerPath, $failureMarkerPath -Force -ErrorAction SilentlyContinue
|
||||
Set-FFUDataPartitionDriveLetters -Manifest $manifest
|
||||
|
||||
if ($Phase -eq 'Audit') {
|
||||
Set-Content -LiteralPath $successMarkerPath -Value (Get-Date -Format 'o') -Encoding ASCII -Force
|
||||
Write-FFUDataPartitionDriveLetterLog 'Audit enforcement completed successfully.'
|
||||
}
|
||||
else {
|
||||
Write-FFUDataPartitionDriveLetterLog 'Specialize enforcement completed successfully. Removing runtime artifacts.'
|
||||
Remove-FFUDataPartitionDriveLetterArtifacts
|
||||
Write-FFUDataPartitionDriveLetterLog 'Runtime artifact cleanup completed successfully.'
|
||||
}
|
||||
|
||||
exit 0
|
||||
}
|
||||
catch {
|
||||
$errorMessage = $_.Exception.Message
|
||||
Write-FFUDataPartitionDriveLetterLog "ERROR: $errorMessage"
|
||||
if (Test-Path -LiteralPath $runtimeDirectory -PathType Container) {
|
||||
Set-Content -LiteralPath $failureMarkerPath -Value $errorMessage -Encoding UTF8 -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
exit 3
|
||||
}
|
||||
@@ -5,6 +5,21 @@ Start-Transcript -Path $logPath -Append -Force | Out-Null
|
||||
|
||||
try {
|
||||
Write-Host 'Starting FFU orchestration bootstrap.'
|
||||
$driveLetterRuntimeDirectory = 'C:\Windows\Setup\Scripts\FFUDL'
|
||||
$driveLetterScriptPath = Join-Path -Path $driveLetterRuntimeDirectory -ChildPath 'Apply.ps1'
|
||||
$driveLetterManifestPath = Join-Path -Path $driveLetterRuntimeDirectory -ChildPath 'Manifest.json'
|
||||
if (Test-Path -LiteralPath $driveLetterScriptPath -PathType Leaf) {
|
||||
Write-Host 'Applying configured data partition drive letters before locating Apps media.'
|
||||
& 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File $driveLetterScriptPath -ManifestPath $driveLetterManifestPath -Phase Audit
|
||||
$driveLetterExitCode = $LASTEXITCODE
|
||||
if ($driveLetterExitCode -ne 0) {
|
||||
Write-Error "Data partition drive-letter enforcement failed with exit code $driveLetterExitCode. Shutting down the build VM."
|
||||
Stop-Computer -Force
|
||||
throw "Data partition drive-letter enforcement failed with exit code $driveLetterExitCode."
|
||||
}
|
||||
Write-Host 'Configured data partition drive letters are ready.'
|
||||
}
|
||||
|
||||
$deadline = (Get-Date).AddMinutes(10)
|
||||
$orchestratorPath = $null
|
||||
|
||||
|
||||
+520
-101
@@ -891,6 +891,8 @@ if ($WindowsRelease -notin 10, 11 -and -not $ISOPath) {
|
||||
}
|
||||
|
||||
#Class definition for vhdx cache
|
||||
$partitionLayoutSignatureVersion = 2
|
||||
|
||||
class VhdxCacheUpdateItem {
|
||||
[string]$Name
|
||||
VhdxCacheUpdateItem([string]$Name) {
|
||||
@@ -903,9 +905,7 @@ class VhdxCacheItem {
|
||||
[uint32]$LogicalSectorSizeBytes = ""
|
||||
[uint64]$Disksize = ""
|
||||
[bool]$CreateRecoveryPartition = $true
|
||||
[string]$SystemPartitionDriveLetter = ""
|
||||
[string]$WindowsPartitionDriveLetter = ""
|
||||
[string]$RecoveryPartitionDriveLetter = ""
|
||||
[uint32]$PartitionLayoutSignatureVersion = 0
|
||||
[string]$PartitionLayoutSignature = ""
|
||||
[string]$WindowsSKU = ""
|
||||
[string]$WindowsRelease = ""
|
||||
@@ -3179,8 +3179,8 @@ function ConvertTo-NormalizedDataPartitions {
|
||||
if ([string]::IsNullOrWhiteSpace($label)) { $label = $name }
|
||||
|
||||
$driveLetter = ([string]$dataPartition.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
|
||||
if ([string]::IsNullOrWhiteSpace($driveLetter) -or $driveLetter -notmatch '^[A-Z]$') {
|
||||
throw "Additional data partition '$name' must specify a single drive letter from A to Z without a colon."
|
||||
if ([string]::IsNullOrWhiteSpace($driveLetter) -or $driveLetter -notmatch '^[D-Z]$') {
|
||||
throw "Additional data partition '$name' must specify a single drive letter from D to Z without a colon."
|
||||
}
|
||||
|
||||
$fileSystem = [string]$dataPartition.FileSystem
|
||||
@@ -3203,17 +3203,23 @@ function ConvertTo-NormalizedDataPartitions {
|
||||
$fillRemaining = [System.Convert]::ToBoolean($dataPartition.FillRemaining)
|
||||
}
|
||||
|
||||
$persistDriveLetter = $false
|
||||
if ($dataPartition.PSObject.Properties.Name -contains 'PersistDriveLetter') {
|
||||
$persistDriveLetter = [System.Convert]::ToBoolean($dataPartition.PersistDriveLetter)
|
||||
}
|
||||
|
||||
if (($sizeBytes -eq 0) -and (-not $fillRemaining)) {
|
||||
throw "Additional data partition '$name' must specify SizeBytes, SizeGB, or FillRemaining."
|
||||
}
|
||||
|
||||
$normalizedPartitions.Add([pscustomobject]@{
|
||||
Name = $name
|
||||
Label = $label
|
||||
DriveLetter = $driveLetter
|
||||
FileSystem = $fileSystem
|
||||
SizeBytes = $sizeBytes
|
||||
FillRemaining = $fillRemaining
|
||||
Name = $name
|
||||
Label = $label
|
||||
DriveLetter = $driveLetter
|
||||
FileSystem = $fileSystem
|
||||
SizeBytes = $sizeBytes
|
||||
FillRemaining = $fillRemaining
|
||||
PersistDriveLetter = $persistDriveLetter
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3242,28 +3248,11 @@ function Get-PartitionLayoutSignature {
|
||||
)
|
||||
|
||||
$dataPartitionSignatures = @($DataPartitions | ForEach-Object {
|
||||
"$($_.Name)|$($_.Label)|$($_.DriveLetter)|$($_.FileSystem)|$($_.SizeBytes)|$($_.FillRemaining)"
|
||||
"$($_.Name)|$($_.Label)|$($_.FileSystem)|$($_.SizeBytes)|$($_.FillRemaining)"
|
||||
})
|
||||
|
||||
return "OS=$OSPartitionSize;CreateRecovery=$CreateRecoveryPartition;Recovery=$RecoveryPartitionSize;Data=$($dataPartitionSignatures -join ';')"
|
||||
}
|
||||
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(
|
||||
@@ -3486,38 +3475,423 @@ function New-DataPartition {
|
||||
|
||||
return $partition
|
||||
}
|
||||
function Get-WindowsPartitionFromDisk {
|
||||
function Resolve-VhdxPartitionLayout {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ciminstance]$Disk,
|
||||
[string]$DriveLetter
|
||||
[object]$Disk,
|
||||
[bool]$CreateRecoveryPartition = $true,
|
||||
[object[]]$AdditionalDataPartitions = @()
|
||||
)
|
||||
|
||||
$basicDataPartitions = @($Disk | Get-Partition | Where-Object { $_.GptType -eq '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' })
|
||||
$normalizedDriveLetter = ([string]$DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
|
||||
if (-not [string]::IsNullOrWhiteSpace($normalizedDriveLetter)) {
|
||||
$driveLetterPartition = $basicDataPartitions | Where-Object { ([string]$_.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant() -eq $normalizedDriveLetter } | Select-Object -First 1
|
||||
if ($null -ne $driveLetterPartition) {
|
||||
return $driveLetterPartition
|
||||
$systemGptType = '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}'
|
||||
$msrGptType = '{e3c9e316-0b5c-4db8-817d-f92df00215ae}'
|
||||
$basicDataGptType = '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}'
|
||||
$recoveryGptType = '{de94bba4-06d1-4d40-a16a-bfd50179d6ac}'
|
||||
$partitions = @($Disk | Get-Partition -ErrorAction Stop | Sort-Object -Property PartitionNumber)
|
||||
|
||||
$systemPartitions = @($partitions | Where-Object { ([string]$_.GptType).ToLowerInvariant() -eq $systemGptType })
|
||||
$msrPartitions = @($partitions | Where-Object { ([string]$_.GptType).ToLowerInvariant() -eq $msrGptType })
|
||||
$basicDataPartitions = @($partitions | Where-Object { ([string]$_.GptType).ToLowerInvariant() -eq $basicDataGptType })
|
||||
$recoveryPartitions = @($partitions | Where-Object { ([string]$_.GptType).ToLowerInvariant() -eq $recoveryGptType })
|
||||
|
||||
if ($systemPartitions.Count -ne 1) {
|
||||
throw "Expected one EFI System partition on VHDX disk $($Disk.Number), found $($systemPartitions.Count)."
|
||||
}
|
||||
if ($msrPartitions.Count -ne 1) {
|
||||
throw "Expected one Microsoft Reserved partition on VHDX disk $($Disk.Number), found $($msrPartitions.Count)."
|
||||
}
|
||||
if ($CreateRecoveryPartition -and $recoveryPartitions.Count -ne 1) {
|
||||
throw "Expected one Recovery partition on VHDX disk $($Disk.Number), found $($recoveryPartitions.Count)."
|
||||
}
|
||||
if (-not $CreateRecoveryPartition -and $recoveryPartitions.Count -ne 0) {
|
||||
throw "Expected no Recovery partition on VHDX disk $($Disk.Number), found $($recoveryPartitions.Count)."
|
||||
}
|
||||
|
||||
$expectedBasicDataPartitionCount = 1 + @($AdditionalDataPartitions).Count
|
||||
if ($basicDataPartitions.Count -ne $expectedBasicDataPartitionCount) {
|
||||
throw "Expected $expectedBasicDataPartitionCount basic data partition(s) on VHDX disk $($Disk.Number), found $($basicDataPartitions.Count)."
|
||||
}
|
||||
|
||||
$windowsPartition = $basicDataPartitions[0]
|
||||
$dataPartitions = @($basicDataPartitions | Select-Object -Skip 1)
|
||||
if ($systemPartitions[0].PartitionNumber -ge $msrPartitions[0].PartitionNumber -or $msrPartitions[0].PartitionNumber -ge $windowsPartition.PartitionNumber) {
|
||||
throw "VHDX disk $($Disk.Number) does not use the expected System, MSR, Windows partition order."
|
||||
}
|
||||
if ($CreateRecoveryPartition) {
|
||||
$firstDataPartitionNumber = if ($dataPartitions.Count -gt 0) { $dataPartitions[0].PartitionNumber } else { [int]::MaxValue }
|
||||
if ($recoveryPartitions[0].PartitionNumber -le $windowsPartition.PartitionNumber -or $recoveryPartitions[0].PartitionNumber -ge $firstDataPartitionNumber) {
|
||||
throw "VHDX disk $($Disk.Number) does not use the expected Windows, Recovery, data partition order."
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($partition in $basicDataPartitions) {
|
||||
if ([string]::IsNullOrWhiteSpace($partition.DriveLetter)) { continue }
|
||||
$volume = Get-Volume -DriveLetter $partition.DriveLetter -ErrorAction SilentlyContinue
|
||||
if ($null -ne $volume -and $volume.FileSystemLabel -eq 'Windows') {
|
||||
return $partition
|
||||
}
|
||||
$windowsVolumes = @($windowsPartition | Get-Volume -ErrorAction SilentlyContinue)
|
||||
if ($windowsVolumes.Count -ne 1 -or ([string]$windowsVolumes[0].FileSystemLabel).Trim() -ine 'Windows') {
|
||||
throw "Unable to verify the Windows volume on VHDX disk $($Disk.Number) partition $($windowsPartition.PartitionNumber)."
|
||||
}
|
||||
|
||||
return $basicDataPartitions | Select-Object -First 1
|
||||
$resolvedDataPartitions = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||
for ($dataPartitionIndex = 0; $dataPartitionIndex -lt @($AdditionalDataPartitions).Count; $dataPartitionIndex++) {
|
||||
$dataPartitionConfig = @($AdditionalDataPartitions)[$dataPartitionIndex]
|
||||
$dataPartition = $dataPartitions[$dataPartitionIndex]
|
||||
$dataVolumes = @($dataPartition | Get-Volume -ErrorAction SilentlyContinue)
|
||||
if ($dataVolumes.Count -ne 1) {
|
||||
throw "Unable to resolve data partition '$($dataPartitionConfig.Name)' on VHDX disk $($Disk.Number) partition $($dataPartition.PartitionNumber)."
|
||||
}
|
||||
if (([string]$dataVolumes[0].FileSystemLabel).Trim() -ine ([string]$dataPartitionConfig.Label).Trim()) {
|
||||
throw "Data partition '$($dataPartitionConfig.Name)' label mismatch on VHDX disk $($Disk.Number) partition $($dataPartition.PartitionNumber)."
|
||||
}
|
||||
if (([string]$dataVolumes[0].FileSystem).Trim() -ine ([string]$dataPartitionConfig.FileSystem).Trim()) {
|
||||
throw "Data partition '$($dataPartitionConfig.Name)' file system mismatch on VHDX disk $($Disk.Number) partition $($dataPartition.PartitionNumber)."
|
||||
}
|
||||
|
||||
$resolvedDataPartitions.Add([pscustomobject]@{
|
||||
Config = $dataPartitionConfig
|
||||
Partition = $dataPartition
|
||||
Volume = $dataVolumes[0]
|
||||
})
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
Disk = $Disk
|
||||
SystemPartition = $systemPartitions[0]
|
||||
MsrPartition = $msrPartitions[0]
|
||||
WindowsPartition = $windowsPartition
|
||||
WindowsVolume = $windowsVolumes[0]
|
||||
RecoveryPartition = if ($CreateRecoveryPartition) { $recoveryPartitions[0] } else { $null }
|
||||
DataPartitions = $resolvedDataPartitions.ToArray()
|
||||
}
|
||||
}
|
||||
|
||||
function Set-VhdxBuildPartitionDriveLetters {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object]$Layout,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SystemPartitionDriveLetter,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$WindowsPartitionDriveLetter,
|
||||
[string]$RecoveryPartitionDriveLetter,
|
||||
[bool]$CreateRecoveryPartition = $true,
|
||||
[object[]]$AdditionalDataPartitions = @()
|
||||
)
|
||||
|
||||
$assignments = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||
$assignments.Add([pscustomobject]@{ Name = 'System'; Partition = $Layout.SystemPartition; DriveLetter = $SystemPartitionDriveLetter })
|
||||
$assignments.Add([pscustomobject]@{ Name = 'Windows'; Partition = $Layout.WindowsPartition; DriveLetter = $WindowsPartitionDriveLetter })
|
||||
if ($CreateRecoveryPartition) {
|
||||
$assignments.Add([pscustomobject]@{ Name = 'Recovery'; Partition = $Layout.RecoveryPartition; DriveLetter = $RecoveryPartitionDriveLetter })
|
||||
}
|
||||
for ($dataPartitionIndex = 0; $dataPartitionIndex -lt @($AdditionalDataPartitions).Count; $dataPartitionIndex++) {
|
||||
$assignments.Add([pscustomobject]@{
|
||||
Name = "Data partition '$(@($AdditionalDataPartitions)[$dataPartitionIndex].Name)'"
|
||||
Partition = $Layout.DataPartitions[$dataPartitionIndex].Partition
|
||||
DriveLetter = @($AdditionalDataPartitions)[$dataPartitionIndex].DriveLetter
|
||||
})
|
||||
}
|
||||
|
||||
foreach ($assignment in $assignments) {
|
||||
$requestedDriveLetter = ([string]$assignment.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
|
||||
$existingPartitions = @(Get-Partition -DriveLetter $requestedDriveLetter -ErrorAction SilentlyContinue)
|
||||
$externalPartition = $existingPartitions | Where-Object { $_.DiskNumber -ne $Layout.Disk.Number } | Select-Object -First 1
|
||||
if ($null -ne $externalPartition) {
|
||||
$externalVolume = @($externalPartition | Get-Volume -ErrorAction SilentlyContinue | Select-Object -First 1)
|
||||
$externalVolumeLabel = if ($externalVolume.Count -gt 0 -and -not [string]::IsNullOrWhiteSpace([string]$externalVolume[0].FileSystemLabel)) { "'$($externalVolume[0].FileSystemLabel)'" } else { 'an unlabeled volume' }
|
||||
throw "Cannot assign drive ${requestedDriveLetter}: to $($assignment.Name) because it is owned by $externalVolumeLabel on disk $($externalPartition.DiskNumber), partition $($externalPartition.PartitionNumber)."
|
||||
}
|
||||
|
||||
$existingFileSystemDrive = Get-PSDrive -Name $requestedDriveLetter -PSProvider FileSystem -ErrorAction SilentlyContinue
|
||||
if ($null -ne $existingFileSystemDrive -and $existingPartitions.Count -eq 0) {
|
||||
throw "Cannot assign drive ${requestedDriveLetter}: to $($assignment.Name) because it is already mapped to '$($existingFileSystemDrive.Root)'."
|
||||
}
|
||||
}
|
||||
|
||||
# Remove target access paths first so swaps cannot collide with old VHDX assignments.
|
||||
foreach ($assignment in $assignments) {
|
||||
$currentDriveLetter = ([string]$assignment.Partition.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
|
||||
if ($currentDriveLetter -match '^[A-Z]$') {
|
||||
WriteLog "Removing drive ${currentDriveLetter}: from $($assignment.Name) on working VHDX disk $($Layout.Disk.Number)."
|
||||
Remove-PartitionAccessPath -DiskNumber $assignment.Partition.DiskNumber -PartitionNumber $assignment.Partition.PartitionNumber -AccessPath "${currentDriveLetter}:\" -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($assignment in $assignments) {
|
||||
$requestedDriveLetter = ([string]$assignment.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
|
||||
WriteLog "Assigning drive ${requestedDriveLetter}: to $($assignment.Name) on working VHDX disk $($Layout.Disk.Number)."
|
||||
Set-Partition -DiskNumber $assignment.Partition.DiskNumber -PartitionNumber $assignment.Partition.PartitionNumber -NewDriveLetter $requestedDriveLetter -ErrorAction Stop
|
||||
$verifiedPartition = Get-Partition -DiskNumber $assignment.Partition.DiskNumber -PartitionNumber $assignment.Partition.PartitionNumber -ErrorAction Stop
|
||||
if (([string]$verifiedPartition.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant() -ne $requestedDriveLetter) {
|
||||
throw "Drive letter verification failed for $($assignment.Name) on working VHDX disk $($Layout.Disk.Number)."
|
||||
}
|
||||
}
|
||||
|
||||
return Resolve-VhdxPartitionLayout -Disk $Layout.Disk -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $AdditionalDataPartitions
|
||||
}
|
||||
|
||||
function New-FFUDataPartitionDriveLetterManifest {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object]$Layout,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object[]]$AdditionalDataPartitions,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$WindowsArch,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ManifestPath,
|
||||
[bool]$Optimize = $false,
|
||||
[int]$OptimizeFFUPartitionNumber = 0
|
||||
)
|
||||
|
||||
$resizablePartitionNumber = 0
|
||||
if ($Optimize) {
|
||||
$resizablePartitionNumber = Get-FFUOptimizePartitionNumber -Layout $Layout -RequestedPartitionNumber $OptimizeFFUPartitionNumber -AdditionalDataPartitions $AdditionalDataPartitions
|
||||
}
|
||||
|
||||
$manifestPartitions = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||
for ($dataPartitionIndex = 0; $dataPartitionIndex -lt $AdditionalDataPartitions.Count; $dataPartitionIndex++) {
|
||||
$dataPartitionConfig = $AdditionalDataPartitions[$dataPartitionIndex]
|
||||
if (-not [bool]$dataPartitionConfig.PersistDriveLetter) {
|
||||
continue
|
||||
}
|
||||
|
||||
$resolvedDataPartition = $Layout.DataPartitions[$dataPartitionIndex]
|
||||
$partitionGuid = [string]$resolvedDataPartition.Partition.Guid
|
||||
if ([string]::IsNullOrWhiteSpace($partitionGuid)) {
|
||||
WriteLog "GPT partition GUID was unavailable for data partition '$($dataPartitionConfig.Name)'. The runtime will require strict ordered fallback validation."
|
||||
}
|
||||
|
||||
$manifestPartitions.Add([pscustomobject][ordered]@{
|
||||
Name = [string]$dataPartitionConfig.Name
|
||||
RequestedDriveLetter = [string]$dataPartitionConfig.DriveLetter
|
||||
DataOrdinal = $dataPartitionIndex + 1
|
||||
PartitionNumber = [int]$resolvedDataPartition.Partition.PartitionNumber
|
||||
PartitionGuid = $partitionGuid
|
||||
Label = [string]$dataPartitionConfig.Label
|
||||
FileSystem = [string]$dataPartitionConfig.FileSystem
|
||||
SizeBytes = [uint64]$resolvedDataPartition.Partition.Size
|
||||
AllowSizeChange = ($Optimize -and ([int]$resolvedDataPartition.Partition.PartitionNumber -eq $resizablePartitionNumber))
|
||||
})
|
||||
}
|
||||
|
||||
if ($manifestPartitions.Count -eq 0) {
|
||||
throw 'Cannot create a data partition drive-letter manifest without opted-in partitions.'
|
||||
}
|
||||
|
||||
$manifestDirectory = Split-Path -Path $ManifestPath -Parent
|
||||
New-Item -Path $manifestDirectory -ItemType Directory -Force | Out-Null
|
||||
$manifest = [pscustomobject][ordered]@{
|
||||
SchemaVersion = 1
|
||||
ProcessorArchitecture = if ($WindowsArch -ieq 'arm64') { 'arm64' } else { 'amd64' }
|
||||
Partitions = $manifestPartitions.ToArray()
|
||||
}
|
||||
$manifest | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $ManifestPath -Encoding UTF8 -Force
|
||||
WriteLog "Created data partition drive-letter manifest at $ManifestPath with $($manifestPartitions.Count) partition(s)."
|
||||
|
||||
return $manifest
|
||||
}
|
||||
|
||||
function Add-FFUDataPartitionDriveLetterArtifacts {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$WindowsPartitionRoot,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object]$Layout,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object[]]$AdditionalDataPartitions,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$WindowsArch,
|
||||
[bool]$Optimize = $false,
|
||||
[int]$OptimizeFFUPartitionNumber = 0,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FFUDevelopmentPath
|
||||
)
|
||||
|
||||
$runtimeSourcePath = Join-Path -Path $FFUDevelopmentPath -ChildPath 'BuildFFUUnattend\Set-FFUDataPartitionDriveLetters.ps1'
|
||||
if (-not (Test-Path -LiteralPath $runtimeSourcePath -PathType Leaf)) {
|
||||
throw "Data partition drive-letter runtime script was not found at $runtimeSourcePath."
|
||||
}
|
||||
|
||||
$runtimeDirectory = Join-Path -Path $WindowsPartitionRoot -ChildPath 'Windows\Setup\Scripts\FFUDL'
|
||||
$runtimeScriptPath = Join-Path -Path $runtimeDirectory -ChildPath 'Apply.ps1'
|
||||
$manifestPath = Join-Path -Path $runtimeDirectory -ChildPath 'Manifest.json'
|
||||
New-Item -Path $runtimeDirectory -ItemType Directory -Force | Out-Null
|
||||
Copy-Item -LiteralPath $runtimeSourcePath -Destination $runtimeScriptPath -Force
|
||||
$null = New-FFUDataPartitionDriveLetterManifest -Layout $Layout -AdditionalDataPartitions $AdditionalDataPartitions -WindowsArch $WindowsArch -ManifestPath $manifestPath -Optimize $Optimize -OptimizeFFUPartitionNumber $OptimizeFFUPartitionNumber
|
||||
Remove-Item -LiteralPath (Join-Path $runtimeDirectory 'Audit.success'), (Join-Path $runtimeDirectory 'Audit.failure'), (Join-Path $runtimeDirectory 'Specialize.failure') -Force -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not (Test-Path -LiteralPath $runtimeScriptPath -PathType Leaf) -or -not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
|
||||
throw "Failed to stage data partition drive-letter runtime artifacts under $runtimeDirectory."
|
||||
}
|
||||
WriteLog "Staged data partition drive-letter runtime artifacts under $runtimeDirectory."
|
||||
|
||||
return [pscustomobject]@{
|
||||
RuntimeDirectory = $runtimeDirectory
|
||||
RuntimeScriptPath = $runtimeScriptPath
|
||||
ManifestPath = $manifestPath
|
||||
}
|
||||
}
|
||||
|
||||
function Add-FFUDataPartitionDriveLetterCommandToUnattend {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$UnattendPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ProcessorArchitecture
|
||||
)
|
||||
|
||||
$unattendNamespace = 'urn:schemas-microsoft-com:unattend'
|
||||
$wcmNamespace = 'http://schemas.microsoft.com/WMIConfig/2002/State'
|
||||
$resolvedProcessorArchitecture = if ($ProcessorArchitecture -ieq 'arm64') { 'arm64' } else { 'amd64' }
|
||||
$unattendXml = New-Object System.Xml.XmlDocument
|
||||
$unattendXml.PreserveWhitespace = $true
|
||||
|
||||
if (Test-Path -LiteralPath $UnattendPath -PathType Leaf) {
|
||||
$unattendXml.Load($UnattendPath)
|
||||
}
|
||||
else {
|
||||
$unattendDirectory = Split-Path -Path $UnattendPath -Parent
|
||||
New-Item -Path $unattendDirectory -ItemType Directory -Force | Out-Null
|
||||
$xmlDeclaration = $unattendXml.CreateXmlDeclaration('1.0', 'utf-8', $null)
|
||||
$null = $unattendXml.AppendChild($xmlDeclaration)
|
||||
$unattendRoot = $unattendXml.CreateElement('unattend', $unattendNamespace)
|
||||
$null = $unattendXml.AppendChild($unattendRoot)
|
||||
}
|
||||
|
||||
$unattendRoot = $unattendXml.DocumentElement
|
||||
if ($null -eq $unattendRoot -or $unattendRoot.LocalName -ne 'unattend' -or $unattendRoot.NamespaceURI -ne $unattendNamespace) {
|
||||
throw "Unattend XML at $UnattendPath does not use the supported unattend root namespace."
|
||||
}
|
||||
|
||||
$namespaceManager = New-Object System.Xml.XmlNamespaceManager($unattendXml.NameTable)
|
||||
$namespaceManager.AddNamespace('un', $unattendNamespace)
|
||||
$specializeSettings = $unattendRoot.SelectSingleNode("un:settings[@pass='specialize']", $namespaceManager)
|
||||
if ($null -eq $specializeSettings) {
|
||||
$specializeSettings = $unattendXml.CreateElement('settings', $unattendNamespace)
|
||||
$null = $specializeSettings.SetAttribute('pass', 'specialize')
|
||||
$firstSettingsNode = $unattendRoot.SelectSingleNode('un:settings', $namespaceManager)
|
||||
if ($null -ne $firstSettingsNode) {
|
||||
$null = $unattendRoot.InsertBefore($specializeSettings, $firstSettingsNode)
|
||||
}
|
||||
else {
|
||||
$null = $unattendRoot.AppendChild($specializeSettings)
|
||||
}
|
||||
}
|
||||
|
||||
$deploymentComponents = @($specializeSettings.SelectNodes("un:component[@name='Microsoft-Windows-Deployment']", $namespaceManager) |
|
||||
Where-Object { $_.GetAttribute('processorArchitecture') -ieq $resolvedProcessorArchitecture })
|
||||
if ($deploymentComponents.Count -gt 1) {
|
||||
throw "Unattend XML at $UnattendPath contains multiple Microsoft-Windows-Deployment components for $resolvedProcessorArchitecture."
|
||||
}
|
||||
if ($deploymentComponents.Count -eq 0) {
|
||||
$deploymentComponent = $unattendXml.CreateElement('component', $unattendNamespace)
|
||||
$null = $deploymentComponent.SetAttribute('name', 'Microsoft-Windows-Deployment')
|
||||
$null = $deploymentComponent.SetAttribute('processorArchitecture', $resolvedProcessorArchitecture)
|
||||
$null = $deploymentComponent.SetAttribute('publicKeyToken', '31bf3856ad364e35')
|
||||
$null = $deploymentComponent.SetAttribute('language', 'neutral')
|
||||
$null = $deploymentComponent.SetAttribute('versionScope', 'nonSxS')
|
||||
$null = $specializeSettings.AppendChild($deploymentComponent)
|
||||
}
|
||||
else {
|
||||
$deploymentComponent = $deploymentComponents[0]
|
||||
}
|
||||
|
||||
$runSynchronous = $deploymentComponent.SelectSingleNode('un:RunSynchronous', $namespaceManager)
|
||||
if ($null -eq $runSynchronous) {
|
||||
$runSynchronous = $unattendXml.CreateElement('RunSynchronous', $unattendNamespace)
|
||||
$null = $deploymentComponent.AppendChild($runSynchronous)
|
||||
}
|
||||
|
||||
$existingCommands = @($runSynchronous.SelectNodes('un:RunSynchronousCommand', $namespaceManager))
|
||||
$orderedExistingCommands = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||
$commandIndex = 0
|
||||
foreach ($existingCommand in $existingCommands) {
|
||||
$pathNode = $existingCommand.SelectSingleNode('un:Path', $namespaceManager)
|
||||
if ($null -ne $pathNode -and $pathNode.InnerText -match '(?i)\\FFUDL\\Apply\.ps1') {
|
||||
$null = $runSynchronous.RemoveChild($existingCommand)
|
||||
continue
|
||||
}
|
||||
|
||||
$orderValue = [int]::MaxValue
|
||||
$orderNode = $existingCommand.SelectSingleNode('un:Order', $namespaceManager)
|
||||
if ($null -ne $orderNode) {
|
||||
$parsedOrder = 0
|
||||
if ([int]::TryParse($orderNode.InnerText, [ref]$parsedOrder)) {
|
||||
$orderValue = $parsedOrder
|
||||
}
|
||||
}
|
||||
$orderedExistingCommands.Add([pscustomobject]@{ Node = $existingCommand; Order = $orderValue; Index = $commandIndex })
|
||||
$commandIndex++
|
||||
}
|
||||
|
||||
$nextOrder = 2
|
||||
foreach ($existingCommandInfo in @($orderedExistingCommands | Sort-Object -Property Order, Index)) {
|
||||
$orderNode = $existingCommandInfo.Node.SelectSingleNode('un:Order', $namespaceManager)
|
||||
if ($null -eq $orderNode) {
|
||||
$orderNode = $unattendXml.CreateElement('Order', $unattendNamespace)
|
||||
$null = $existingCommandInfo.Node.PrependChild($orderNode)
|
||||
}
|
||||
$orderNode.InnerText = [string]$nextOrder
|
||||
$nextOrder++
|
||||
}
|
||||
|
||||
$newCommand = $unattendXml.CreateElement('RunSynchronousCommand', $unattendNamespace)
|
||||
$actionAttribute = $unattendXml.CreateAttribute('wcm', 'action', $wcmNamespace)
|
||||
$actionAttribute.Value = 'add'
|
||||
$null = $newCommand.Attributes.Append($actionAttribute)
|
||||
$orderElement = $unattendXml.CreateElement('Order', $unattendNamespace)
|
||||
$orderElement.InnerText = '1'
|
||||
$null = $newCommand.AppendChild($orderElement)
|
||||
$descriptionElement = $unattendXml.CreateElement('Description', $unattendNamespace)
|
||||
$descriptionElement.InnerText = 'Apply FFU data partition drive letters'
|
||||
$null = $newCommand.AppendChild($descriptionElement)
|
||||
$pathElement = $unattendXml.CreateElement('Path', $unattendNamespace)
|
||||
$specializeCommand = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\Windows\Setup\Scripts\FFUDL\Apply.ps1" -ManifestPath "C:\Windows\Setup\Scripts\FFUDL\Manifest.json" -Phase Specialize'
|
||||
if ($specializeCommand.Length -gt 259) {
|
||||
throw "Data partition drive-letter specialize command exceeds the 259-character unattend Path limit. Length: $($specializeCommand.Length)."
|
||||
}
|
||||
$pathElement.InnerText = $specializeCommand
|
||||
$null = $newCommand.AppendChild($pathElement)
|
||||
$willRebootElement = $unattendXml.CreateElement('WillReboot', $unattendNamespace)
|
||||
$willRebootElement.InnerText = 'OnRequest'
|
||||
$null = $newCommand.AppendChild($willRebootElement)
|
||||
if ($null -ne $runSynchronous.FirstChild) {
|
||||
$null = $runSynchronous.InsertBefore($newCommand, $runSynchronous.FirstChild)
|
||||
}
|
||||
else {
|
||||
$null = $runSynchronous.AppendChild($newCommand)
|
||||
}
|
||||
|
||||
$unattendXml.Save($UnattendPath)
|
||||
WriteLog "Merged data partition drive-letter specialize command into $UnattendPath."
|
||||
}
|
||||
|
||||
function Test-FFUDataPartitionDriveLetterAuditResult {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$WindowsPartitionRoot
|
||||
)
|
||||
|
||||
$runtimeDirectory = Join-Path -Path $WindowsPartitionRoot -ChildPath 'Windows\Setup\Scripts\FFUDL'
|
||||
$successMarkerPath = Join-Path -Path $runtimeDirectory -ChildPath 'Audit.success'
|
||||
$failureMarkerPath = Join-Path -Path $runtimeDirectory -ChildPath 'Audit.failure'
|
||||
$auditLogPath = Join-Path -Path $WindowsPartitionRoot -ChildPath 'Windows\Temp\FFUDataPartitionDriveLetters.log'
|
||||
if (Test-Path -LiteralPath $failureMarkerPath -PathType Leaf) {
|
||||
$failureMessage = Get-Content -LiteralPath $failureMarkerPath -Raw -ErrorAction SilentlyContinue
|
||||
throw "Build VM data partition drive-letter enforcement failed. $failureMessage"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $successMarkerPath -PathType Leaf)) {
|
||||
throw 'Build VM data partition drive-letter enforcement did not create its audit success marker.'
|
||||
}
|
||||
|
||||
Remove-Item -LiteralPath $successMarkerPath, $failureMarkerPath -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $auditLogPath -Force -ErrorAction SilentlyContinue
|
||||
WriteLog 'Validated build VM data partition drive-letter enforcement and removed audit result files.'
|
||||
}
|
||||
|
||||
function Get-FFUOptimizePartitionNumber {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ciminstance]$Disk,
|
||||
[object]$Layout,
|
||||
[int]$RequestedPartitionNumber = 0,
|
||||
[string]$WindowsPartitionDriveLetter,
|
||||
[object[]]$AdditionalDataPartitions = @()
|
||||
)
|
||||
|
||||
@@ -3545,33 +3919,9 @@ function Get-FFUOptimizePartitionNumber {
|
||||
$dataPartitionIndex++
|
||||
}
|
||||
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."
|
||||
}
|
||||
|
||||
$basicDataPartitions = @($Disk | Get-Partition | Where-Object { $_.GptType -eq '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' } | Sort-Object -Property PartitionNumber)
|
||||
$windowsPartition = Get-WindowsPartitionFromDisk -Disk $Disk -DriveLetter $WindowsPartitionDriveLetter
|
||||
$dataPartitionsOnDisk = @($basicDataPartitions | Where-Object { $null -eq $windowsPartition -or $_.PartitionNumber -ne $windowsPartition.PartitionNumber } | Sort-Object -Property PartitionNumber)
|
||||
$dataPartition = $null
|
||||
if ($fillRemainingDataPartitionIndex -ge 0 -and $fillRemainingDataPartitionIndex -lt $dataPartitionsOnDisk.Count) {
|
||||
$dataPartition = $dataPartitionsOnDisk[$fillRemainingDataPartitionIndex]
|
||||
}
|
||||
$dataPartition = if ($fillRemainingDataPartitionIndex -ge 0 -and $fillRemainingDataPartitionIndex -lt $Layout.DataPartitions.Count) { $Layout.DataPartitions[$fillRemainingDataPartitionIndex].Partition } else { $null }
|
||||
if ($null -eq $dataPartition) {
|
||||
throw "Unable to resolve FillRemaining data partition '$($fillRemainingDataPartition.Name)' at drive ${driveLetter}: for FFU optimization."
|
||||
}
|
||||
|
||||
$resolvedDriveLetter = ([string]$dataPartition.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
|
||||
if ($resolvedDriveLetter -ne $driveLetter) {
|
||||
$existingDrive = Get-PSDrive -Name $driveLetter -PSProvider FileSystem -ErrorAction SilentlyContinue
|
||||
if ($null -ne $existingDrive) {
|
||||
throw "Unable to assign drive letter ${driveLetter}: to FillRemaining data partition '$($fillRemainingDataPartition.Name)' for FFU optimization because ${driveLetter}: is already in use."
|
||||
}
|
||||
|
||||
$resolvedDriveLetterLog = if ([string]::IsNullOrWhiteSpace($resolvedDriveLetter)) { 'no drive letter' } else { "drive ${resolvedDriveLetter}:" }
|
||||
WriteLog "Reassigning FillRemaining data partition '$($fillRemainingDataPartition.Name)' from $resolvedDriveLetterLog to drive ${driveLetter}: for FFU optimization."
|
||||
Set-Partition -DiskNumber $dataPartition.DiskNumber -PartitionNumber $dataPartition.PartitionNumber -NewDriveLetter $driveLetter -ErrorAction Stop
|
||||
$dataPartition = Get-Partition -DiskNumber $dataPartition.DiskNumber -PartitionNumber $dataPartition.PartitionNumber -ErrorAction Stop
|
||||
throw "Unable to resolve FillRemaining data partition '$($fillRemainingDataPartition.Name)' for FFU optimization."
|
||||
}
|
||||
|
||||
$resolvedDriveLetter = ([string]$dataPartition.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
|
||||
@@ -3580,12 +3930,11 @@ function Get-FFUOptimizePartitionNumber {
|
||||
return [int]$dataPartition.PartitionNumber
|
||||
}
|
||||
|
||||
$windowsPartition = Get-WindowsPartitionFromDisk -Disk $Disk -DriveLetter $WindowsPartitionDriveLetter
|
||||
if ($null -eq $windowsPartition) {
|
||||
if ($null -eq $Layout.WindowsPartition) {
|
||||
throw 'Unable to resolve Windows partition for FFU optimization.'
|
||||
}
|
||||
|
||||
WriteLog "Using DISM default Windows partition selection for FFU optimization. Resolved Windows partition number $($windowsPartition.PartitionNumber)."
|
||||
WriteLog "Using DISM default Windows partition selection for FFU optimization. Resolved Windows partition number $($Layout.WindowsPartition.PartitionNumber)."
|
||||
return 0
|
||||
}
|
||||
#Add boot files
|
||||
@@ -4328,7 +4677,12 @@ function New-PEMedia {
|
||||
function Optimize-FFUCaptureDrive {
|
||||
param (
|
||||
[string]$VhdxPath,
|
||||
[bool]$EnableVolumeRetrim = $false
|
||||
[bool]$EnableVolumeRetrim = $false,
|
||||
[string]$SystemPartitionDriveLetter,
|
||||
[string]$WindowsPartitionDriveLetter,
|
||||
[string]$RecoveryPartitionDriveLetter,
|
||||
[bool]$CreateRecoveryPartition = $true,
|
||||
[object[]]$AdditionalDataPartitions = @()
|
||||
)
|
||||
try {
|
||||
# Resolve whether the VHDX is already attached and get the disk reference
|
||||
@@ -4342,8 +4696,9 @@ function Optimize-FFUCaptureDrive {
|
||||
$mountedDisk = Mount-VHD -Path $VhdxPath -Passthru | Get-Disk
|
||||
}
|
||||
|
||||
# Resolve the OS partition drive letter used for volume-level optimization
|
||||
$osPartition = Get-WindowsPartitionFromDisk -Disk $mountedDisk -DriveLetter $WindowsPartitionDriveLetter
|
||||
$partitionLayout = Resolve-VhdxPartitionLayout -Disk $mountedDisk -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $AdditionalDataPartitions
|
||||
$partitionLayout = Set-VhdxBuildPartitionDriveLetters -Layout $partitionLayout -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $AdditionalDataPartitions
|
||||
$osPartition = $partitionLayout.WindowsPartition
|
||||
if ($null -eq $osPartition -or [string]::IsNullOrWhiteSpace($osPartition.DriveLetter)) {
|
||||
throw 'Unable to resolve Windows partition drive letter for VHDX optimization.'
|
||||
}
|
||||
@@ -4383,7 +4738,12 @@ function Optimize-FFUCaptureDrive {
|
||||
function Get-CaptureVhdContext {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$VhdxPath
|
||||
[string]$VhdxPath,
|
||||
[string]$SystemPartitionDriveLetter,
|
||||
[string]$WindowsPartitionDriveLetter,
|
||||
[string]$RecoveryPartitionDriveLetter,
|
||||
[bool]$CreateRecoveryPartition = $true,
|
||||
[object[]]$AdditionalDataPartitions = @()
|
||||
)
|
||||
|
||||
WriteLog 'Resolving VHDX context for host-side FFU capture'
|
||||
@@ -4398,7 +4758,9 @@ function Get-CaptureVhdContext {
|
||||
$captureDisk = Mount-VHD -Path $VhdxPath -Passthru | Get-Disk
|
||||
}
|
||||
|
||||
$captureOsPartition = Get-WindowsPartitionFromDisk -Disk $captureDisk -DriveLetter $WindowsPartitionDriveLetter
|
||||
$partitionLayout = Resolve-VhdxPartitionLayout -Disk $captureDisk -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $AdditionalDataPartitions
|
||||
$partitionLayout = Set-VhdxBuildPartitionDriveLetters -Layout $partitionLayout -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $AdditionalDataPartitions
|
||||
$captureOsPartition = $partitionLayout.WindowsPartition
|
||||
if ($null -eq $captureOsPartition) {
|
||||
throw 'Unable to resolve Windows partition for FFU capture.'
|
||||
}
|
||||
@@ -4411,6 +4773,7 @@ function Get-CaptureVhdContext {
|
||||
OsPartition = $captureOsPartition
|
||||
OsPartitionDriveLetter = $captureOsPartition.DriveLetter
|
||||
WindowsPartition = "$($captureOsPartition.DriveLetter):\"
|
||||
Layout = $partitionLayout
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4535,11 +4898,11 @@ function New-FFUFileName {
|
||||
}
|
||||
|
||||
function New-FFU {
|
||||
$captureContext = Get-CaptureVhdContext -VhdxPath $VHDXPath
|
||||
$captureContext = Get-CaptureVhdContext -VhdxPath $VHDXPath -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $normalizedAdditionalDataPartitions
|
||||
$captureDisk = $captureContext.Disk
|
||||
$resolvedFFUOptimizePartitionNumber = 0
|
||||
if ($Optimize -eq $true) {
|
||||
$resolvedFFUOptimizePartitionNumber = Get-FFUOptimizePartitionNumber -Disk $captureDisk -RequestedPartitionNumber $OptimizeFFUPartitionNumber -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -AdditionalDataPartitions $normalizedAdditionalDataPartitions
|
||||
$resolvedFFUOptimizePartitionNumber = Get-FFUOptimizePartitionNumber -Layout $captureContext.Layout -RequestedPartitionNumber $OptimizeFFUPartitionNumber -AdditionalDataPartitions $normalizedAdditionalDataPartitions
|
||||
}
|
||||
$ffuCaptureNamingInfo = Get-FFUCaptureNamingInfo -ShortenedWindowsSKU $shortenedWindowsSKU -WindowsRelease $WindowsRelease -WindowsVersion $WindowsVersion -InstallationType $installationType -IsWindows10LtscClient:$isWindows10LtscClient
|
||||
|
||||
@@ -6317,6 +6680,7 @@ Set-Progress -Percentage 2 -Message "Validating parameters..."
|
||||
#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 {
|
||||
$normalizedAdditionalDataPartitions = ConvertTo-NormalizedDataPartitions -DataPartitions $AdditionalDataPartitions
|
||||
$persistDataPartitionDriveLetters = @($normalizedAdditionalDataPartitions | Where-Object { $_.PersistDriveLetter }).Count -gt 0
|
||||
if ($normalizedAdditionalDataPartitions.Count -gt 0 -and $OSPartitionSize -le 0) {
|
||||
$osPartitionSizeMessage = if ($CreateRecoveryPartition) {
|
||||
'OSPartitionSize must be set when AdditionalDataPartitions are configured so space remains for Recovery and data partitions.'
|
||||
@@ -7638,17 +8002,8 @@ try {
|
||||
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 ($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
|
||||
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 ($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 'PartitionLayoutSignatureVersion') { WriteLog 'PartitionLayoutSignatureVersion missing in cached config, continuing'; continue }
|
||||
if ([uint32]$vhdxCacheItem.PartitionLayoutSignatureVersion -ne $partitionLayoutSignatureVersion) { WriteLog "PartitionLayoutSignatureVersion mismatch (cached: $($vhdxCacheItem.PartitionLayoutSignatureVersion), current: $partitionLayoutSignatureVersion), 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 }
|
||||
|
||||
@@ -8122,7 +8477,9 @@ try {
|
||||
$VHDXPath = Join-Path $($VMPath) $($cachedVHDXInfo.VhdxFileName)
|
||||
|
||||
$vhdxDisk = Get-VHD -Path $VHDXPath | Mount-VHD -Passthru | Get-Disk
|
||||
$osPartition = Get-WindowsPartitionFromDisk -Disk $vhdxDisk -DriveLetter $WindowsPartitionDriveLetter
|
||||
$partitionLayout = Resolve-VhdxPartitionLayout -Disk $vhdxDisk -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $normalizedAdditionalDataPartitions
|
||||
$partitionLayout = Set-VhdxBuildPartitionDriveLetters -Layout $partitionLayout -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $normalizedAdditionalDataPartitions
|
||||
$osPartition = $partitionLayout.WindowsPartition
|
||||
$osPartitionDriveLetter = $osPartition.DriveLetter
|
||||
$WindowsPartition = $osPartitionDriveLetter + ':\'
|
||||
|
||||
@@ -8140,7 +8497,7 @@ try {
|
||||
|
||||
# Run full VHDX optimization after servicing/cleanup and before cache copy
|
||||
WriteLog 'Optimizing VHDX before copying to cache dir'
|
||||
Optimize-FFUCaptureDrive -VhdxPath $VHDXPath -EnableVolumeRetrim $true
|
||||
Optimize-FFUCaptureDrive -VhdxPath $VHDXPath -EnableVolumeRetrim $true -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $normalizedAdditionalDataPartitions
|
||||
|
||||
WriteLog 'Copying to cache dir'
|
||||
|
||||
@@ -8162,9 +8519,7 @@ try {
|
||||
$cachedVHDXInfo.LogicalSectorSizeBytes = $LogicalSectorSizeBytes
|
||||
$cachedVHDXInfo.Disksize = $Disksize
|
||||
$cachedVHDXInfo.CreateRecoveryPartition = $CreateRecoveryPartition
|
||||
$cachedVHDXInfo.SystemPartitionDriveLetter = [string]$SystemPartitionDriveLetter
|
||||
$cachedVHDXInfo.WindowsPartitionDriveLetter = [string]$WindowsPartitionDriveLetter
|
||||
$cachedVHDXInfo.RecoveryPartitionDriveLetter = [string]$RecoveryPartitionDriveLetter
|
||||
$cachedVHDXInfo.PartitionLayoutSignatureVersion = $partitionLayoutSignatureVersion
|
||||
$cachedVHDXInfo.PartitionLayoutSignature = $partitionLayoutSignature
|
||||
$cachedVHDXInfo.WindowsSKU = $WindowsSKU
|
||||
$cachedVHDXInfo.WindowsRelease = $WindowsRelease
|
||||
@@ -8365,7 +8720,9 @@ if ($InstallApps) {
|
||||
WriteLog 'Mounting VHDX to inject unattend for audit-mode boot'
|
||||
$disk = Mount-VHD -Path $VHDXPath -Passthru | Get-Disk
|
||||
}
|
||||
$osPartition = Get-WindowsPartitionFromDisk -Disk $disk -DriveLetter $WindowsPartitionDriveLetter
|
||||
$partitionLayout = Resolve-VhdxPartitionLayout -Disk $disk -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $normalizedAdditionalDataPartitions
|
||||
$partitionLayout = Set-VhdxBuildPartitionDriveLetters -Layout $partitionLayout -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $normalizedAdditionalDataPartitions
|
||||
$osPartition = $partitionLayout.WindowsPartition
|
||||
$osPartitionDriveLetter = $osPartition.DriveLetter
|
||||
WriteLog 'Copying unattend file to boot to audit mode'
|
||||
New-Item -Path "$($osPartitionDriveLetter):\Windows\Panther\Unattend" -ItemType Directory -Force | Out-Null
|
||||
@@ -8376,6 +8733,10 @@ if ($InstallApps) {
|
||||
$orchestrationBootstrapTargetFolder = "$($osPartitionDriveLetter):\Windows\Setup\Scripts"
|
||||
New-Item -Path $orchestrationBootstrapTargetFolder -ItemType Directory -Force | Out-Null
|
||||
Copy-Item -Path $orchestrationBootstrapSourcePath -Destination (Join-Path -Path $orchestrationBootstrapTargetFolder -ChildPath 'Start-FFUOrchestration.ps1') -Force | Out-Null
|
||||
if ($persistDataPartitionDriveLetters) {
|
||||
$windowsPartitionRoot = "$($osPartitionDriveLetter):\"
|
||||
$null = Add-FFUDataPartitionDriveLetterArtifacts -WindowsPartitionRoot $windowsPartitionRoot -Layout $partitionLayout -AdditionalDataPartitions $normalizedAdditionalDataPartitions -WindowsArch $WindowsArch -Optimize $Optimize -OptimizeFFUPartitionNumber $OptimizeFFUPartitionNumber -FFUDevelopmentPath $FFUDevelopmentPath
|
||||
}
|
||||
if ($WindowsArch -eq 'x64') {
|
||||
Copy-Item -Path "$FFUDevelopmentPath\BuildFFUUnattend\unattend_x64.xml" -Destination "$($osPartitionDriveLetter):\Windows\Panther\Unattend\Unattend.xml" -Force | Out-Null
|
||||
}
|
||||
@@ -8386,6 +8747,24 @@ if ($InstallApps) {
|
||||
# Always dismount so downstream VM creation logic has a clean starting point
|
||||
Dismount-ScratchVhdx -VhdxPath $VHDXPath
|
||||
}
|
||||
elseif ($persistDataPartitionDriveLetters) {
|
||||
$vhdMeta = Get-VHD -Path $VHDXPath
|
||||
if ($vhdMeta.Attached) {
|
||||
WriteLog 'VHDX already mounted; reusing existing mount for data partition drive-letter staging'
|
||||
$disk = Get-Disk -Number $vhdMeta.DiskNumber
|
||||
}
|
||||
else {
|
||||
WriteLog 'Mounting VHDX to stage data partition drive-letter persistence'
|
||||
$disk = Mount-VHD -Path $VHDXPath -Passthru | Get-Disk
|
||||
}
|
||||
$partitionLayout = Resolve-VhdxPartitionLayout -Disk $disk -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $normalizedAdditionalDataPartitions
|
||||
$partitionLayout = Set-VhdxBuildPartitionDriveLetters -Layout $partitionLayout -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $normalizedAdditionalDataPartitions
|
||||
$osPartitionDriveLetter = $partitionLayout.WindowsPartition.DriveLetter
|
||||
$windowsPartitionRoot = "$($osPartitionDriveLetter):\"
|
||||
$null = Add-FFUDataPartitionDriveLetterArtifacts -WindowsPartitionRoot $windowsPartitionRoot -Layout $partitionLayout -AdditionalDataPartitions $normalizedAdditionalDataPartitions -WindowsArch $WindowsArch -Optimize $Optimize -OptimizeFFUPartitionNumber $OptimizeFFUPartitionNumber -FFUDevelopmentPath $FFUDevelopmentPath
|
||||
$deploymentUnattendPath = Join-Path -Path $windowsPartitionRoot -ChildPath 'Windows\Panther\Unattend.xml'
|
||||
Add-FFUDataPartitionDriveLetterCommandToUnattend -UnattendPath $deploymentUnattendPath -ProcessorArchitecture $WindowsArch
|
||||
}
|
||||
|
||||
#If installing apps (Office or 3rd party), we need to build a VM and capture that FFU, if not, just cut the FFU from the VHDX file
|
||||
if ($InstallApps) {
|
||||
@@ -8445,8 +8824,48 @@ try {
|
||||
WriteLog 'Waiting for VM to shutdown'
|
||||
} while ($FFUVM.State -ne 'Off')
|
||||
WriteLog 'VM Shutdown'
|
||||
if ($persistDataPartitionDriveLetters) {
|
||||
$vhdMeta = Get-VHD -Path $VHDXPath
|
||||
if ($vhdMeta.Attached) {
|
||||
WriteLog 'VHDX already mounted; reusing existing mount to validate data partition drive-letter audit results'
|
||||
$disk = Get-Disk -Number $vhdMeta.DiskNumber
|
||||
}
|
||||
else {
|
||||
WriteLog 'Mounting VHDX to validate data partition drive-letter audit results'
|
||||
$disk = Mount-VHD -Path $VHDXPath -Passthru | Get-Disk
|
||||
}
|
||||
$partitionLayout = Resolve-VhdxPartitionLayout -Disk $disk -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $normalizedAdditionalDataPartitions
|
||||
$partitionLayout = Set-VhdxBuildPartitionDriveLetters -Layout $partitionLayout -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $normalizedAdditionalDataPartitions
|
||||
$osPartitionDriveLetter = $partitionLayout.WindowsPartition.DriveLetter
|
||||
$windowsPartitionRoot = "$($osPartitionDriveLetter):\"
|
||||
Test-FFUDataPartitionDriveLetterAuditResult -WindowsPartitionRoot $windowsPartitionRoot
|
||||
|
||||
$runtimeDirectory = Join-Path -Path $windowsPartitionRoot -ChildPath 'Windows\Setup\Scripts\FFUDL'
|
||||
$runtimeScriptPath = Join-Path -Path $runtimeDirectory -ChildPath 'Apply.ps1'
|
||||
$manifestPath = Join-Path -Path $runtimeDirectory -ChildPath 'Manifest.json'
|
||||
if (-not (Test-Path -LiteralPath $runtimeScriptPath -PathType Leaf) -or -not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
|
||||
throw "Data partition drive-letter runtime artifacts are missing from $runtimeDirectory after audit validation."
|
||||
}
|
||||
|
||||
$deploymentUnattendPath = Join-Path -Path $windowsPartitionRoot -ChildPath 'Windows\Panther\Unattend.xml'
|
||||
if (-not (Test-Path -LiteralPath $deploymentUnattendPath -PathType Leaf) -and $InjectUnattend) {
|
||||
$deploymentUnattendDirectory = Split-Path -Path $deploymentUnattendPath -Parent
|
||||
New-Item -Path $deploymentUnattendDirectory -ItemType Directory -Force | Out-Null
|
||||
$stagedInjectedUnattendPath = Join-Path -Path $AppsPath -ChildPath 'Unattend\Unattend.xml'
|
||||
if (Test-Path -LiteralPath $stagedInjectedUnattendPath -PathType Leaf) {
|
||||
Copy-Item -LiteralPath $stagedInjectedUnattendPath -Destination $deploymentUnattendPath -Force
|
||||
WriteLog "Restored staged deployment unattend to $deploymentUnattendPath before adding data partition drive-letter persistence."
|
||||
}
|
||||
else {
|
||||
$unattendSource = Get-UnattendSourcePath -UnattendFolder $UnattendFolder -WindowsArch $WindowsArch -UnattendX64FilePath $UnattendX64FilePath -UnattendArm64FilePath $UnattendArm64FilePath
|
||||
Save-StagedUnattendFile -SourcePath $unattendSource -DestinationPath $deploymentUnattendPath -DeviceNamingMode $DeviceNamingMode -DeviceNameTemplate $normalizedDeviceNameTemplate -WindowsArch $WindowsArch
|
||||
WriteLog "Restaged deployment unattend to $deploymentUnattendPath before adding data partition drive-letter persistence."
|
||||
}
|
||||
}
|
||||
Add-FFUDataPartitionDriveLetterCommandToUnattend -UnattendPath $deploymentUnattendPath -ProcessorArchitecture $WindowsArch
|
||||
}
|
||||
Set-Progress -Percentage 65 -Message "Optimizing VHDX before capture..."
|
||||
Optimize-FFUCaptureDrive -VhdxPath $VHDXPath
|
||||
Optimize-FFUCaptureDrive -VhdxPath $VHDXPath -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $normalizedAdditionalDataPartitions
|
||||
#Capture FFU file
|
||||
New-FFU
|
||||
}
|
||||
|
||||
@@ -365,6 +365,7 @@
|
||||
<ColumnDefinition Width="120"/>
|
||||
<ColumnDefinition Width="140"/>
|
||||
<ColumnDefinition Width="130"/>
|
||||
<ColumnDefinition Width="160"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
@@ -374,8 +375,9 @@
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Name" Margin="0,0,8,8"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="Drive Letter" Margin="0,0,8,8"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="2" Text="Size (GB)" Margin="0,0,8,8"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="3" Text="Fill Remaining" Margin="0,0,0,8"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="4" Text=" " Margin="0,0,0,8"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="3" Text="Fill Remaining" Margin="0,0,8,8"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="4" Text="Persist Drive Letter" Margin="0,0,8,8"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="5" Text=" " Margin="0,0,0,8"/>
|
||||
<TextBox x:Name="txtDataPartitionName" Grid.Row="1" Grid.Column="0" Margin="0,0,8,0" ToolTip="Name and volume label for the data partition."/>
|
||||
<ComboBox x:Name="cmbDataPartitionDriveLetter" Grid.Row="1" Grid.Column="1" Margin="0,0,8,0" ToolTip="Build-time drive letter for the data partition.">
|
||||
<ComboBoxItem Content="D" IsSelected="True"/>
|
||||
@@ -392,16 +394,20 @@
|
||||
<ComboBoxItem Content="O"/>
|
||||
<ComboBoxItem Content="P"/>
|
||||
<ComboBoxItem Content="Q"/>
|
||||
<ComboBoxItem Content="R"/>
|
||||
<ComboBoxItem Content="S"/>
|
||||
<ComboBoxItem Content="T"/>
|
||||
<ComboBoxItem Content="U"/>
|
||||
<ComboBoxItem Content="V"/>
|
||||
<ComboBoxItem Content="W"/>
|
||||
<ComboBoxItem Content="X"/>
|
||||
<ComboBoxItem Content="Y"/>
|
||||
<ComboBoxItem Content="Z"/>
|
||||
</ComboBox>
|
||||
<TextBox x:Name="txtDataPartitionSizeGB" Grid.Row="1" Grid.Column="2" Margin="0,0,8,0" ToolTip="Data partition size in GB. Leave blank only when Fill Remaining is selected."/>
|
||||
<CheckBox x:Name="chkDataPartitionFillRemaining" Grid.Row="1" Grid.Column="3" VerticalAlignment="Center" ToolTip="Use the remaining VHDX space for this data partition. Only one data partition can fill remaining space."/>
|
||||
<Button x:Name="btnAddDataPartition" Grid.Row="1" Grid.Column="4" Content="Add Data Partition" Width="160" ToolTip="Add the configured data partition to the build configuration."/>
|
||||
<CheckBox x:Name="chkDataPartitionFillRemaining" Grid.Row="1" Grid.Column="3" Margin="0,0,8,0" VerticalAlignment="Center" ToolTip="Use the remaining VHDX space for this data partition. Only one data partition can fill remaining space."/>
|
||||
<CheckBox x:Name="chkDataPartitionPersistDriveLetter" Grid.Row="1" Grid.Column="4" Margin="0,0,8,0" VerticalAlignment="Center" ToolTip="Keep this drive letter in the build VM and on devices deployed with FFU Builder."/>
|
||||
<Button x:Name="btnAddDataPartition" Grid.Row="1" Grid.Column="5" Content="Add Data Partition" Width="160" ToolTip="Add the configured data partition to the build configuration."/>
|
||||
</Grid>
|
||||
|
||||
<Grid Margin="0,0,0,12">
|
||||
|
||||
@@ -341,6 +341,14 @@ function Get-PartitionSizeGBDisplay {
|
||||
}
|
||||
|
||||
function Get-PartitionDriveLetterOptions {
|
||||
param(
|
||||
[switch]$DataPartition
|
||||
)
|
||||
|
||||
if ($DataPartition) {
|
||||
return @('D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z')
|
||||
}
|
||||
|
||||
return @('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z')
|
||||
}
|
||||
|
||||
@@ -355,12 +363,15 @@ function New-DiskLayoutPartitionRow {
|
||||
[string]$SizeGB,
|
||||
[int64]$SizeBytes = 0,
|
||||
[bool]$FillRemaining,
|
||||
[bool]$PersistDriveLetter,
|
||||
[string]$FileSystem = 'NTFS',
|
||||
[bool]$CanSelect,
|
||||
[bool]$CanEditDriveLetter,
|
||||
[bool]$CanEditSize,
|
||||
[bool]$CanEditFillRemaining,
|
||||
[string]$FillRemainingVisibility = 'Visible',
|
||||
[bool]$CanEditPersistDriveLetter,
|
||||
[string]$PersistDriveLetterVisibility = 'Hidden',
|
||||
[bool]$CanRemove,
|
||||
[bool]$CanReorder
|
||||
)
|
||||
@@ -376,15 +387,18 @@ function New-DiskLayoutPartitionRow {
|
||||
Name = $Name
|
||||
Label = $Label
|
||||
DriveLetter = $DriveLetter
|
||||
DriveLetterOptions = @(Get-PartitionDriveLetterOptions)
|
||||
DriveLetterOptions = @(Get-PartitionDriveLetterOptions -DataPartition:($PartitionType -eq 'Data'))
|
||||
SizeGB = $SizeGB
|
||||
SizeBytes = $SizeBytes
|
||||
FillRemaining = $FillRemaining
|
||||
FillRemainingVisibility = $FillRemainingVisibility
|
||||
PersistDriveLetter = $PersistDriveLetter
|
||||
PersistDriveLetterVisibility = $PersistDriveLetterVisibility
|
||||
FileSystem = $FileSystem
|
||||
CanEditDriveLetter = $CanEditDriveLetter
|
||||
CanEditSize = $CanEditSize
|
||||
CanEditFillRemaining = $CanEditFillRemaining
|
||||
CanEditPersistDriveLetter = $CanEditPersistDriveLetter
|
||||
CanRemove = $CanRemove
|
||||
CanReorder = $CanReorder
|
||||
}
|
||||
@@ -433,7 +447,11 @@ function Get-DiskLayoutPartitionRows {
|
||||
}
|
||||
|
||||
foreach ($dataPartition in @($State.Data.additionalDataPartitionsDataList)) {
|
||||
$rows.Add((New-DiskLayoutPartitionRow -PartitionType 'Data' -Name $dataPartition.Name -Label $dataPartition.Label -DriveLetter $dataPartition.DriveLetter -SizeGB $dataPartition.SizeGB -SizeBytes ([int64]$dataPartition.SizeBytes) -FillRemaining ([bool]$dataPartition.FillRemaining) -FileSystem $dataPartition.FileSystem -CanSelect $true -CanEditDriveLetter $true -CanEditSize $true -CanEditFillRemaining $true -CanRemove $true -CanReorder $true))
|
||||
$persistDriveLetter = $false
|
||||
if ($dataPartition.PSObject.Properties.Name -contains 'PersistDriveLetter') {
|
||||
$persistDriveLetter = [bool]$dataPartition.PersistDriveLetter
|
||||
}
|
||||
$rows.Add((New-DiskLayoutPartitionRow -PartitionType 'Data' -Name $dataPartition.Name -Label $dataPartition.Label -DriveLetter $dataPartition.DriveLetter -SizeGB $dataPartition.SizeGB -SizeBytes ([int64]$dataPartition.SizeBytes) -FillRemaining ([bool]$dataPartition.FillRemaining) -PersistDriveLetter $persistDriveLetter -FileSystem $dataPartition.FileSystem -CanSelect $true -CanEditDriveLetter $true -CanEditSize $true -CanEditFillRemaining $true -CanEditPersistDriveLetter $true -PersistDriveLetterVisibility 'Visible' -CanRemove $true -CanReorder $true))
|
||||
}
|
||||
|
||||
return $rows.ToArray()
|
||||
@@ -493,6 +511,7 @@ function Sync-DiskLayoutRowsToControls {
|
||||
SizeGB = $row.SizeGB
|
||||
SizeBytes = [int64]$row.SizeBytes
|
||||
FillRemaining = [bool]$row.FillRemaining
|
||||
PersistDriveLetter = [bool]$row.PersistDriveLetter
|
||||
FileSystem = $row.FileSystem
|
||||
})
|
||||
}
|
||||
@@ -805,8 +824,10 @@ function Test-DiskLayoutConfiguration {
|
||||
if ($partitionRow.PartitionType -eq 'Recovery' -and -not [bool]$Config.CreateRecoveryPartition) { continue }
|
||||
|
||||
$driveLetter = ([string]$partitionRow.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
|
||||
if ([string]::IsNullOrWhiteSpace($driveLetter) -or $driveLetter -notmatch '^[A-Z]$') {
|
||||
$errors.Add("$($partitionRow.Name) must use a single drive letter from A to Z.")
|
||||
$validDriveLetterPattern = if ($partitionRow.PartitionType -eq 'Data') { '^[D-Z]$' } else { '^[A-Z]$' }
|
||||
$validDriveLetterRange = if ($partitionRow.PartitionType -eq 'Data') { 'D to Z' } else { 'A to Z' }
|
||||
if ([string]::IsNullOrWhiteSpace($driveLetter) -or $driveLetter -notmatch $validDriveLetterPattern) {
|
||||
$errors.Add("$($partitionRow.Name) must use a single drive letter from $validDriveLetterRange.")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -883,6 +904,7 @@ function Clear-AdditionalDataPartitionForm {
|
||||
$State.Controls.txtDataPartitionName.Clear()
|
||||
$State.Controls.txtDataPartitionSizeGB.Clear()
|
||||
$State.Controls.chkDataPartitionFillRemaining.IsChecked = $false
|
||||
$State.Controls.chkDataPartitionPersistDriveLetter.IsChecked = $false
|
||||
$State.Controls.txtDataPartitionSizeGB.IsEnabled = $true
|
||||
}
|
||||
|
||||
@@ -915,8 +937,8 @@ function Add-AdditionalDataPartition {
|
||||
}
|
||||
|
||||
$driveLetter = ([string](Get-ComboBoxSelectedContent -ComboBox $State.Controls.cmbDataPartitionDriveLetter)).Trim().TrimEnd(':').ToUpperInvariant()
|
||||
if ([string]::IsNullOrWhiteSpace($driveLetter) -or $driveLetter -notmatch '^[A-Z]$') {
|
||||
[System.Windows.MessageBox]::Show("Select a drive letter for the data partition.", "Data Partition Drive Letter", "OK", "Warning") | Out-Null
|
||||
if ([string]::IsNullOrWhiteSpace($driveLetter) -or $driveLetter -notmatch '^[D-Z]$') {
|
||||
[System.Windows.MessageBox]::Show("Select a drive letter from D through Z for the data partition.", "Data Partition Drive Letter", "OK", "Warning") | Out-Null
|
||||
return $false
|
||||
}
|
||||
|
||||
@@ -944,6 +966,7 @@ function Add-AdditionalDataPartition {
|
||||
}
|
||||
|
||||
$fillRemaining = $true -eq $State.Controls.chkDataPartitionFillRemaining.IsChecked
|
||||
$persistDriveLetter = $true -eq $State.Controls.chkDataPartitionPersistDriveLetter.IsChecked
|
||||
if ($fillRemaining -and ($State.Data.additionalDataPartitionsDataList | Where-Object { $_.FillRemaining } | Select-Object -First 1)) {
|
||||
[System.Windows.MessageBox]::Show("Only one data partition can fill the remaining VHDX space.", "Fill Remaining Already Used", "OK", "Warning") | Out-Null
|
||||
return $false
|
||||
@@ -975,11 +998,12 @@ function Add-AdditionalDataPartition {
|
||||
SizeGB = $sizeGbDisplay
|
||||
SizeBytes = $sizeBytes
|
||||
FillRemaining = $fillRemaining
|
||||
PersistDriveLetter = $persistDriveLetter
|
||||
FileSystem = 'NTFS'
|
||||
}
|
||||
|
||||
$State.Data.additionalDataPartitionsDataList.Add($newItem)
|
||||
WriteLog "Added additional data partition '$partitionName' (DriveLetter=$driveLetter, SizeBytes=$sizeBytes, FillRemaining=$fillRemaining)."
|
||||
WriteLog "Added additional data partition '$partitionName' (DriveLetter=$driveLetter, SizeBytes=$sizeBytes, FillRemaining=$fillRemaining, PersistDriveLetter=$persistDriveLetter)."
|
||||
Update-AdditionalDataPartitionsListView -State $State
|
||||
Clear-AdditionalDataPartitionForm -State $State
|
||||
return $true
|
||||
@@ -993,7 +1017,8 @@ function Add-PendingAdditionalDataPartition {
|
||||
|
||||
$hasPendingDataPartitionInput = -not [string]::IsNullOrWhiteSpace([string]$State.Controls.txtDataPartitionName.Text) -or
|
||||
-not [string]::IsNullOrWhiteSpace([string]$State.Controls.txtDataPartitionSizeGB.Text) -or
|
||||
($true -eq $State.Controls.chkDataPartitionFillRemaining.IsChecked)
|
||||
($true -eq $State.Controls.chkDataPartitionFillRemaining.IsChecked) -or
|
||||
($true -eq $State.Controls.chkDataPartitionPersistDriveLetter.IsChecked)
|
||||
|
||||
if (-not $hasPendingDataPartitionInput) {
|
||||
return $true
|
||||
@@ -1049,6 +1074,7 @@ function Get-AdditionalDataPartitionConfigRows {
|
||||
DriveLetter = $_.DriveLetter
|
||||
SizeBytes = [int64]$_.SizeBytes
|
||||
FillRemaining = [bool]$_.FillRemaining
|
||||
PersistDriveLetter = [bool]$_.PersistDriveLetter
|
||||
FileSystem = $_.FileSystem
|
||||
}
|
||||
})
|
||||
@@ -1105,6 +1131,16 @@ function Import-AdditionalDataPartitionsFromConfig {
|
||||
try { $fillRemaining = [System.Convert]::ToBoolean($partition.FillRemaining) } catch { $fillRemaining = $false }
|
||||
}
|
||||
|
||||
$persistDriveLetter = $false
|
||||
if ($partition.PSObject.Properties.Name -contains 'PersistDriveLetter') {
|
||||
try {
|
||||
$persistDriveLetter = [System.Convert]::ToBoolean($partition.PersistDriveLetter)
|
||||
}
|
||||
catch {
|
||||
throw "Data partition '$partitionName' PersistDriveLetter must be true or false."
|
||||
}
|
||||
}
|
||||
|
||||
[int64]$sizeBytes = 0
|
||||
if ($partition.PSObject.Properties.Name -contains 'SizeBytes') {
|
||||
[int64]::TryParse([string]$partition.SizeBytes, [ref]$sizeBytes) | Out-Null
|
||||
@@ -1120,6 +1156,7 @@ function Import-AdditionalDataPartitionsFromConfig {
|
||||
SizeGB = Get-PartitionSizeGBDisplay -SizeBytes $sizeBytes
|
||||
SizeBytes = $sizeBytes
|
||||
FillRemaining = $fillRemaining
|
||||
PersistDriveLetter = $persistDriveLetter
|
||||
FileSystem = $fileSystem
|
||||
})
|
||||
}
|
||||
|
||||
@@ -266,6 +266,7 @@ function Initialize-UIControls {
|
||||
$State.Controls.cmbDataPartitionDriveLetter = $window.FindName('cmbDataPartitionDriveLetter')
|
||||
$State.Controls.txtDataPartitionSizeGB = $window.FindName('txtDataPartitionSizeGB')
|
||||
$State.Controls.chkDataPartitionFillRemaining = $window.FindName('chkDataPartitionFillRemaining')
|
||||
$State.Controls.chkDataPartitionPersistDriveLetter = $window.FindName('chkDataPartitionPersistDriveLetter')
|
||||
$State.Controls.btnAddDataPartition = $window.FindName('btnAddDataPartition')
|
||||
$State.Controls.btnRemoveSelectedDataPartitions = $window.FindName('btnRemoveSelectedDataPartitions')
|
||||
$State.Controls.btnClearDataPartitions = $window.FindName('btnClearDataPartitions')
|
||||
@@ -895,6 +896,27 @@ function Initialize-DynamicUIElements {
|
||||
$fillRemainingColumn.CellTemplate = $fillRemainingTemplate
|
||||
$diskLayoutGridView.Columns.Add($fillRemainingColumn)
|
||||
|
||||
$persistDriveLetterColumn = New-Object System.Windows.Controls.GridViewColumn
|
||||
$persistDriveLetterColumn.Header = "Persist Drive Letter"
|
||||
$persistDriveLetterColumn.Width = 160
|
||||
$persistDriveLetterTemplate = New-Object System.Windows.DataTemplate
|
||||
$persistDriveLetterGridFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.Grid])
|
||||
$persistDriveLetterGridFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)
|
||||
$persistDriveLetterFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.CheckBox])
|
||||
$persistDriveLetterFactory.SetValue([System.Windows.Controls.Control]::ToolTipProperty, 'Keep this drive letter in the build VM and on devices deployed with FFU Builder.')
|
||||
$persistDriveLetterFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Center)
|
||||
$persistDriveLetterFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
|
||||
$persistDriveLetterBinding = New-Object System.Windows.Data.Binding("PersistDriveLetter")
|
||||
$persistDriveLetterBinding.Mode = [System.Windows.Data.BindingMode]::TwoWay
|
||||
$persistDriveLetterBinding.UpdateSourceTrigger = [System.Windows.Data.UpdateSourceTrigger]::PropertyChanged
|
||||
$persistDriveLetterFactory.SetBinding([System.Windows.Controls.Primitives.ToggleButton]::IsCheckedProperty, $persistDriveLetterBinding)
|
||||
$persistDriveLetterFactory.SetBinding([System.Windows.Controls.Control]::IsEnabledProperty, (New-Object System.Windows.Data.Binding("CanEditPersistDriveLetter")))
|
||||
$persistDriveLetterFactory.SetBinding([System.Windows.UIElement]::VisibilityProperty, (New-Object System.Windows.Data.Binding("PersistDriveLetterVisibility")))
|
||||
$persistDriveLetterGridFactory.AppendChild($persistDriveLetterFactory)
|
||||
$persistDriveLetterTemplate.VisualTree = $persistDriveLetterGridFactory
|
||||
$persistDriveLetterColumn.CellTemplate = $persistDriveLetterTemplate
|
||||
$diskLayoutGridView.Columns.Add($persistDriveLetterColumn)
|
||||
|
||||
Update-AdditionalDataPartitionsListView -State $State
|
||||
|
||||
# Apps Script Variables ListView setup
|
||||
|
||||
@@ -132,6 +132,235 @@ function Get-UnattendComputerNameValue {
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-FFUDataPartitionDriveLetterDeploymentContext {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$WindowsPartitionRoot
|
||||
)
|
||||
|
||||
$runtimeDirectory = Join-Path -Path $WindowsPartitionRoot -ChildPath 'Windows\Setup\Scripts\FFUDL'
|
||||
$runtimeScriptPath = Join-Path -Path $runtimeDirectory -ChildPath 'Apply.ps1'
|
||||
$manifestPath = Join-Path -Path $runtimeDirectory -ChildPath 'Manifest.json'
|
||||
if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
|
||||
if (Test-Path -LiteralPath $runtimeDirectory -PathType Container) {
|
||||
throw "Data partition drive-letter runtime directory exists without a manifest at $manifestPath."
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $runtimeScriptPath -PathType Leaf)) {
|
||||
throw "Data partition drive-letter manifest exists without its runtime script at $runtimeScriptPath."
|
||||
}
|
||||
|
||||
$manifest = Get-Content -LiteralPath $manifestPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
||||
if ([int]$manifest.SchemaVersion -ne 1) {
|
||||
throw "Unsupported data partition drive-letter manifest schema version '$($manifest.SchemaVersion)'."
|
||||
}
|
||||
if (@($manifest.Partitions).Count -eq 0) {
|
||||
throw 'Data partition drive-letter manifest does not contain any partitions.'
|
||||
}
|
||||
|
||||
$processorArchitecture = ([string]$manifest.ProcessorArchitecture).Trim().ToLowerInvariant()
|
||||
if ($processorArchitecture -notin @('amd64', 'arm64')) {
|
||||
throw "Unsupported data partition drive-letter processor architecture '$processorArchitecture'."
|
||||
}
|
||||
|
||||
WriteLog "Found data partition drive-letter persistence manifest with $(@($manifest.Partitions).Count) partition(s)."
|
||||
return [pscustomobject]@{
|
||||
RuntimeDirectory = $runtimeDirectory
|
||||
RuntimeScriptPath = $runtimeScriptPath
|
||||
ManifestPath = $manifestPath
|
||||
ProcessorArchitecture = $processorArchitecture
|
||||
}
|
||||
}
|
||||
|
||||
function Add-FFUDataPartitionDriveLetterCommandToAppliedUnattend {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$UnattendPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('amd64', 'arm64')]
|
||||
[string]$ProcessorArchitecture
|
||||
)
|
||||
|
||||
$unattendNamespace = 'urn:schemas-microsoft-com:unattend'
|
||||
$wcmNamespace = 'http://schemas.microsoft.com/WMIConfig/2002/State'
|
||||
$unattendXml = New-Object System.Xml.XmlDocument
|
||||
$unattendXml.PreserveWhitespace = $true
|
||||
if (-not (Test-Path -LiteralPath $UnattendPath -PathType Leaf)) {
|
||||
throw "Deployment unattend was not found at the expected path $UnattendPath."
|
||||
}
|
||||
$unattendXml.Load($UnattendPath)
|
||||
|
||||
$unattendRoot = $unattendXml.DocumentElement
|
||||
if ($null -eq $unattendRoot -or $unattendRoot.LocalName -ne 'unattend' -or $unattendRoot.NamespaceURI -ne $unattendNamespace) {
|
||||
throw "Unattend XML at $UnattendPath does not use the supported unattend root namespace."
|
||||
}
|
||||
|
||||
$namespaceManager = New-Object System.Xml.XmlNamespaceManager($unattendXml.NameTable)
|
||||
$namespaceManager.AddNamespace('un', $unattendNamespace)
|
||||
$specializeSettings = $unattendRoot.SelectSingleNode("un:settings[@pass='specialize']", $namespaceManager)
|
||||
if ($null -eq $specializeSettings) {
|
||||
$specializeSettings = $unattendXml.CreateElement('settings', $unattendNamespace)
|
||||
$null = $specializeSettings.SetAttribute('pass', 'specialize')
|
||||
$firstSettingsNode = $unattendRoot.SelectSingleNode('un:settings', $namespaceManager)
|
||||
if ($null -ne $firstSettingsNode) {
|
||||
$null = $unattendRoot.InsertBefore($specializeSettings, $firstSettingsNode)
|
||||
}
|
||||
else {
|
||||
$null = $unattendRoot.AppendChild($specializeSettings)
|
||||
}
|
||||
}
|
||||
|
||||
$deploymentComponents = @($specializeSettings.SelectNodes("un:component[@name='Microsoft-Windows-Deployment']", $namespaceManager) |
|
||||
Where-Object { $_.GetAttribute('processorArchitecture') -ieq $ProcessorArchitecture })
|
||||
if ($deploymentComponents.Count -gt 1) {
|
||||
throw "Unattend XML at $UnattendPath contains multiple Microsoft-Windows-Deployment components for $ProcessorArchitecture."
|
||||
}
|
||||
if ($deploymentComponents.Count -eq 0) {
|
||||
$deploymentComponent = $unattendXml.CreateElement('component', $unattendNamespace)
|
||||
$null = $deploymentComponent.SetAttribute('name', 'Microsoft-Windows-Deployment')
|
||||
$null = $deploymentComponent.SetAttribute('processorArchitecture', $ProcessorArchitecture)
|
||||
$null = $deploymentComponent.SetAttribute('publicKeyToken', '31bf3856ad364e35')
|
||||
$null = $deploymentComponent.SetAttribute('language', 'neutral')
|
||||
$null = $deploymentComponent.SetAttribute('versionScope', 'nonSxS')
|
||||
$null = $specializeSettings.AppendChild($deploymentComponent)
|
||||
}
|
||||
else {
|
||||
$deploymentComponent = $deploymentComponents[0]
|
||||
}
|
||||
|
||||
$runSynchronous = $deploymentComponent.SelectSingleNode('un:RunSynchronous', $namespaceManager)
|
||||
if ($null -eq $runSynchronous) {
|
||||
$runSynchronous = $unattendXml.CreateElement('RunSynchronous', $unattendNamespace)
|
||||
$null = $deploymentComponent.AppendChild($runSynchronous)
|
||||
}
|
||||
|
||||
$existingCommands = @($runSynchronous.SelectNodes('un:RunSynchronousCommand', $namespaceManager))
|
||||
$orderedExistingCommands = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||
$commandIndex = 0
|
||||
foreach ($existingCommand in $existingCommands) {
|
||||
$pathNode = $existingCommand.SelectSingleNode('un:Path', $namespaceManager)
|
||||
if ($null -ne $pathNode -and $pathNode.InnerText -match '(?i)\\FFUDL\\Apply\.ps1') {
|
||||
$null = $runSynchronous.RemoveChild($existingCommand)
|
||||
continue
|
||||
}
|
||||
|
||||
$orderValue = [int]::MaxValue
|
||||
$orderNode = $existingCommand.SelectSingleNode('un:Order', $namespaceManager)
|
||||
if ($null -ne $orderNode) {
|
||||
$parsedOrder = 0
|
||||
if ([int]::TryParse($orderNode.InnerText, [ref]$parsedOrder)) {
|
||||
$orderValue = $parsedOrder
|
||||
}
|
||||
}
|
||||
$orderedExistingCommands.Add([pscustomobject]@{ Node = $existingCommand; Order = $orderValue; Index = $commandIndex })
|
||||
$commandIndex++
|
||||
}
|
||||
|
||||
$nextOrder = 2
|
||||
foreach ($existingCommandInfo in @($orderedExistingCommands | Sort-Object -Property Order, Index)) {
|
||||
$orderNode = $existingCommandInfo.Node.SelectSingleNode('un:Order', $namespaceManager)
|
||||
if ($null -eq $orderNode) {
|
||||
$orderNode = $unattendXml.CreateElement('Order', $unattendNamespace)
|
||||
$null = $existingCommandInfo.Node.PrependChild($orderNode)
|
||||
}
|
||||
$orderNode.InnerText = [string]$nextOrder
|
||||
$nextOrder++
|
||||
}
|
||||
|
||||
$newCommand = $unattendXml.CreateElement('RunSynchronousCommand', $unattendNamespace)
|
||||
$actionAttribute = $unattendXml.CreateAttribute('wcm', 'action', $wcmNamespace)
|
||||
$actionAttribute.Value = 'add'
|
||||
$null = $newCommand.Attributes.Append($actionAttribute)
|
||||
$orderElement = $unattendXml.CreateElement('Order', $unattendNamespace)
|
||||
$orderElement.InnerText = '1'
|
||||
$null = $newCommand.AppendChild($orderElement)
|
||||
$descriptionElement = $unattendXml.CreateElement('Description', $unattendNamespace)
|
||||
$descriptionElement.InnerText = 'Apply FFU data partition drive letters'
|
||||
$null = $newCommand.AppendChild($descriptionElement)
|
||||
$pathElement = $unattendXml.CreateElement('Path', $unattendNamespace)
|
||||
$specializeCommand = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\Windows\Setup\Scripts\FFUDL\Apply.ps1" -ManifestPath "C:\Windows\Setup\Scripts\FFUDL\Manifest.json" -Phase Specialize'
|
||||
if ($specializeCommand.Length -gt 259) {
|
||||
throw "Data partition drive-letter specialize command exceeds the 259-character unattend Path limit. Length: $($specializeCommand.Length)."
|
||||
}
|
||||
$pathElement.InnerText = $specializeCommand
|
||||
$null = $newCommand.AppendChild($pathElement)
|
||||
$willRebootElement = $unattendXml.CreateElement('WillReboot', $unattendNamespace)
|
||||
$willRebootElement.InnerText = 'OnRequest'
|
||||
$null = $newCommand.AppendChild($willRebootElement)
|
||||
if ($null -ne $runSynchronous.FirstChild) {
|
||||
$null = $runSynchronous.InsertBefore($newCommand, $runSynchronous.FirstChild)
|
||||
}
|
||||
else {
|
||||
$null = $runSynchronous.AppendChild($newCommand)
|
||||
}
|
||||
|
||||
$unattendXml.Save($UnattendPath)
|
||||
WriteLog "Merged data partition drive-letter specialize command into $UnattendPath."
|
||||
}
|
||||
|
||||
function Test-FFUDataPartitionDriveLetterCommandInAppliedUnattend {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$UnattendPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('amd64', 'arm64')]
|
||||
[string]$ProcessorArchitecture
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $UnattendPath -PathType Leaf)) {
|
||||
throw "Data partition drive-letter deployment unattend was not found at $UnattendPath."
|
||||
}
|
||||
|
||||
$unattendNamespace = 'urn:schemas-microsoft-com:unattend'
|
||||
$wcmNamespace = 'http://schemas.microsoft.com/WMIConfig/2002/State'
|
||||
$unattendXml = New-Object System.Xml.XmlDocument
|
||||
$unattendXml.Load($UnattendPath)
|
||||
if ($null -eq $unattendXml.DocumentElement -or $unattendXml.DocumentElement.NamespaceURI -ne $unattendNamespace) {
|
||||
throw "Unattend XML at $UnattendPath does not use the supported unattend root namespace."
|
||||
}
|
||||
|
||||
$namespaceManager = New-Object System.Xml.XmlNamespaceManager($unattendXml.NameTable)
|
||||
$namespaceManager.AddNamespace('un', $unattendNamespace)
|
||||
$deploymentComponents = @($unattendXml.SelectNodes("//un:settings[@pass='specialize']/un:component[@name='Microsoft-Windows-Deployment']", $namespaceManager) |
|
||||
Where-Object { $_.GetAttribute('processorArchitecture') -ieq $ProcessorArchitecture })
|
||||
if ($deploymentComponents.Count -ne 1) {
|
||||
throw "Expected exactly one specialize Microsoft-Windows-Deployment component for $ProcessorArchitecture in $UnattendPath."
|
||||
}
|
||||
|
||||
$commands = @($deploymentComponents[0].SelectNodes('un:RunSynchronous/un:RunSynchronousCommand', $namespaceManager))
|
||||
$driveLetterCommands = @($commands | Where-Object {
|
||||
$pathNode = $_.SelectSingleNode('un:Path', $namespaceManager)
|
||||
$null -ne $pathNode -and $pathNode.InnerText -match '(?i)\\FFUDL\\Apply\.ps1'
|
||||
})
|
||||
if ($driveLetterCommands.Count -ne 1) {
|
||||
throw "Expected exactly one data partition drive-letter specialize command in $UnattendPath."
|
||||
}
|
||||
|
||||
$driveLetterCommand = $driveLetterCommands[0]
|
||||
$orderNode = $driveLetterCommand.SelectSingleNode('un:Order', $namespaceManager)
|
||||
$willRebootNode = $driveLetterCommand.SelectSingleNode('un:WillReboot', $namespaceManager)
|
||||
if ($null -eq $orderNode -or $orderNode.InnerText -ne '1') {
|
||||
throw "Data partition drive-letter specialize command is not first in $UnattendPath."
|
||||
}
|
||||
if ($null -eq $willRebootNode -or $willRebootNode.InnerText -ne 'OnRequest') {
|
||||
throw "Data partition drive-letter specialize command does not use WillReboot=OnRequest in $UnattendPath."
|
||||
}
|
||||
if ($driveLetterCommand.GetAttribute('action', $wcmNamespace) -ne 'add') {
|
||||
throw "Data partition drive-letter specialize command does not use wcm:action=add in $UnattendPath."
|
||||
}
|
||||
foreach ($otherCommand in @($commands | Where-Object { $_ -ne $driveLetterCommand })) {
|
||||
$otherOrderNode = $otherCommand.SelectSingleNode('un:Order', $namespaceManager)
|
||||
$otherOrder = 0
|
||||
if ($null -eq $otherOrderNode -or -not [int]::TryParse($otherOrderNode.InnerText, [ref]$otherOrder) -or $otherOrder -le 1) {
|
||||
throw "Another specialize command conflicts with first order in $UnattendPath."
|
||||
}
|
||||
}
|
||||
|
||||
WriteLog "Verified data partition drive-letter specialize command in $UnattendPath."
|
||||
}
|
||||
|
||||
function Test-LegacyPromptComputerName($computername) {
|
||||
if ([string]::IsNullOrWhiteSpace($computername)) {
|
||||
return $false
|
||||
@@ -1603,6 +1832,15 @@ if ($null -eq $windowsVolume) {
|
||||
}
|
||||
WriteLog "Successfully assigned drive letter 'W'."
|
||||
|
||||
$dataPartitionDriveLetterDeploymentContext = $null
|
||||
try {
|
||||
$dataPartitionDriveLetterDeploymentContext = Get-FFUDataPartitionDriveLetterDeploymentContext -WindowsPartitionRoot 'W:\'
|
||||
}
|
||||
catch {
|
||||
WriteLog "Validating data partition drive-letter deployment artifacts failed with error: $_"
|
||||
Stop-Script -Message "Validating data partition drive-letter deployment artifacts failed with error: $_"
|
||||
}
|
||||
|
||||
$recoveryPartition = Get-Partition -DiskNumber $DiskID | Where-Object Type -eq Recovery | Select-Object -First 1
|
||||
if ($recoveryPartition) {
|
||||
WriteLog 'Setting recovery partition attributes'
|
||||
@@ -1701,6 +1939,25 @@ If ($Unattend) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -ne $dataPartitionDriveLetterDeploymentContext) {
|
||||
$deploymentUnattendPath = 'W:\Windows\Panther\Unattend.xml'
|
||||
try {
|
||||
if ($Unattend) {
|
||||
WriteLog "Merging data partition drive-letter persistence into custom unattend at $deploymentUnattendPath."
|
||||
Add-FFUDataPartitionDriveLetterCommandToAppliedUnattend -UnattendPath $deploymentUnattendPath -ProcessorArchitecture $dataPartitionDriveLetterDeploymentContext.ProcessorArchitecture
|
||||
}
|
||||
else {
|
||||
WriteLog "No custom unattend was supplied. Verifying the FFU-embedded data partition drive-letter command at $deploymentUnattendPath."
|
||||
}
|
||||
|
||||
Test-FFUDataPartitionDriveLetterCommandInAppliedUnattend -UnattendPath $deploymentUnattendPath -ProcessorArchitecture $dataPartitionDriveLetterDeploymentContext.ProcessorArchitecture
|
||||
}
|
||||
catch {
|
||||
WriteLog "Preparing data partition drive-letter deployment hook failed with error: $_"
|
||||
Stop-Script -Message "Preparing data partition drive-letter deployment hook failed with error: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# Add Drivers
|
||||
if ($null -ne $DriverSourcePath) {
|
||||
Write-SectionHeader -Title 'Installing Drivers'
|
||||
|
||||
@@ -250,6 +250,11 @@ A cached VHDX is reused only when the cache metadata matches your current build
|
||||
- Logical sector size (512 vs 4096)
|
||||
- Optional features selection
|
||||
- The exact set of update payload file names downloaded for that run (SSU/CU/.NET/etc.)
|
||||
- Disk size, Recovery layout, and the ordered data-partition name, label, file system, size, and **Fill Remaining** settings
|
||||
|
||||
System, Windows, Recovery, and data drive letters do not affect cache matching. The per-data-partition **Persist Drive Letter** setting also does not affect matching. On a cache hit, FFU Builder copies the cached base and applies the current host build letters only to that working copy.
|
||||
|
||||
Cache metadata from the earlier drive-letter-aware schema is skipped rather than migrated or deleted. The first build after this change may create one replacement cache. Remove older VHDX and config pairs manually after confirming the replacement is usable.
|
||||
|
||||
#### Disk Usage and Cleanup
|
||||
|
||||
@@ -261,6 +266,20 @@ VHDX caching trades disk space for speed. The `VHDXCache` folder can grow over t
|
||||
>
|
||||
> To force a full rebuild, delete the contents of `$FFUDevelopmentPath\VHDXCache` (or disable **Allow VHDX Caching**) and run the build again.
|
||||
|
||||
### Persisted Data Drive Letters
|
||||
|
||||
Data partitions can opt in to **Persist Drive Letter** in **Disk Layout**. The option is off by default, and data letters are limited to `D:` through `Z:`.
|
||||
|
||||
For builds that install applications in a VM, FFU Builder applies and verifies opted-in letters in audit mode before it searches for the Apps ISO or runs application scripts. The host requires an audit success marker before it captures the VHDX. For builds without a VM, the same deployment files are added directly to the working VHDX before capture.
|
||||
|
||||
On a deployed device, FFU Builder inserts the assignment command first in `Microsoft-Windows-Deployment\RunSynchronous` for the Windows `specialize` pass. Microsoft documents that these commands run in order and in system context during `specialize`; see [RunSynchronous](https://learn.microsoft.com/windows-hardware/customize/desktop/unattend/microsoft-windows-deployment-runsynchronous). The command uses `WillReboot=OnRequest`, returns `0` only after assignment and cleanup succeed, and returns `3` on failure. Microsoft documents that with `OnRequest`, return codes other than `0`, `1`, or `2` terminate installation; see [WillReboot](https://learn.microsoft.com/windows-hardware/customize/desktop/unattend/microsoft-windows-deployment-runsynchronous-runsynchronouscommand-willreboot).
|
||||
|
||||
An optimized FFU can resize the Windows partition or the partition selected by `-OptimizeFFUPartitionNumber` when it is applied to a differently sized drive. If an opted-in data partition is the selected resize target, drive-letter enforcement accepts its deployed size while still requiring its GPT identity, partition number, volume label, and file system to match the captured partition. Other data partitions must retain their captured sizes. See [Optimize an FFU](https://learn.microsoft.com/windows-hardware/manufacture/desktop/deploy-windows-using-full-flash-update--ffu?view=windows-11#optimize-an-ffu).
|
||||
|
||||
All requested-letter conflicts are fatal. FFU Builder does not move an Apps ISO, deployment USB, existing fixed volume, or another partition, and it does not select a substitute letter. A successful deployed run removes the dedicated runtime directory, script, manifest, and markers. It leaves only `C:\Windows\Temp\FFUDataPartitionDriveLetters.log`. A failed run retains the runtime inputs and failure marker for diagnosis.
|
||||
|
||||
When FFU Builder deployment media applies the image, `ApplyFFU.ps1` treats PE letters as temporary. If a custom deployment unattend is copied from USB, the script merges the persistence command into that final Panther answer file and preserves its other settings. Without a custom unattend, it verifies that the command embedded in the FFU is still present. Builds with no opted-in data partitions add no persistence directory, manifest, markers, or unattend command.
|
||||
|
||||
### Create Deployment Media
|
||||
|
||||
Controls the `-CreateDeploymentMedia` parameter.
|
||||
|
||||
@@ -47,7 +47,7 @@ Configures the VHDX size and the build-time partition layout used for the captur
|
||||
|
||||
The partition list shows the build order: System, MSR, Windows, Recovery, and optional data partitions. System, Windows, and Recovery have editable build-time drive letters. Defaults are `S`, `W`, and `R`. The MSR row is display-only, fixed at 16MB, and does not use a drive letter.
|
||||
|
||||
These drive letters only affect FFU creation. The deployment script discovers the Windows and Recovery partitions from the applied disk instead of relying on fixed partition numbers.
|
||||
The System, Windows, and Recovery selections are host-side build letters only. Installed Windows uses `C:` for its Windows partition, while System and Recovery normally have no letter. The deployment script discovers these partitions from the applied disk instead of relying on the build letters.
|
||||
|
||||
Leave the Windows size blank to let Windows fill the remaining disk. Set a fixed Windows size before adding data partitions so the VHDX has space left for Recovery and data volumes.
|
||||
|
||||
@@ -55,7 +55,11 @@ Use the Recovery size only when you need a fixed Recovery partition size. Leave
|
||||
|
||||
The Recovery partition can be removed by selecting its row checkbox and using **Remove Selected**. Use **Restore Recovery** to add it back before saving or building. Removing Recovery saves `CreateRecoveryPartition` as `false` in the generated config.
|
||||
|
||||
Each data partition has a name, build-time drive letter, and either a size in GB or **Fill Remaining**. Only one data partition can use **Fill Remaining**. Data partitions can be reordered with the arrow buttons. **Clear** removes only data partitions, not the base partition rows.
|
||||
Each data partition has a name, a drive letter from `D:` through `Z:`, and either a size in GB or **Fill Remaining**. Only one data partition can use **Fill Remaining**. Data partitions can be reordered with the arrow buttons. **Clear** removes only data partitions, not the base partition rows.
|
||||
|
||||
**Persist Drive Letter** is off by default. When selected for a data partition, FFU Builder requires that partition to use its configured letter before build-VM application scripts run and when a deployed device enters the Windows `specialize` pass. If another volume already owns the requested letter, the build or deployment stops instead of moving that volume or selecting another letter.
|
||||
|
||||
Windows PE drive letters are temporary and can change when hardware is detected, so assigning a letter only in PE does not provide the installed-Windows guarantee. See [WinPE: Identify drive letters with a script](https://learn.microsoft.com/windows-hardware/manufacture/desktop/winpe-identify-drive-letters?view=windows-11). Successful first-boot enforcement removes its temporary script and manifest and leaves the diagnostic log at `C:\Windows\Temp\FFUDataPartitionDriveLetters.log`.
|
||||
|
||||
The Apps ISO drive letter is discovered at runtime. If you create a data partition that uses `D:`, application installs should use `%FFUAppsRoot%` for Apps ISO paths. Legacy `D:\` paths in `UserAppList.json` are still supported when they point to files on the Apps ISO.
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ This table lists all top-level parameters in BuildFFUVM.ps1.
|
||||
| Parameter | Type | UI Control | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| -AdditionalFFUFiles | string[] | Copy Additional FFU Files + Additional FFU Files list | Array of full file paths to existing FFU files that should also be copied to the deployment USB when -CopyAdditionalFFUFiles is set to $true. |
|
||||
| -AdditionalDataPartitions | object[] | Disk Layout | Creates optional data partitions after the base Windows layout. Recovery normally remains before data partitions unless -CreateRecoveryPartition is $false. Each item supports Name, Label, DriveLetter, SizeBytes or SizeGB, FillRemaining, and FileSystem. Only one item can use FillRemaining. |
|
||||
| -AdditionalDataPartitions | object[] | Disk Layout | Creates optional data partitions after the base Windows layout. Recovery normally remains before data partitions unless -CreateRecoveryPartition is $false. Each item supports Name, Label, DriveLetter, SizeBytes or SizeGB, FillRemaining, FileSystem, and PersistDriveLetter. DriveLetter must be D through Z, only one item can use FillRemaining, and PersistDriveLetter defaults to $false. |
|
||||
| -AllowExternalHardDiskMedia | bool | Allow External Hard Disk Media | When set to $true, will allow the use of media identified as External Hard Disk media via WMI class Win32_DiskDrive. Default is not defined. |
|
||||
| -AllowVHDXCaching | bool | Allow VHDX Caching | When set to $true, will cache the VHDX file to the $FFUDevelopmentPath\VHDXCache folder and create a config json file that will keep track of the Windows build information, the updates installed, and the logical sector byte size information. Default is $false. |
|
||||
| -AppListPath | string | AppList.json Path | Path to a JSON file containing a list of applications to install using WinGet. Default is $FFUDevelopmentPath\Apps\AppList.json. |
|
||||
@@ -112,4 +112,20 @@ This table lists all top-level parameters in BuildFFUVM.ps1.
|
||||
| -WindowsVersion | string | Windows Version | String value of the Windows version to download. This is used to identify which version of Windows to download. Default is '25h2'. |
|
||||
{: .parameters-reference-table }
|
||||
|
||||
{% include page_nav.html %}
|
||||
## AdditionalDataPartitions Items
|
||||
|
||||
Each item supports the following fields:
|
||||
|
||||
| Field | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `Name` | Yes | Partition name used in configuration and logs. |
|
||||
| `Label` | No | Volume label. Defaults to `Name`. |
|
||||
| `DriveLetter` | Yes | Build letter from `D` through `Z`, without a colon. It must be unique across the complete build layout. |
|
||||
| `SizeBytes` or `SizeGB` | Conditional | Fixed partition size. Omit only when `FillRemaining` is `$true`. |
|
||||
| `FillRemaining` | No | Uses the remaining VHDX space. Default is `$false`; only one data partition can enable it. |
|
||||
| `FileSystem` | No | `NTFS` or `ReFS`. Default is `NTFS`. |
|
||||
| `PersistDriveLetter` | No | When `$true`, requires the selected data letter in the build VM and deployed Windows. Default is `$false`. Any occupied-letter conflict stops the build or deployment. |
|
||||
|
||||
`PersistDriveLetter` does not change VHDX cache compatibility. It controls per-build runtime artifacts that are added only to the working VHDX after the reusable base has been cached or copied.
|
||||
|
||||
{% include page_nav.html %}
|
||||
|
||||
Reference in New Issue
Block a user