Compare commits

..

14 Commits

Author SHA1 Message Date
rbalsleyMSFT 56a2597818 Consolidate WinGet version detection
Add a shared WinGet component status helper that uses Get-WinGetVersion
through Microsoft.WinGet.Client, and route both the CLI and UI status checks
through it. This removes the UI-only winget.exe --version parsing path and
adds clearer logging for missing modules, missing cmdlets, parse failures,
and caught WinGet version errors.
2026-06-05 13:35:07 -07:00
rbalsleyMSFT 895728ebe8 Prefer DeviceGroup arch for Dell client drivers
Use DeviceGroup OperatingSystem osArch when selecting Dell client driver packages, falling back to SupportedOperatingSystems when DeviceGroup metadata is absent. This keeps existing server catalog behavior unchanged.
2026-06-05 10:41:00 -07:00
rbalsleyMSFT 9fb9e81701 Add configurable FFU build partition drive letters
Add System, Windows, and Recovery partition drive-letter settings to the Hyper-V Settings UI, config save/load, BuildFFUVM parameters, and sample/docs. Defaults remain S, W, and R.

Validate selected letters early, log validation failures for the UI, include drive-letter metadata in VHDX cache matching, and normalize legacy malformed cache values. This only affects FFU build-time partitioning; ApplyFFU deployment letters remain unchanged.
2026-06-05 09:46:00 -07:00
rbalsleyMSFT c32cb93434 Fix stale WinGetWin32Apps.json handling
Treat WinGetWin32Apps.json as generated app orchestration output so failed or cancelled builds do not restore stale install metadata. Reuse Apps.iso when app inputs have not changed, and refresh it only when current app content or orchestration inputs are newer.
2026-06-04 16:54:16 -07:00
rbalsleyMSFT 7decd8f1ba Merge pull request #495 from ConfigMatt/HyperV-Deployment-Instructions
Added HyperV Deployment instructions to quickstart documentation
2026-05-29 15:43:28 -07:00
rbalsleyMSFT 75f4025cf1 Merge pull request #378 from iambdud/add-try-catch-for-creating-vm
adding a simple try/catch for cases where the VM fails to start
2026-05-29 15:39:06 -07:00
rbalsleyMSFT f750cd872a Merge pull request #380 from dodexahedron/fix-winget-check-fail-if-multiple-versions-present
Fix failure on winget check if multiple versions exist
2026-05-29 14:55:18 -07:00
Matt Atkinson 9de2b30c93 Added HyperV Deployment instructions to quickstart documentation 2026-05-22 22:33:55 -07:00
rbalsleyMSFT e1c6259021 Processes unattend from a temp copy
Copies the source answer file to local temp storage before use, preserving the original media file and failing fast if the working copy cannot be prepared.
2026-04-28 13:10:48 -07:00
Brandon Thetford d0e17eb0f7 No need to double-assign 🫣 2026-01-15 15:21:21 -07:00
Brandon Thetford e8aa1b5b4a Get instead of import, as an array, and get the best available 2026-01-15 15:19:12 -07:00
Brandon Thetford b21cb0bbf6 Actually include the global parameter 2026-01-15 14:58:11 -07:00
Brandon Thetford 372806e5aa Fix failure on winget check if multiple versions exist
Also imports the module at this step, globally
2026-01-15 14:53:30 -07:00
iambdud fafe28baf7 adding a simple try/catch for cases where the VM fails to start 2026-01-15 14:52:45 -06:00
17 changed files with 604 additions and 120 deletions
+195 -33
View File
@@ -147,6 +147,15 @@ Path to the Windows 10/11 ISO file.
.PARAMETER LogicalSectorSizeBytes .PARAMETER LogicalSectorSizeBytes
UInt32 value of 512 or 4096. Useful for 4Kn drives or devices shipping with UFS drives. Default is 512. UInt32 value of 512 or 4096. Useful for 4Kn drives or devices shipping with UFS drives. Default is 512.
.PARAMETER SystemPartitionDriveLetter
Drive letter used for the System partition while building the FFU VHDX. Default is S.
.PARAMETER WindowsPartitionDriveLetter
Drive letter used for the Windows partition while building the FFU VHDX. Default is W.
.PARAMETER RecoveryPartitionDriveLetter
Drive letter used for the Recovery partition while building the FFU VHDX. Default is R.
.PARAMETER Make .PARAMETER Make
Make of the device to download drivers. Accepted values are: 'Microsoft', 'Dell', 'HP', 'Lenovo'. Make of the device to download drivers. Accepted values are: 'Microsoft', 'Dell', 'HP', 'Lenovo'.
@@ -411,6 +420,9 @@ param(
[string]$MediaType = 'consumer', [string]$MediaType = 'consumer',
[ValidateSet(512, 4096)] [ValidateSet(512, 4096)]
[uint32]$LogicalSectorSizeBytes = 512, [uint32]$LogicalSectorSizeBytes = 512,
[string]$SystemPartitionDriveLetter = 'S',
[string]$WindowsPartitionDriveLetter = 'W',
[string]$RecoveryPartitionDriveLetter = 'R',
[bool]$Optimize = $true, [bool]$Optimize = $true,
[string]$DriversJsonPath, [string]$DriversJsonPath,
[bool]$CompressDownloadedDriversToWim = $false, [bool]$CompressDownloadedDriversToWim = $false,
@@ -866,6 +878,9 @@ class VhdxCacheItem {
[string]$VhdxFileName = "" [string]$VhdxFileName = ""
[uint32]$LogicalSectorSizeBytes = "" [uint32]$LogicalSectorSizeBytes = ""
[uint64]$Disksize = "" [uint64]$Disksize = ""
[string]$SystemPartitionDriveLetter = ""
[string]$WindowsPartitionDriveLetter = ""
[string]$RecoveryPartitionDriveLetter = ""
[string]$WindowsSKU = "" [string]$WindowsSKU = ""
[string]$WindowsRelease = "" [string]$WindowsRelease = ""
[string]$WindowsVersion = "" [string]$WindowsVersion = ""
@@ -2744,6 +2759,42 @@ function New-AppsISO {
$AppsPath = '\\?\' + $AppsPath $AppsPath = '\\?\' + $AppsPath
Invoke-Process $OSCDIMG "-n -m -d $Appspath $AppsISO" | Out-Null Invoke-Process $OSCDIMG "-n -m -d $Appspath $AppsISO" | Out-Null
} }
function Test-AppsIsoRefreshRequired {
param(
[string]$AppsISOPath,
[string]$AppsPath,
[string[]]$InputPaths = @()
)
if ([string]::IsNullOrWhiteSpace($AppsISOPath) -or -not (Test-Path -Path $AppsISOPath -PathType Leaf)) {
return $true
}
$appsIsoLastWriteUtc = (Get-Item -Path $AppsISOPath).LastWriteTimeUtc
foreach ($inputPath in $InputPaths) {
if ([string]::IsNullOrWhiteSpace($inputPath) -or -not (Test-Path -Path $inputPath)) { continue }
$inputItem = Get-Item -Path $inputPath -Force
if ($inputItem.LastWriteTimeUtc -gt $appsIsoLastWriteUtc) {
WriteLog "Apps ISO refresh required because $($inputItem.FullName) is newer than $AppsISOPath"
return $true
}
}
if (-not [string]::IsNullOrWhiteSpace($AppsPath) -and (Test-Path -Path $AppsPath -PathType Container)) {
$newerAppInput = Get-ChildItem -Path $AppsPath -File -Force -Recurse -ErrorAction SilentlyContinue | Where-Object {
$_.LastWriteTimeUtc -gt $appsIsoLastWriteUtc
} | Select-Object -First 1
if ($null -ne $newerAppInput) {
WriteLog "Apps ISO refresh required because $($newerAppInput.FullName) is newer than $AppsISOPath"
return $true
}
}
return $false
}
function Get-WimFromISO { function Get-WimFromISO {
#Mount ISO, get Wim file #Mount ISO, get Wim file
$mountResult = Mount-DiskImage -ImagePath $isoPath -PassThru $mountResult = Mount-DiskImage -ImagePath $isoPath -PassThru
@@ -3012,21 +3063,80 @@ function New-ScratchVhdx {
Writelog "Done." Writelog "Done."
return $toReturn return $toReturn
} }
function Get-NormalizedPartitionDriveLetters {
param(
[string]$SystemPartitionDriveLetter,
[string]$WindowsPartitionDriveLetter,
[string]$RecoveryPartitionDriveLetter,
[switch]$ValidateAvailable
)
$requestedLetters = [ordered]@{
SystemPartitionDriveLetter = $SystemPartitionDriveLetter
WindowsPartitionDriveLetter = $WindowsPartitionDriveLetter
RecoveryPartitionDriveLetter = $RecoveryPartitionDriveLetter
}
$normalizedLetters = [ordered]@{}
foreach ($entry in $requestedLetters.GetEnumerator()) {
$driveLetter = ([string]$entry.Value).Trim().TrimEnd(':').ToUpperInvariant()
if ([string]::IsNullOrWhiteSpace($driveLetter) -or $driveLetter -notmatch '^[A-Z]$') {
throw "$($entry.Key) must be a single drive letter from A to Z without a colon."
}
$normalizedLetters[$entry.Key] = $driveLetter
}
$duplicateLetters = @($normalizedLetters.Values | Group-Object | Where-Object { $_.Count -gt 1 })
if ($duplicateLetters.Count -gt 0) {
$duplicateLetterList = ($duplicateLetters | ForEach-Object { $_.Name }) -join ', '
throw "System, Windows, and Recovery partition drive letters must be unique. Duplicate value(s): $duplicateLetterList."
}
if ($ValidateAvailable) {
foreach ($entry in $normalizedLetters.GetEnumerator()) {
$existingDrive = Get-PSDrive -Name $entry.Value -PSProvider FileSystem -ErrorAction SilentlyContinue
if ($null -ne $existingDrive) {
throw "$($entry.Key) uses drive letter $($entry.Value), but that letter is already assigned on this host. Choose an unused drive letter."
}
}
}
return [pscustomobject]$normalizedLetters
}
function Get-PartitionDriveLetterCacheValue {
param(
[object]$DriveLetterValue
)
$driveLetter = ([string]$DriveLetterValue).Trim().TrimEnd(':').ToUpperInvariant()
if ($driveLetter -match '^[A-Z]$') {
return $driveLetter
}
$trailingDriveLetter = [regex]::Match($driveLetter, '(?i)(?:^|[^A-Z])([A-Z])$')
if ($trailingDriveLetter.Success) {
return $trailingDriveLetter.Groups[1].Value.ToUpperInvariant()
}
return $driveLetter
}
#Add System Partition #Add System Partition
function New-SystemPartition { function New-SystemPartition {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[ciminstance]$VhdxDisk, [ciminstance]$VhdxDisk,
[string]$DriveLetter = 'S',
[uint64]$SystemPartitionSize = 260MB [uint64]$SystemPartitionSize = 260MB
) )
WriteLog "Creating System partition..." WriteLog "Creating System partition..."
$sysPartition = $VhdxDisk | New-Partition -DriveLetter 'S' -Size $SystemPartitionSize -GptType "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" -IsHidden $sysPartition = $VhdxDisk | New-Partition -DriveLetter $DriveLetter -Size $SystemPartitionSize -GptType "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" -IsHidden
$sysPartition | Format-Volume -FileSystem FAT32 -Force -NewFileSystemLabel "System" $sysPartition | Format-Volume -FileSystem FAT32 -Force -NewFileSystemLabel "System" | Out-Null
WriteLog 'Done.' WriteLog 'Done.'
return $sysPartition.DriveLetter return [string]$sysPartition.DriveLetter
} }
#Add MSRPartition #Add MSRPartition
function New-MSRPartition { function New-MSRPartition {
@@ -3052,16 +3162,17 @@ function New-OSPartition {
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$WimPath, [string]$WimPath,
[uint32]$WimIndex, [uint32]$WimIndex,
[string]$DriveLetter = 'W',
[uint64]$OSPartitionSize = 0 [uint64]$OSPartitionSize = 0
) )
WriteLog "Creating OS partition..." WriteLog "Creating OS partition..."
if ($OSPartitionSize -gt 0) { if ($OSPartitionSize -gt 0) {
$osPartition = $vhdxDisk | New-Partition -DriveLetter 'W' -Size $OSPartitionSize -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}" $osPartition = $vhdxDisk | New-Partition -DriveLetter $DriveLetter -Size $OSPartitionSize -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}"
} }
else { else {
$osPartition = $vhdxDisk | New-Partition -DriveLetter 'W' -UseMaximumSize -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}" $osPartition = $vhdxDisk | New-Partition -DriveLetter $DriveLetter -UseMaximumSize -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}"
} }
$osPartition | Format-Volume -FileSystem NTFS -Confirm:$false -Force -NewFileSystemLabel "Windows" $osPartition | Format-Volume -FileSystem NTFS -Confirm:$false -Force -NewFileSystemLabel "Windows"
@@ -3093,6 +3204,7 @@ function New-RecoveryPartition {
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
$OsPartition, $OsPartition,
[uint64]$RecoveryPartitionSize = 0, [uint64]$RecoveryPartitionSize = 0,
[string]$DriveLetter = 'R',
[ciminstance]$DataPartition [ciminstance]$DataPartition
) )
@@ -3127,7 +3239,7 @@ function New-RecoveryPartition {
WriteLog "OS partition shrunk by $calculatedRecoverySize bytes for Recovery partition." WriteLog "OS partition shrunk by $calculatedRecoverySize bytes for Recovery partition."
} }
$recoveryPartition = $VhdxDisk | New-Partition -DriveLetter 'R' -UseMaximumSize -GptType "{de94bba4-06d1-4d40-a16a-bfd50179d6ac}" ` $recoveryPartition = $VhdxDisk | New-Partition -DriveLetter $DriveLetter -UseMaximumSize -GptType "{de94bba4-06d1-4d40-a16a-bfd50179d6ac}" `
| Format-Volume -FileSystem NTFS -Confirm:$false -Force -NewFileSystemLabel 'Recovery' | Format-Volume -FileSystem NTFS -Confirm:$false -Force -NewFileSystemLabel 'Recovery'
WriteLog "Done. Recovery partition at drive $($recoveryPartition.DriveLetter):" WriteLog "Done. Recovery partition at drive $($recoveryPartition.DriveLetter):"
@@ -3248,7 +3360,13 @@ function New-FFUVM {
& vmconnect localhost "$VMName" & vmconnect localhost "$VMName"
#Start VM #Start VM
Start-VM -Name $VMName try{
Start-VM -Name $VMName -ErrorAction Stop
}
catch{
WriteLog "Error starting VM: $_"
throw $_
}
return $VM return $VM
} }
@@ -5062,15 +5180,6 @@ function New-RunSession {
WriteLog "Backed up DriverMapping.json to $backup" WriteLog "Backed up DriverMapping.json to $backup"
} }
} }
if ($OrchestrationPath) {
$wgPath = Join-Path $OrchestrationPath 'WinGetWin32Apps.json'
if (Test-Path $wgPath) {
$backup2 = Join-Path $backupDir 'WinGetWin32Apps.json'
Copy-Item -Path $wgPath -Destination $backup2 -Force
$manifest.JsonBackups += @{ Path = $wgPath; Backup = $backup2 }
WriteLog "Backed up WinGetWin32Apps.json to $backup2"
}
}
# Backup Office XMLs (DeployFFU.xml, DownloadFFU.xml) if present so we can restore them after cleanup # Backup Office XMLs (DeployFFU.xml, DownloadFFU.xml) if present so we can restore them after cleanup
if ($OfficePath) { if ($OfficePath) {
foreach ($n in @('DeployFFU.xml', 'DownloadFFU.xml')) { foreach ($n in @('DeployFFU.xml', 'DownloadFFU.xml')) {
@@ -5669,6 +5778,7 @@ function Cleanup-CurrentRunDownloads {
'Update-Edge.ps1', 'Update-Edge.ps1',
'Install-Office.ps1', 'Install-Office.ps1',
'Install-LTSCUpdate.ps1', 'Install-LTSCUpdate.ps1',
'WinGetWin32Apps.json',
'AppsScriptVariables.json' 'AppsScriptVariables.json'
) )
@@ -5843,7 +5953,6 @@ if ($ExportConfigFile) {
####### End Generate Config File ####### ####### End Generate Config File #######
#Setting long path support - this prevents issues where some applications have deep directory structures #Setting long path support - this prevents issues where some applications have deep directory structures
#and oscdimg fails to create the Apps ISO #and oscdimg fails to create the Apps ISO
try { try {
@@ -5862,6 +5971,21 @@ if ($LongPathsEnabled -ne 1) {
Set-Progress -Percentage 2 -Message "Validating parameters..." Set-Progress -Percentage 2 -Message "Validating parameters..."
###PARAMETER VALIDATION ###PARAMETER VALIDATION
#Set build partition drive letters and validate they are available for use; this is required before any build steps that require drive access to ensure the expected drive letters are reserved and to fail fast if there are conflicts.
try {
$partitionDriveLetters = Get-NormalizedPartitionDriveLetters -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -ValidateAvailable
$SystemPartitionDriveLetter = $partitionDriveLetters.SystemPartitionDriveLetter
$WindowsPartitionDriveLetter = $partitionDriveLetters.WindowsPartitionDriveLetter
$RecoveryPartitionDriveLetter = $partitionDriveLetters.RecoveryPartitionDriveLetter
WriteLog "Using build partition drive letters: System=$SystemPartitionDriveLetter, Windows=$WindowsPartitionDriveLetter, Recovery=$RecoveryPartitionDriveLetter"
}
catch {
$partitionDriveLetterValidationError = "Build validation failed: $($_.Exception.Message)"
Set-Progress -Percentage 2 -Message $partitionDriveLetterValidationError
WriteLog $partitionDriveLetterValidationError
throw $partitionDriveLetterValidationError
}
#Validate CopyDrivers dependency on BuildUSBDrive #Validate CopyDrivers dependency on BuildUSBDrive
if ($CopyDrivers -and (-not $BuildUSBDrive)) { if ($CopyDrivers -and (-not $BuildUSBDrive)) {
WriteLog "-CopyDrivers is set to `$true, but -BuildUSBDrive is not set to `$true" WriteLog "-CopyDrivers is set to `$true, but -BuildUSBDrive is not set to `$true"
@@ -6455,24 +6579,47 @@ catch {
#Create apps ISO for Office and/or 3rd party apps #Create apps ISO for Office and/or 3rd party apps
if ($InstallApps) { if ($InstallApps) {
Set-Progress -Percentage 6 -Message "Downloading and preparing applications..." Set-Progress -Percentage 6 -Message "Downloading and preparing applications..."
if (Test-Path -Path $AppsISO) { $appsIsoRefreshRequired = -not (Test-Path -Path $AppsISO -PathType Leaf)
if (-not $appsIsoRefreshRequired) {
WriteLog "Apps ISO exists at: $AppsISO" WriteLog "Apps ISO exists at: $AppsISO"
# Refresh the Apps ISO when a BYO app list is present so the staged manifest $appIsoInputPaths = @(
# and AppInstallConfig.json stay in sync with the current build inputs. $AppListPath,
if (Test-Path -Path $UserAppListPath) { $UserAppListPath,
WriteLog "Configured BYO app list detected. Refreshing Apps ISO to include the latest BYO app list data." $appInstallConfigPath,
Sync-UserAppListForOrchestration -SourcePath $UserAppListPath -AppsPath $AppsPath -OrchestrationPath $OrchestrationPath -AppInstallConfigPath $appInstallConfigPath $wingetWin32jsonFile,
Remove-Item -Path $AppsISO -Force -ErrorAction SilentlyContinue $appsScriptVarsJsonPath,
New-AppsISO $OfficeDownloadXML,
WriteLog "Apps ISO refreshed to include the latest BYO app list data." $OfficeConfigXMLFile
)
if ($InjectUnattend) {
$appIsoInputPaths += Get-UnattendSourcePath -UnattendFolder $UnattendFolder -WindowsArch $WindowsArch -UnattendX64FilePath $UnattendX64FilePath -UnattendArm64FilePath $UnattendArm64FilePath
} }
else {
WriteLog "Will use existing ISO" $appsIsoRefreshRequired = Test-AppsIsoRefreshRequired -AppsISOPath $AppsISO -AppsPath $AppsPath -InputPaths $appIsoInputPaths
}
if (Test-Path -Path $wingetWin32jsonFile -PathType Leaf) {
try {
WriteLog "Removing generated Winget Win32 app manifest before app preparation: $wingetWin32jsonFile"
Remove-Item -Path $wingetWin32jsonFile -Force -ErrorAction Stop
WriteLog 'Removal complete'
}
catch {
WriteLog "Failed removing generated Winget Win32 app manifest: $($_.Exception.Message)"
if ($appsIsoRefreshRequired) { throw $_ }
} }
} }
else {
if ($appsIsoRefreshRequired) {
try { try {
if (Test-Path -Path $AppsISO -PathType Leaf) {
WriteLog "Refreshing Apps ISO because app inputs changed: $AppsISO"
Remove-Item -Path $AppsISO -Force -ErrorAction SilentlyContinue
WriteLog 'Removal complete'
}
#Check for and download WinGet applications #Check for and download WinGet applications
if (Test-Path -Path $AppListPath) { if (Test-Path -Path $AppListPath) {
$appList = Get-Content -Path $AppListPath -Raw | ConvertFrom-Json $appList = Get-Content -Path $AppListPath -Raw | ConvertFrom-Json
@@ -6924,6 +7071,9 @@ if ($InstallApps) {
throw $_ throw $_
} }
} }
else {
WriteLog "Will use existing ISO"
}
} }
#Create VHDX #Create VHDX
@@ -7127,6 +7277,15 @@ try {
[uint64]$cachedDisksize = 0 [uint64]$cachedDisksize = 0
if (-not [uint64]::TryParse([string]$vhdxCacheItem.Disksize, [ref]$cachedDisksize)) { WriteLog "Disksize invalid in cached config ($($vhdxCacheItem.Disksize)), continuing"; continue } if (-not [uint64]::TryParse([string]$vhdxCacheItem.Disksize, [ref]$cachedDisksize)) { WriteLog "Disksize invalid in cached config ($($vhdxCacheItem.Disksize)), continuing"; continue }
if ($cachedDisksize -ne $Disksize) { WriteLog "Disksize mismatch (cached: $cachedDisksize, current: $Disksize), continuing"; continue } if ($cachedDisksize -ne $Disksize) { WriteLog "Disksize mismatch (cached: $cachedDisksize, current: $Disksize), continuing"; continue }
if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'SystemPartitionDriveLetter') { WriteLog 'SystemPartitionDriveLetter missing in cached config, continuing'; continue }
if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'WindowsPartitionDriveLetter') { WriteLog 'WindowsPartitionDriveLetter missing in cached config, continuing'; continue }
if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'RecoveryPartitionDriveLetter') { WriteLog 'RecoveryPartitionDriveLetter missing in cached config, continuing'; continue }
$cachedSystemPartitionDriveLetter = Get-PartitionDriveLetterCacheValue -DriveLetterValue $vhdxCacheItem.SystemPartitionDriveLetter
$cachedWindowsPartitionDriveLetter = Get-PartitionDriveLetterCacheValue -DriveLetterValue $vhdxCacheItem.WindowsPartitionDriveLetter
$cachedRecoveryPartitionDriveLetter = Get-PartitionDriveLetterCacheValue -DriveLetterValue $vhdxCacheItem.RecoveryPartitionDriveLetter
if ($cachedSystemPartitionDriveLetter -ne $SystemPartitionDriveLetter) { WriteLog "SystemPartitionDriveLetter mismatch (cached: $($vhdxCacheItem.SystemPartitionDriveLetter), current: $SystemPartitionDriveLetter), continuing"; continue }
if ($cachedWindowsPartitionDriveLetter -ne $WindowsPartitionDriveLetter) { WriteLog "WindowsPartitionDriveLetter mismatch (cached: $($vhdxCacheItem.WindowsPartitionDriveLetter), current: $WindowsPartitionDriveLetter), continuing"; continue }
if ($cachedRecoveryPartitionDriveLetter -ne $RecoveryPartitionDriveLetter) { WriteLog "RecoveryPartitionDriveLetter mismatch (cached: $($vhdxCacheItem.RecoveryPartitionDriveLetter), current: $RecoveryPartitionDriveLetter), continuing"; continue }
$cachedUpdateNames = @() $cachedUpdateNames = @()
if ($vhdxCacheItem.IncludedUpdates -and $vhdxCacheItem.IncludedUpdates.Count -gt 0) { if ($vhdxCacheItem.IncludedUpdates -and $vhdxCacheItem.IncludedUpdates.Count -gt 0) {
@@ -7444,21 +7603,21 @@ try {
$vhdxDisk = New-ScratchVhdx -VhdxPath $VHDXPath -SizeBytes $disksize -LogicalSectorSizeBytes $LogicalSectorSizeBytes $vhdxDisk = New-ScratchVhdx -VhdxPath $VHDXPath -SizeBytes $disksize -LogicalSectorSizeBytes $LogicalSectorSizeBytes
$systemPartitionDriveLetter = New-SystemPartition -VhdxDisk $vhdxDisk $createdSystemPartitionDriveLetter = New-SystemPartition -VhdxDisk $vhdxDisk -DriveLetter $SystemPartitionDriveLetter
New-MSRPartition -VhdxDisk $vhdxDisk New-MSRPartition -VhdxDisk $vhdxDisk
Set-Progress -Percentage 16 -Message "Applying base Windows image to VHDX..." Set-Progress -Percentage 16 -Message "Applying base Windows image to VHDX..."
$osPartition = New-OSPartition -VhdxDisk $vhdxDisk -OSPartitionSize $OSPartitionSize -WimPath $WimPath -WimIndex $index $osPartition = New-OSPartition -VhdxDisk $vhdxDisk -OSPartitionSize $OSPartitionSize -WimPath $WimPath -WimIndex $index -DriveLetter $WindowsPartitionDriveLetter
$osPartitionDriveLetter = $osPartition[1].DriveLetter $osPartitionDriveLetter = $osPartition[1].DriveLetter
$WindowsPartition = $osPartitionDriveLetter + ':\' $WindowsPartition = $osPartitionDriveLetter + ':\'
#$recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DataPartition $dataPartition #$recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DataPartition $dataPartition
$recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DataPartition $dataPartition $recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DriveLetter $RecoveryPartitionDriveLetter -DataPartition $dataPartition
WriteLog 'All necessary partitions created.' WriteLog 'All necessary partitions created.'
Add-BootFiles -OsPartitionDriveLetter $osPartitionDriveLetter -SystemPartitionDriveLetter $systemPartitionDriveLetter[1] -AdkPath $adkPath -WindowsArch $WindowsArch Add-BootFiles -OsPartitionDriveLetter $osPartitionDriveLetter -SystemPartitionDriveLetter $createdSystemPartitionDriveLetter -AdkPath $adkPath -WindowsArch $WindowsArch
#Add Windows packages #Add Windows packages
if ($UpdateLatestCU -or $UpdateLatestNet -or $UpdatePreviewCU ) { if ($UpdateLatestCU -or $UpdateLatestNet -or $UpdatePreviewCU ) {
@@ -7629,6 +7788,9 @@ try {
$cachedVHDXInfo.VhdxFileName = $("$VMName.vhdx") $cachedVHDXInfo.VhdxFileName = $("$VMName.vhdx")
$cachedVHDXInfo.LogicalSectorSizeBytes = $LogicalSectorSizeBytes $cachedVHDXInfo.LogicalSectorSizeBytes = $LogicalSectorSizeBytes
$cachedVHDXInfo.Disksize = $Disksize $cachedVHDXInfo.Disksize = $Disksize
$cachedVHDXInfo.SystemPartitionDriveLetter = [string]$SystemPartitionDriveLetter
$cachedVHDXInfo.WindowsPartitionDriveLetter = [string]$WindowsPartitionDriveLetter
$cachedVHDXInfo.RecoveryPartitionDriveLetter = [string]$RecoveryPartitionDriveLetter
$cachedVHDXInfo.WindowsSKU = $WindowsSKU $cachedVHDXInfo.WindowsSKU = $WindowsSKU
$cachedVHDXInfo.WindowsRelease = $WindowsRelease $cachedVHDXInfo.WindowsRelease = $WindowsRelease
$cachedVHDXInfo.WindowsVersion = $WindowsVersion $cachedVHDXInfo.WindowsVersion = $WindowsVersion
+24 -10
View File
@@ -338,7 +338,6 @@ $script:uiState.Controls.btnRun.Add_Click({
$script:uiState.Flags.isCleanupRunning = $true $script:uiState.Flags.isCleanupRunning = $true
$script:uiState.Data.pollTimer.Add_Tick({ $script:uiState.Data.pollTimer.Add_Tick({
param($sender, $e)
$currentProcess = $script:uiState.Data.currentBuildProcess $currentProcess = $script:uiState.Data.currentBuildProcess
# Read new lines from log # Read new lines from log
@@ -353,13 +352,13 @@ $script:uiState.Controls.btnRun.Add_Click({
} }
if ($null -eq $currentProcess -or $null -eq $script:uiState.Data.pollTimer) { if ($null -eq $currentProcess -or $null -eq $script:uiState.Data.pollTimer) {
if ($null -ne $sender) { $sender.Stop() } if ($null -ne $script:uiState.Data.pollTimer) { $script:uiState.Data.pollTimer.Stop() }
$script:uiState.Data.pollTimer = $null $script:uiState.Data.pollTimer = $null
return return
} }
if ($currentProcess.HasExited) { if ($currentProcess.HasExited) {
if ($null -ne $sender) { $sender.Stop() } if ($null -ne $script:uiState.Data.pollTimer) { $script:uiState.Data.pollTimer.Stop() }
$script:uiState.Data.pollTimer = $null $script:uiState.Data.pollTimer = $null
if ($null -ne $script:uiState.Data.logStreamReader) { if ($null -ne $script:uiState.Data.logStreamReader) {
@@ -621,7 +620,6 @@ $script:uiState.Controls.btnRun.Add_Click({
# Add the Tick event handler # Add the Tick event handler
$script:uiState.Data.pollTimer.Add_Tick({ $script:uiState.Data.pollTimer.Add_Tick({
param($sender, $e)
# This scriptblock runs on the UI thread, so it can safely access script-scoped variables # This scriptblock runs on the UI thread, so it can safely access script-scoped variables
$currentProcess = $script:uiState.Data.currentBuildProcess $currentProcess = $script:uiState.Data.currentBuildProcess
@@ -649,8 +647,8 @@ $script:uiState.Controls.btnRun.Add_Click({
# If process is somehow null or the timer has been nulled out, stop the timer # If process is somehow null or the timer has been nulled out, stop the timer
if ($null -eq $currentProcess -or $null -eq $script:uiState.Data.pollTimer) { if ($null -eq $currentProcess -or $null -eq $script:uiState.Data.pollTimer) {
if ($null -ne $sender) { if ($null -ne $script:uiState.Data.pollTimer) {
$sender.Stop() $script:uiState.Data.pollTimer.Stop()
} }
$script:uiState.Data.pollTimer = $null $script:uiState.Data.pollTimer = $null
return return
@@ -659,8 +657,8 @@ $script:uiState.Controls.btnRun.Add_Click({
# Check if the build process has exited # Check if the build process has exited
if ($currentProcess.HasExited) { if ($currentProcess.HasExited) {
# Stop the timer, we're done polling # Stop the timer, we're done polling
if ($null -ne $sender) { if ($null -ne $script:uiState.Data.pollTimer) {
$sender.Stop() $script:uiState.Data.pollTimer.Stop()
} }
$script:uiState.Data.pollTimer = $null $script:uiState.Data.pollTimer = $null
@@ -698,9 +696,25 @@ $script:uiState.Controls.btnRun.Add_Click({
# Determine final status based on process exit code # Determine final status based on process exit code
$finalStatusText = "FFU build completed successfully." $finalStatusText = "FFU build completed successfully."
if ($exitCode -ne 0) { if ($exitCode -ne 0) {
$finalStatusText = "FFU build failed. Check FFUDevelopment.log for details." $failureDetail = $null
if ($null -ne $script:uiState.Data.logData) {
for ($logIndex = $script:uiState.Data.logData.Count - 1; $logIndex -ge 0; $logIndex--) {
$logLine = [string]$script:uiState.Data.logData[$logIndex]
if ($logLine -match '(?i)(Build validation failed|Exception|ERROR|already assigned|must be a single drive letter|must be unique)') {
$failureDetail = $logLine
break
}
}
}
$finalStatusText = if ($failureDetail) { "FFU build failed. $failureDetail" } else { "FFU build failed. Check FFUDevelopment.log for details." }
WriteLog "BuildFFUVM.ps1 process failed with exit code: $exitCode" WriteLog "BuildFFUVM.ps1 process failed with exit code: $exitCode"
[System.Windows.MessageBox]::Show("The build process failed. Please check the $FFUDevelopmentPath\FFUDevelopment.log file for details.`n`nExit code: $exitCode", "Build Error", "OK", "Error") | Out-Null $buildErrorMessage = "The build process failed. Please check the $FFUDevelopmentPath\FFUDevelopment.log file for details."
if ($failureDetail) {
$buildErrorMessage += "`n`n$failureDetail"
}
$buildErrorMessage += "`n`nExit code: $exitCode"
[System.Windows.MessageBox]::Show($buildErrorMessage, "Build Error", "OK", "Error") | Out-Null
$script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed' $script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed'
} }
else { else {
+90
View File
@@ -339,6 +339,96 @@
<!-- VM Name Prefix --> <!-- VM Name Prefix -->
<TextBlock Text="VM Name Prefix" Margin="0,0,0,8" ToolTip="Prefix for the VM Name. The default is _FFU."/> <TextBlock Text="VM Name Prefix" Margin="0,0,0,8" ToolTip="Prefix for the VM Name. The default is _FFU."/>
<TextBox x:Name="txtVMNamePrefix" HorizontalAlignment="Stretch" Margin="0,0,0,20" ToolTip="Prefix for the VM Name. The default is _FFU."/> <TextBox x:Name="txtVMNamePrefix" HorizontalAlignment="Stretch" Margin="0,0,0,20" ToolTip="Prefix for the VM Name. The default is _FFU."/>
<!-- System Partition Drive Letter -->
<TextBlock Text="System Partition Drive Letter" Margin="0,0,0,8" ToolTip="Drive letter used for the System partition while building the FFU VHDX. Default is S."/>
<ComboBox x:Name="cmbSystemPartitionDriveLetter" HorizontalAlignment="Left" Margin="0,0,0,20" ToolTip="Drive letter used for the System partition while building the FFU VHDX. Default is S.">
<ComboBoxItem Content="A"/>
<ComboBoxItem Content="B"/>
<ComboBoxItem Content="C"/>
<ComboBoxItem Content="D"/>
<ComboBoxItem Content="E"/>
<ComboBoxItem Content="F"/>
<ComboBoxItem Content="G"/>
<ComboBoxItem Content="H"/>
<ComboBoxItem Content="I"/>
<ComboBoxItem Content="J"/>
<ComboBoxItem Content="K"/>
<ComboBoxItem Content="L"/>
<ComboBoxItem Content="M"/>
<ComboBoxItem Content="N"/>
<ComboBoxItem Content="O"/>
<ComboBoxItem Content="P"/>
<ComboBoxItem Content="Q"/>
<ComboBoxItem Content="R"/>
<ComboBoxItem Content="S" IsSelected="True"/>
<ComboBoxItem Content="T"/>
<ComboBoxItem Content="U"/>
<ComboBoxItem Content="V"/>
<ComboBoxItem Content="W"/>
<ComboBoxItem Content="X"/>
<ComboBoxItem Content="Y"/>
<ComboBoxItem Content="Z"/>
</ComboBox>
<!-- Windows Partition Drive Letter -->
<TextBlock Text="Windows Partition Drive Letter" Margin="0,0,0,8" ToolTip="Drive letter used for the Windows partition while building the FFU VHDX. Default is W."/>
<ComboBox x:Name="cmbWindowsPartitionDriveLetter" HorizontalAlignment="Left" Margin="0,0,0,20" ToolTip="Drive letter used for the Windows partition while building the FFU VHDX. Default is W.">
<ComboBoxItem Content="A"/>
<ComboBoxItem Content="B"/>
<ComboBoxItem Content="C"/>
<ComboBoxItem Content="D"/>
<ComboBoxItem Content="E"/>
<ComboBoxItem Content="F"/>
<ComboBoxItem Content="G"/>
<ComboBoxItem Content="H"/>
<ComboBoxItem Content="I"/>
<ComboBoxItem Content="J"/>
<ComboBoxItem Content="K"/>
<ComboBoxItem Content="L"/>
<ComboBoxItem Content="M"/>
<ComboBoxItem Content="N"/>
<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" IsSelected="True"/>
<ComboBoxItem Content="X"/>
<ComboBoxItem Content="Y"/>
<ComboBoxItem Content="Z"/>
</ComboBox>
<!-- Recovery Partition Drive Letter -->
<TextBlock Text="Recovery Partition Drive Letter" Margin="0,0,0,8" ToolTip="Drive letter used for the Recovery partition while building the FFU VHDX. Default is R."/>
<ComboBox x:Name="cmbRecoveryPartitionDriveLetter" HorizontalAlignment="Left" Margin="0,0,0,20" ToolTip="Drive letter used for the Recovery partition while building the FFU VHDX. Default is R.">
<ComboBoxItem Content="A"/>
<ComboBoxItem Content="B"/>
<ComboBoxItem Content="C"/>
<ComboBoxItem Content="D"/>
<ComboBoxItem Content="E"/>
<ComboBoxItem Content="F"/>
<ComboBoxItem Content="G"/>
<ComboBoxItem Content="H"/>
<ComboBoxItem Content="I"/>
<ComboBoxItem Content="J"/>
<ComboBoxItem Content="K"/>
<ComboBoxItem Content="L"/>
<ComboBoxItem Content="M"/>
<ComboBoxItem Content="N"/>
<ComboBoxItem Content="O"/>
<ComboBoxItem Content="P"/>
<ComboBoxItem Content="Q"/>
<ComboBoxItem Content="R" IsSelected="True"/>
<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>
<!-- Logical Sector Size --> <!-- Logical Sector Size -->
<TextBlock Text="Logical Sector Size" Margin="0,0,0,8" ToolTip="Unit32 value of 512 or 4096. Useful for 4Kn drives or devices shipping with UFS drives. Default is 512."/> <TextBlock Text="Logical Sector Size" Margin="0,0,0,8" ToolTip="Unit32 value of 512 or 4096. Useful for 4Kn drives or devices shipping with UFS drives. Default is 512."/>
<ComboBox x:Name="cmbLogicalSectorSize" HorizontalAlignment="Left" ToolTip="Unit32 value of 512 or 4096. Useful for 4Kn drives or devices shipping with UFS drives. Default is 512."> <ComboBox x:Name="cmbLogicalSectorSize" HorizontalAlignment="Left" ToolTip="Unit32 value of 512 or 4096. Useful for 4Kn drives or devices shipping with UFS drives. Default is 512.">
@@ -69,6 +69,11 @@ function Invoke-FFUPostBuildCleanup {
WriteLog "CommonCleanup: Removing $store" WriteLog "CommonCleanup: Removing $store"
try { Remove-Item -LiteralPath $store -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $store : $($_.Exception.Message)" } try { Remove-Item -LiteralPath $store -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $store : $($_.Exception.Message)" }
} }
$wingetWin32AppsJson = Join-Path (Join-Path $AppsPath 'Orchestration') 'WinGetWin32Apps.json'
if (Test-Path -LiteralPath $wingetWin32AppsJson) {
WriteLog "CommonCleanup: Removing $wingetWin32AppsJson"
try { Remove-Item -LiteralPath $wingetWin32AppsJson -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $wingetWin32AppsJson : $($_.Exception.Message)" }
}
$office = Join-Path $AppsPath 'Office' $office = Join-Path $AppsPath 'Office'
if ((Test-Path -LiteralPath $office) -and $InstallOffice) { if ((Test-Path -LiteralPath $office) -and $InstallOffice) {
WriteLog "CommonCleanup: Checking for Office artifacts in $office" WriteLog "CommonCleanup: Checking for Office artifacts in $office"
@@ -26,6 +26,26 @@ function Compare-DellVendorVersion {
return 0 return 0
} }
function Test-DellDriverComponentOsArch {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][System.Xml.XmlElement]$Component,
[Parameter(Mandatory=$true)][ValidateSet('x64', 'x86', 'ARM64')][string]$WindowsArch
)
$deviceGroupOsNodes = @($Component.SelectNodes("*[local-name()='DeviceGroup']/*[local-name()='OperatingSystem']"))
if ($deviceGroupOsNodes.Count -gt 0) {
$validDeviceGroupOs = $deviceGroupOsNodes | Where-Object { $_.GetAttribute('osArch') -eq $WindowsArch } | Select-Object -First 1
return $null -ne $validDeviceGroupOs
}
$supportedOsNodes = @($Component.SelectNodes("*[local-name()='SupportedOperatingSystems']/*[local-name()='OperatingSystem']"))
if ($supportedOsNodes.Count -eq 0) { return $false }
$validSupportedOs = $supportedOsNodes | Where-Object { $_.GetAttribute('osArch') -eq $WindowsArch } | Select-Object -First 1
return $null -ne $validSupportedOs
}
function Get-DellCatalogIndex { function Get-DellCatalogIndex {
[CmdletBinding()] [CmdletBinding()]
param( param(
@@ -155,10 +175,7 @@ function Get-DellLatestDriverPackages {
if ($ctype.GetAttribute('value') -ne 'DRVR') { continue } if ($ctype.GetAttribute('value') -ne 'DRVR') { continue }
# OS filtering (arch only release filtering intentionally minimal for now) # OS filtering (arch only release filtering intentionally minimal for now)
$osNodes = @($comp.SelectNodes("*[local-name()='SupportedOperatingSystems']/*[local-name()='OperatingSystem']")) if (-not (Test-DellDriverComponentOsArch -Component $comp -WindowsArch $WindowsArch)) { continue }
if (-not $osNodes) { continue }
$validOS = $osNodes | Where-Object { $_.GetAttribute('osArch') -eq $WindowsArch } | Select-Object -First 1
if (-not $validOS) { continue }
$path = $comp.GetAttribute('path') $path = $comp.GetAttribute('path')
if (-not $path) { continue } if (-not $path) { continue }
+118 -12
View File
@@ -1023,6 +1023,94 @@ function Install-WinGet {
} }
WriteLog "WinGet installation complete." WriteLog "WinGet installation complete."
} }
function Get-WinGetComponentStatus {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[version]$MinimumVersion = [version]"1.8.1911"
)
$moduleName = 'Microsoft.WinGet.Client'
$status = [PSCustomObject]@{
Success = $false
NeedsUpdate = $true
WinGetInstalled = $false
WinGetNeedsUpdate = $true
WinGetVersion = "Unknown"
WinGetVersionObject = $null
WinGetStatus = "Unknown"
ModuleInstalled = $false
ModuleNeedsUpdate = $true
ModuleVersion = "Not installed"
ModuleVersionObject = $null
CmdletAvailable = $false
ErrorMessage = ""
}
try {
$installedModule = @(Get-InstalledModule -Name $moduleName -ErrorAction SilentlyContinue) | Sort-Object -Property Version -Descending | Select-Object -First 1
$availableModule = @(Get-Module -ListAvailable -Name $moduleName -ErrorAction SilentlyContinue) | Sort-Object -Property Version -Descending | Select-Object -First 1
$wingetModule = if ($null -ne $installedModule) { $installedModule } else { $availableModule }
if ($null -eq $wingetModule) {
$status.WinGetStatus = "$moduleName module is not installed."
WriteLog $status.WinGetStatus
return $status
}
$status.ModuleInstalled = $true
$status.ModuleVersion = $wingetModule.Version.ToString()
$status.ModuleVersionObject = [version]$wingetModule.Version
$status.ModuleNeedsUpdate = $status.ModuleVersionObject -lt $MinimumVersion
WriteLog "$moduleName module version detected: $($status.ModuleVersion)"
Import-Module -Name $moduleName -Force -ErrorAction Stop
$wingetVersionCommand = Get-Command -Name Get-WinGetVersion -ErrorAction SilentlyContinue
if ($null -eq $wingetVersionCommand) {
$status.WinGetStatus = "Get-WinGetVersion cmdlet is not available."
$status.ErrorMessage = $status.WinGetStatus
WriteLog $status.WinGetStatus
return $status
}
$status.CmdletAvailable = $true
$wingetVersion = Get-WinGetVersion -ErrorAction Stop
$wingetVersionText = [string]$wingetVersion
WriteLog "Get-WinGetVersion returned: $wingetVersionText"
if ([string]::IsNullOrWhiteSpace($wingetVersionText)) {
$status.WinGetVersion = "Not installed"
$status.WinGetStatus = "WinGet is not installed."
WriteLog $status.WinGetStatus
return $status
}
if ($wingetVersionText -match 'v?(\d+\.\d+\.\d+)') {
$parsedVersion = [version]$matches[1]
$status.WinGetInstalled = $true
$status.WinGetVersion = $parsedVersion.ToString()
$status.WinGetVersionObject = $parsedVersion
$status.WinGetNeedsUpdate = $parsedVersion -lt $MinimumVersion
$status.WinGetStatus = if ($status.WinGetNeedsUpdate) { "Update required" } else { $parsedVersion.ToString() }
$status.NeedsUpdate = $status.ModuleNeedsUpdate -or $status.WinGetNeedsUpdate
$status.Success = -not $status.NeedsUpdate
return $status
}
$status.WinGetStatus = "Version check failed."
$status.ErrorMessage = "Could not parse Get-WinGetVersion output: $wingetVersionText"
WriteLog $status.ErrorMessage
return $status
}
catch {
$status.ErrorMessage = $_.Exception.Message
$status.WinGetStatus = "Get-WinGetVersion failed."
WriteLog "Get-WinGetVersion failed: $($status.ErrorMessage)"
return $status
}
}
function Confirm-WinGetInstallation { function Confirm-WinGetInstallation {
[CmdletBinding()] [CmdletBinding()]
param( param(
@@ -1032,12 +1120,11 @@ function Confirm-WinGetInstallation {
WriteLog 'Checking if WinGet is installed...' WriteLog 'Checking if WinGet is installed...'
$minVersion = [version]"1.8.1911" $minVersion = [version]"1.8.1911"
$wingetStatus = Get-WinGetComponentStatus -MinimumVersion $minVersion
# Check WinGet PowerShell module # Check WinGet PowerShell module
$wingetModule = Get-InstalledModule -Name Microsoft.Winget.Client -ErrorAction SilentlyContinue if ($wingetStatus.ModuleNeedsUpdate) {
$wingetModuleVersion = [version]$wingetModule.Version WriteLog 'Microsoft.WinGet.Client module is not installed or is an older version. Installing the latest version...'
if ($wingetModuleVersion -lt $minVersion -or -not $wingetModule) {
WriteLog 'Microsoft.Winget.Client module is not installed or is an older version. Installing the latest version...'
# Handle PSGallery trust settings # Handle PSGallery trust settings
$PSGalleryTrust = (Get-PSRepository -Name 'PSGallery').InstallationPolicy $PSGalleryTrust = (Get-PSRepository -Name 'PSGallery').InstallationPolicy
@@ -1046,30 +1133,49 @@ function Confirm-WinGetInstallation {
Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted
} }
Install-Module -Name Microsoft.Winget.Client -Force -Repository 'PSGallery' Install-Module -Name Microsoft.WinGet.Client -Force -Repository 'PSGallery'
if ($PSGalleryTrust -eq 'Untrusted') { if ($PSGalleryTrust -eq 'Untrusted') {
WriteLog 'Setting PSGallery back to untrusted repository...' WriteLog 'Setting PSGallery back to untrusted repository...'
Set-PSRepository -Name 'PSGallery' -InstallationPolicy Untrusted Set-PSRepository -Name 'PSGallery' -InstallationPolicy Untrusted
WriteLog 'Done' WriteLog 'Done'
} }
$wingetStatus = Get-WinGetComponentStatus -MinimumVersion $minVersion
} }
else { else {
WriteLog "Installed Microsoft.Winget.Client module version: $($wingetModule.Version)" WriteLog "Installed Microsoft.WinGet.Client module version: $($wingetStatus.ModuleVersion)"
} }
if ($wingetStatus.ModuleNeedsUpdate) {
$message = "Microsoft.WinGet.Client module version $($wingetStatus.ModuleVersion) does not meet the minimum required version $minVersion."
WriteLog $message
throw $message
}
if (-not $wingetStatus.CmdletAvailable) {
$message = "Get-WinGetVersion cmdlet is not available from Microsoft.WinGet.Client. $($wingetStatus.ErrorMessage)"
WriteLog $message
throw $message
}
if (-not [string]::IsNullOrWhiteSpace($wingetStatus.ErrorMessage)) {
$message = "Unable to determine WinGet version by using Get-WinGetVersion. $($wingetStatus.ErrorMessage)"
WriteLog $message
throw $message
}
# Check WinGet CLI # Check WinGet CLI
$wingetVersion = Get-WinGetVersion if (-not $wingetStatus.WinGetInstalled) {
if (-not $wingetVersion) {
WriteLog "WinGet is not installed. Installing WinGet..." WriteLog "WinGet is not installed. Installing WinGet..."
Install-WinGet -Architecture $WindowsArch Install-WinGet -Architecture $WindowsArch
} }
elseif ($wingetVersion -match 'v?(\d+\.\d+\.\d+)' -and [version]$matches[1] -lt $minVersion) { elseif ($wingetStatus.WinGetNeedsUpdate) {
WriteLog "The installed version of WinGet $($matches[1]) does not support downloading MSStore apps. Installing the latest version of WinGet..." WriteLog "The installed version of WinGet $($wingetStatus.WinGetVersion) does not support downloading MSStore apps. Installing the latest version of WinGet..."
Install-WinGet -Architecture $WindowsArch Install-WinGet -Architecture $WindowsArch
} }
else { else {
WriteLog "Installed WinGet version: $wingetVersion" WriteLog "Installed WinGet version: $($wingetStatus.WinGetVersion)"
} }
} }
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@@ -1561,4 +1667,4 @@ function Add-Win32SilentInstallCommand {
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# Export functions needed by both BuildFFUVM and the UI Core module # Export functions needed by both BuildFFUVM and the UI Core module
Export-ModuleMember -Function Get-Application, Get-Apps, Start-WingetAppDownloadTask, Confirm-WinGetInstallation, Add-Win32SilentInstallCommand, Install-Winget Export-ModuleMember -Function Get-Application, Get-Apps, Start-WingetAppDownloadTask, Confirm-WinGetInstallation, Get-WinGetComponentStatus, Add-Win32SilentInstallCommand, Install-Winget
@@ -62,6 +62,9 @@ function Get-UIConfig {
InstallWingetApps = $State.Controls.chkInstallWingetApps.IsChecked InstallWingetApps = $State.Controls.chkInstallWingetApps.IsChecked
ISOPath = $State.Controls.txtISOPath.Text ISOPath = $State.Controls.txtISOPath.Text
WindowsMediaSource = if ($null -ne $State.Controls.rbProvideISO -and $State.Controls.rbProvideISO.IsChecked) { "Provide Windows ISO" } else { "Download Windows ESD" } WindowsMediaSource = if ($null -ne $State.Controls.rbProvideISO -and $State.Controls.rbProvideISO.IsChecked) { "Provide Windows ISO" } else { "Download Windows ESD" }
SystemPartitionDriveLetter = $State.Controls.cmbSystemPartitionDriveLetter.SelectedItem.Content
WindowsPartitionDriveLetter = $State.Controls.cmbWindowsPartitionDriveLetter.SelectedItem.Content
RecoveryPartitionDriveLetter = $State.Controls.cmbRecoveryPartitionDriveLetter.SelectedItem.Content
LogicalSectorSizeBytes = [int]$State.Controls.cmbLogicalSectorSize.SelectedItem.Content LogicalSectorSizeBytes = [int]$State.Controls.cmbLogicalSectorSize.SelectedItem.Content
# Make = $null # Make = $null
MediaType = $State.Controls.cmbMediaType.SelectedItem MediaType = $State.Controls.cmbMediaType.SelectedItem
@@ -523,6 +526,9 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'txtProcessors' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Processors' -State $State Set-UIValue -ControlName 'txtProcessors' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Processors' -State $State
Set-UIValue -ControlName 'txtVMLocation' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'VMLocation' -State $State Set-UIValue -ControlName 'txtVMLocation' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'VMLocation' -State $State
Set-UIValue -ControlName 'txtVMNamePrefix' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'FFUPrefix' -State $State Set-UIValue -ControlName 'txtVMNamePrefix' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'FFUPrefix' -State $State
Set-UIValue -ControlName 'cmbSystemPartitionDriveLetter' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'SystemPartitionDriveLetter' -TransformValue { param($val) ([string]$val).Trim().TrimEnd(':').ToUpperInvariant() } -State $State
Set-UIValue -ControlName 'cmbWindowsPartitionDriveLetter' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'WindowsPartitionDriveLetter' -TransformValue { param($val) ([string]$val).Trim().TrimEnd(':').ToUpperInvariant() } -State $State
Set-UIValue -ControlName 'cmbRecoveryPartitionDriveLetter' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'RecoveryPartitionDriveLetter' -TransformValue { param($val) ([string]$val).Trim().TrimEnd(':').ToUpperInvariant() } -State $State
Set-UIValue -ControlName 'cmbLogicalSectorSize' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'LogicalSectorSizeBytes' -TransformValue { param($val) $val.ToString() } -State $State Set-UIValue -ControlName 'cmbLogicalSectorSize' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'LogicalSectorSizeBytes' -TransformValue { param($val) $val.ToString() } -State $State
$State.Controls.spVMNetworkingSettings.IsEnabled = $true -eq $State.Controls.chkEnableVMNetworking.IsChecked $State.Controls.spVMNetworkingSettings.IsEnabled = $true -eq $State.Controls.chkEnableVMNetworking.IsChecked
if (-not ($true -eq $State.Controls.chkEnableVMNetworking.IsChecked)) { if (-not ($true -eq $State.Controls.chkEnableVMNetworking.IsChecked)) {
@@ -255,6 +255,9 @@ function Initialize-UIControls {
$State.Controls.txtProcessors = $window.FindName('txtProcessors') $State.Controls.txtProcessors = $window.FindName('txtProcessors')
$State.Controls.txtVMLocation = $window.FindName('txtVMLocation') $State.Controls.txtVMLocation = $window.FindName('txtVMLocation')
$State.Controls.txtVMNamePrefix = $window.FindName('txtVMNamePrefix') $State.Controls.txtVMNamePrefix = $window.FindName('txtVMNamePrefix')
$State.Controls.cmbSystemPartitionDriveLetter = $window.FindName('cmbSystemPartitionDriveLetter')
$State.Controls.cmbWindowsPartitionDriveLetter = $window.FindName('cmbWindowsPartitionDriveLetter')
$State.Controls.cmbRecoveryPartitionDriveLetter = $window.FindName('cmbRecoveryPartitionDriveLetter')
$State.Controls.cmbLogicalSectorSize = $window.FindName('cmbLogicalSectorSize') $State.Controls.cmbLogicalSectorSize = $window.FindName('cmbLogicalSectorSize')
$State.Controls.txtProductKey = $window.FindName('txtProductKey') $State.Controls.txtProductKey = $window.FindName('txtProductKey')
$State.Controls.txtOfficePath = $window.FindName('txtOfficePath') $State.Controls.txtOfficePath = $window.FindName('txtOfficePath')
@@ -445,6 +448,9 @@ function Initialize-UIDefaults {
$State.Controls.txtProcessors.Text = $State.Defaults.generalDefaults.Processors $State.Controls.txtProcessors.Text = $State.Defaults.generalDefaults.Processors
$State.Controls.txtVMLocation.Text = $State.Defaults.generalDefaults.VMLocation $State.Controls.txtVMLocation.Text = $State.Defaults.generalDefaults.VMLocation
$State.Controls.txtVMNamePrefix.Text = $State.Defaults.generalDefaults.VMNamePrefix $State.Controls.txtVMNamePrefix.Text = $State.Defaults.generalDefaults.VMNamePrefix
$State.Controls.cmbSystemPartitionDriveLetter.SelectedItem = ($State.Controls.cmbSystemPartitionDriveLetter.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.SystemPartitionDriveLetter })
$State.Controls.cmbWindowsPartitionDriveLetter.SelectedItem = ($State.Controls.cmbWindowsPartitionDriveLetter.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.WindowsPartitionDriveLetter })
$State.Controls.cmbRecoveryPartitionDriveLetter.SelectedItem = ($State.Controls.cmbRecoveryPartitionDriveLetter.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.RecoveryPartitionDriveLetter })
$State.Controls.cmbLogicalSectorSize.SelectedItem = ($State.Controls.cmbLogicalSectorSize.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.LogicalSectorSize.ToString() }) $State.Controls.cmbLogicalSectorSize.SelectedItem = ($State.Controls.cmbLogicalSectorSize.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.LogicalSectorSize.ToString() })
# Populate Windows Release, Version, and SKU comboboxes # Populate Windows Release, Version, and SKU comboboxes
@@ -233,44 +233,6 @@ function Search-WingetPackagesPublic {
} }
} }
function Test-WingetCLI {
[CmdletBinding()]
param()
$minVersion = [version]"1.8.1911"
# Check Winget CLI
$wingetCmd = Get-Command -Name winget -ErrorAction SilentlyContinue
if (-not $wingetCmd) {
return @{
Version = "Not installed"
Status = "Not installed - Install from Microsoft Store"
}
}
# Get and check version
$wingetVersion = & winget.exe --version
if ($wingetVersion -match 'v?(\d+\.\d+.\d+)') {
$version = [version]$matches[1]
if ($version -lt $minVersion) {
return @{
Version = $version.ToString()
Status = "Update required - Install from Microsoft Store"
}
}
return @{
Version = $version.ToString()
Status = $version.ToString()
}
}
return @{
Version = "Unknown"
Status = "Version check failed"
}
}
function Install-WingetComponents { function Install-WingetComponents {
[CmdletBinding()] [CmdletBinding()]
param( param(
@@ -339,19 +301,22 @@ function Confirm-WingetInstallationUI {
try { try {
# Initial Check # Initial Check
WriteLog "Confirm-WingetInstallationUI: Starting checks..." WriteLog "Confirm-WingetInstallationUI: Starting checks..."
$cliStatus = Test-WingetCLI $wingetStatus = Get-WinGetComponentStatus -MinimumVersion $minVersion
$module = Get-InstalledModule -Name Microsoft.WinGet.Client -ErrorAction SilentlyContinue
$result.CliVersion = $cliStatus.Version $result.CliVersion = $wingetStatus.WinGetVersion
$result.ModuleVersion = if ($null -ne $module) { $module.Version.ToString() } else { "Not installed" } $result.ModuleVersion = $wingetStatus.ModuleVersion
# Use callback for initial status display # Use callback for initial status display
& $UiUpdateCallback $result.CliVersion $result.ModuleVersion & $UiUpdateCallback $result.CliVersion $result.ModuleVersion
# Determine if install/update is needed # Determine if install/update is needed
$needsCliUpdate = $cliStatus.Status -notmatch '^\d+\.\d+\.\d+$' -or ([version]$cliStatus.Version -lt $minVersion) $needsCliUpdate = $wingetStatus.WinGetNeedsUpdate
$needsModuleUpdate = ($null -eq $module) -or ([version]$module.Version -lt $minVersion) $needsModuleUpdate = $wingetStatus.ModuleNeedsUpdate
$result.NeedsUpdate = $needsCliUpdate -or $needsModuleUpdate $result.NeedsUpdate = $wingetStatus.NeedsUpdate
if (-not [string]::IsNullOrWhiteSpace($wingetStatus.ErrorMessage)) {
WriteLog "Confirm-WingetInstallationUI: WinGet status error - $($wingetStatus.ErrorMessage)"
}
if ($result.NeedsUpdate) { if ($result.NeedsUpdate) {
WriteLog "Confirm-WingetInstallationUI: Update needed. CLI Needs Update: $needsCliUpdate, Module Needs Update: $needsModuleUpdate" WriteLog "Confirm-WingetInstallationUI: Update needed. CLI Needs Update: $needsCliUpdate, Module Needs Update: $needsModuleUpdate"
@@ -361,21 +326,29 @@ function Confirm-WingetInstallationUI {
& $UiUpdateCallback $result.CliVersion "Installing/Updating..." & $UiUpdateCallback $result.CliVersion "Installing/Updating..."
# Attempt to install/update Winget CLI and module # Attempt to install/update Winget CLI and module
$installedModule = Install-WingetComponents -UiUpdateCallback $UiUpdateCallback Install-WingetComponents -UiUpdateCallback $UiUpdateCallback | Out-Null
# Re-check status after attempt # Re-check status after attempt
WriteLog "Confirm-WingetInstallationUI: Re-checking status after update attempt..." WriteLog "Confirm-WingetInstallationUI: Re-checking status after update attempt..."
$cliStatus = Test-WingetCLI $wingetStatus = Get-WinGetComponentStatus -MinimumVersion $minVersion
$result.CliVersion = $cliStatus.Version $result.CliVersion = $wingetStatus.WinGetVersion
$result.ModuleVersion = if ($null -ne $installedModule) { $installedModule.Version } else { "Install Failed" } $result.ModuleVersion = $wingetStatus.ModuleVersion
# Use callback for final status display after update attempt # Use callback for final status display after update attempt
& $UiUpdateCallback $result.CliVersion $result.ModuleVersion & $UiUpdateCallback $result.CliVersion $result.ModuleVersion
# Check if update was successful # Check if update was successful
$cliOk = $cliStatus.Status -match '^\d+\.\d+\.\d+$' -and ([version]$cliStatus.Version -ge $minVersion) $cliOk = $wingetStatus.WinGetInstalled -and -not $wingetStatus.WinGetNeedsUpdate
$moduleOk = ($null -ne $installedModule) -and ([version]$installedModule.Version -ge $minVersion) $moduleOk = $wingetStatus.ModuleInstalled -and -not $wingetStatus.ModuleNeedsUpdate
$result.Success = $cliOk -and $moduleOk $result.Success = $cliOk -and $moduleOk -and [string]::IsNullOrWhiteSpace($wingetStatus.ErrorMessage)
$result.Message = if ($result.Success) { "Winget components installed/updated successfully." } else { "Winget component installation/update failed or is incomplete." } $result.Message = if ($result.Success) {
"Winget components installed/updated successfully."
}
elseif (-not [string]::IsNullOrWhiteSpace($wingetStatus.ErrorMessage)) {
"Winget component installation/update failed: $($wingetStatus.ErrorMessage)"
}
else {
"Winget component installation/update failed or is incomplete."
}
WriteLog "Confirm-WingetInstallationUI: Update attempt finished. Success: $($result.Success). Message: $($result.Message)" WriteLog "Confirm-WingetInstallationUI: Update attempt finished. Success: $($result.Success). Message: $($result.Message)"
} }
else { else {
@@ -159,6 +159,9 @@ function Get-GeneralDefaults {
Processors = 4 Processors = 4
VMLocation = $vmLocationPath VMLocation = $vmLocationPath
VMNamePrefix = "_FFU" VMNamePrefix = "_FFU"
SystemPartitionDriveLetter = 'S'
WindowsPartitionDriveLetter = 'W'
RecoveryPartitionDriveLetter = 'R'
LogicalSectorSize = 512 LogicalSectorSize = 512
# Updates Tab Defaults # Updates Tab Defaults
UpdateLatestCU = $true UpdateLatestCU = $true
@@ -1092,13 +1092,24 @@ if (Test-Path -Path $PPKGFolder) {
#FindUnattend #FindUnattend
$UnattendFolder = $USBDrive + "unattend\" $UnattendFolder = $USBDrive + "unattend\"
$UnattendFilePath = $UnattendFolder + "unattend.xml" $UnattendSourceFilePath = $UnattendFolder + "unattend.xml"
$UnattendWorkingFilePath = Join-Path -Path $env:TEMP -ChildPath 'unattend.xml'
$UnattendPrefixPath = $UnattendFolder + "prefixes.txt" $UnattendPrefixPath = $UnattendFolder + "prefixes.txt"
$UnattendComputerNamePath = $UnattendFolder + "SerialComputerNames.csv" $UnattendComputerNamePath = $UnattendFolder + "SerialComputerNames.csv"
If (Test-Path -Path $UnattendFilePath) { If (Test-Path -Path $UnattendSourceFilePath) {
$UnattendFile = Get-ChildItem -Path $UnattendFilePath $UnattendSourceFile = Get-ChildItem -Path $UnattendSourceFilePath
If ($UnattendFile) { If ($UnattendSourceFile) {
$Unattend = $true try {
WriteLog "Copying source unattend file $($UnattendSourceFile.FullName) to temporary working file $UnattendWorkingFilePath"
Copy-Item -Path $UnattendSourceFile.FullName -Destination $UnattendWorkingFilePath -Force -ErrorAction Stop
$UnattendFile = Get-ChildItem -Path $UnattendWorkingFilePath -ErrorAction Stop
WriteLog "Using temporary unattend working file $($UnattendFile.FullName)"
$Unattend = $true
}
catch {
WriteLog "Copying source unattend file to temporary working file failed with error: $_"
Stop-Script -Message "Copying source unattend file to temporary working file failed with error: $_"
}
} }
} }
If (Test-Path -Path $UnattendPrefixPath) { If (Test-Path -Path $UnattendPrefixPath) {
Binary file not shown.
+3
View File
@@ -96,9 +96,11 @@ s{
"Processors": 4, "Processors": 4,
"ProductKey": "", "ProductKey": "",
"PromptExternalHardDiskMedia": true, "PromptExternalHardDiskMedia": true,
"RecoveryPartitionDriveLetter": "R",
"RemoveApps": false, "RemoveApps": false,
"RemoveFFU": false, "RemoveFFU": false,
"RemoveUpdates": false, "RemoveUpdates": false,
"SystemPartitionDriveLetter": "S",
"Threads": 5, "Threads": 5,
"UpdateADK": true, "UpdateADK": true,
"UpdateEdge": true, "UpdateEdge": true,
@@ -116,6 +118,7 @@ s{
"VMLocation": "C:\\FFUDevelopment\\VM", "VMLocation": "C:\\FFUDevelopment\\VM",
"VMSwitchName": "External", "VMSwitchName": "External",
"WindowsArch": "x64", "WindowsArch": "x64",
"WindowsPartitionDriveLetter": "W",
"WindowsLang": "en-us", "WindowsLang": "en-us",
"WindowsRelease": 11, "WindowsRelease": 11,
"WindowsSKU": "Pro", "WindowsSKU": "Pro",
+3 -1
View File
@@ -659,6 +659,8 @@ Controls the `-CleanupAppsISO` parameter. When checked, the Apps ISO file is aut
During the build process, when apps are being installed, the script creates an `Apps.iso` file in the FFU Development Path (for example, `.\FFUDevelopment\Apps.iso`). This ISO contains the contents of the `.\FFUDevelopment\Apps` folder, including application installers, Office deployment files, and orchestration scripts, and is mounted to the VM during the build to install applications. During the build process, when apps are being installed, the script creates an `Apps.iso` file in the FFU Development Path (for example, `.\FFUDevelopment\Apps.iso`). This ISO contains the contents of the `.\FFUDevelopment\Apps` folder, including application installers, Office deployment files, and orchestration scripts, and is mounted to the VM during the build to install applications.
If an existing `Apps.iso` is present, FFU Builder reuses it when the app inputs have not changed. If app content, app configuration, BYO app data, or the generated `WinGetWin32Apps.json` orchestration file is newer than the ISO, the ISO is refreshed before the VM starts so the VM receives current install commands.
#### When to Disable #### When to Disable
You may want to disable Cleanup Apps ISO in the following scenarios: You may want to disable Cleanup Apps ISO in the following scenarios:
@@ -755,7 +757,7 @@ During the build process, application content accumulates in several subfolders
| `MSStore` | Microsoft Store applications downloaded via Winget | | `MSStore` | Microsoft Store applications downloaded via Winget |
| `Office` | Microsoft 365 Apps installer files downloaded by the Office Deployment Tool | | `Office` | Microsoft 365 Apps installer files downloaded by the Office Deployment Tool |
Additionally, the `WinGetWin32Apps.json` orchestration file in `.\FFUDevelopment\Apps\Orchestration` is removed. This file is automatically regenerated at build time based on downloaded applications. Additionally, the `WinGetWin32Apps.json` orchestration file in `.\FFUDevelopment\Apps\Orchestration` is removed. This file is generated at build time based on the current downloaded Winget applications and is refreshed when app inputs change.
When this option is enabled, the cleanup process removes: When this option is enabled, the cleanup process removes:
+14
View File
@@ -43,6 +43,20 @@ Default is `$FFUDevelopmentPath\VM`. This is the location of the VHDX that gets
Prefix for the generated VM. Default is _FFU. Prefix for the generated VM. Default is _FFU.
## System Partition Drive Letter
Drive letter used for the System partition while building the FFU VHDX. Default is `S`.
## Windows Partition Drive Letter
Drive letter used for the Windows partition while building the FFU VHDX. Default is `W`.
## Recovery Partition Drive Letter
Drive letter used for the Recovery partition while building the FFU VHDX. Default is `R`.
These settings only affect FFU creation. They do not change the hard-coded drive letters used by `ApplyFFU.ps1` during deployment.
## Logical Sector Size ## Logical Sector Size
Uint32 value of 512 or 4096. Useful for 4Kn drives or devices shipping with UFS drives. Default is 512. Uint32 value of 512 or 4096. Useful for 4Kn drives or devices shipping with UFS drives. Default is 512.
+3
View File
@@ -75,10 +75,12 @@ This table lists all top-level parameters in BuildFFUVM.ps1.
| -Processors | int | Processors | Number of virtual processors for the virtual machine. Recommended to use at least 4. | | -Processors | int | Processors | Number of virtual processors for the virtual machine. Recommended to use at least 4. |
| -ProductKey | string | Product Key | Product key for the Windows edition specified in WindowsSKU. This will overwrite whatever SKU is entered for WindowsSKU. Recommended to use if you want to use a MAK or KMS key to activate Enterprise or Education. If using VL media instead of consumer media, you'll want to enter a MAK or KMS key here. | | -ProductKey | string | Product Key | Product key for the Windows edition specified in WindowsSKU. This will overwrite whatever SKU is entered for WindowsSKU. Recommended to use if you want to use a MAK or KMS key to activate Enterprise or Education. If using VL media instead of consumer media, you'll want to enter a MAK or KMS key here. |
| -PromptExternalHardDiskMedia | bool | Prompt for External Hard Disk Media | When set to $true, will prompt the user to confirm the use of media identified as External Hard Disk media via WMI class Win32_DiskDrive. Default is $true. | | -PromptExternalHardDiskMedia | bool | Prompt for External Hard Disk Media | When set to $true, will prompt the user to confirm the use of media identified as External Hard Disk media via WMI class Win32_DiskDrive. Default is $true. |
| -RecoveryPartitionDriveLetter | string | Recovery Partition Drive Letter | Drive letter used for the Recovery partition while building the FFU VHDX. Default is R. |
| -RemoveApps | bool | Remove Apps Folder Content | When set to $true, will remove the application content in the Apps folder after the FFU has been captured. Default is $true. | | -RemoveApps | bool | Remove Apps Folder Content | When set to $true, will remove the application content in the Apps folder after the FFU has been captured. Default is $true. |
| -RemoveDownloadedESD | bool | Remove Downloaded ESD file(s) | When set to $true, downloaded Windows ESD files are automatically deleted after they have been applied. Default is $true. | | -RemoveDownloadedESD | bool | Remove Downloaded ESD file(s) | When set to $true, downloaded Windows ESD files are automatically deleted after they have been applied. Default is $true. |
| -RemoveFFU | bool | Remove FFU | When set to $true, will remove the FFU file from the $FFUDevelopmentPath\FFU folder after it has been copied to the USB drive. Default is $false. | | -RemoveFFU | bool | Remove FFU | When set to $true, will remove the FFU file from the $FFUDevelopmentPath\FFU folder after it has been copied to the USB drive. Default is $false. |
| -RemoveUpdates | bool | Remove Downloaded Update Files | When set to $true, will remove the downloaded CU, MSRT, Defender, Edge, OneDrive, and .NET files downloaded. Default is $true. | | -RemoveUpdates | bool | Remove Downloaded Update Files | When set to $true, will remove the downloaded CU, MSRT, Defender, Edge, OneDrive, and .NET files downloaded. Default is $true. |
| -SystemPartitionDriveLetter | string | System Partition Drive Letter | Drive letter used for the System partition while building the FFU VHDX. Default is S. |
| -Threads | int | Threads | Controls the throttle applied to parallel tasks inside the script. Default is 5, matching the UI Threads field, and applies to driver downloads invoked through Invoke-ParallelProcessing. | | -Threads | int | Threads | Controls the throttle applied to parallel tasks inside the script. Default is 5, matching the UI Threads field, and applies to driver downloads invoked through Invoke-ParallelProcessing. |
| -UnattendArm64FilePath | string | arm64 Unattend File Path | Path to the arm64 unattend XML source file used by Copy Unattend.xml and Inject Unattend.xml. Default is $FFUDevelopmentPath\Unattend\unattend_arm64.xml. | | -UnattendArm64FilePath | string | arm64 Unattend File Path | Path to the arm64 unattend XML source file used by Copy Unattend.xml and Inject Unattend.xml. Default is $FFUDevelopmentPath\Unattend\unattend_arm64.xml. |
| -UnattendX64FilePath | string | x64 Unattend File Path | Path to the x64 unattend XML source file used by Copy Unattend.xml and Inject Unattend.xml. Default is $FFUDevelopmentPath\Unattend\unattend_x64.xml. | | -UnattendX64FilePath | string | x64 Unattend File Path | Path to the x64 unattend XML source file used by Copy Unattend.xml and Inject Unattend.xml. Default is $FFUDevelopmentPath\Unattend\unattend_x64.xml. |
@@ -98,6 +100,7 @@ This table lists all top-level parameters in BuildFFUVM.ps1.
| -VMLocation | string | VM Location | Default is $FFUDevelopmentPath\VM. This is the location of the VHDX that gets created where Windows will be installed to. | | -VMLocation | string | VM Location | Default is $FFUDevelopmentPath\VM. This is the location of the VHDX that gets created where Windows will be installed to. |
| -VMSwitchName | string | VM Switch Name + Custom VM Switch Name (when Other selected) | Name of the Hyper-V virtual switch used when -EnableVMNetworking is set to $true. | | -VMSwitchName | string | VM Switch Name + Custom VM Switch Name (when Other selected) | Name of the Hyper-V virtual switch used when -EnableVMNetworking is set to $true. |
| -WindowsArch | string | Windows Architecture | String value of 'x86', 'x64', or 'arm64'. This is used to identify which architecture of Windows to download. Default is 'x64'. | | -WindowsArch | string | Windows Architecture | String value of 'x86', 'x64', or 'arm64'. This is used to identify which architecture of Windows to download. Default is 'x64'. |
| -WindowsPartitionDriveLetter | string | Windows Partition Drive Letter | Drive letter used for the Windows partition while building the FFU VHDX. Default is W. |
| -WindowsLang | string | Windows Language | String value in language-region format (e.g., 'en-us'). This is used to identify which language of media to download. Default is 'en-us'. | | -WindowsLang | string | Windows Language | String value in language-region format (e.g., 'en-us'). This is used to identify which language of media to download. Default is 'en-us'. |
| -WindowsRelease | int | Windows Release | Integer value of 10, 11, 2016, 2019, 2021, 2022, 2024, or 2025. This is used to identify which Windows client/LTSC/server release to use. Default is 11. | | -WindowsRelease | int | Windows Release | Integer value of 10, 11, 2016, 2019, 2021, 2022, 2024, or 2025. This is used to identify which Windows client/LTSC/server release to use. Default is 11. |
| -WindowsSKU | string | Windows SKU | Edition/SKU to install. Accepted values are: 'Home', 'Home N', 'Home Single Language', 'Education', 'Education N', 'Pro', 'Pro N', 'Pro Education', 'Pro Education N', 'Pro for Workstations', 'Pro N for Workstations', 'Enterprise', 'Enterprise N', 'Enterprise 2016 LTSB', 'Enterprise N 2016 LTSB', 'Enterprise LTSC', 'Enterprise N LTSC', 'IoT Enterprise LTSC', 'IoT Enterprise N LTSC', 'Standard', 'Standard (Desktop Experience)', 'Datacenter', 'Datacenter (Desktop Experience)'. | | -WindowsSKU | string | Windows SKU | Edition/SKU to install. Accepted values are: 'Home', 'Home N', 'Home Single Language', 'Education', 'Education N', 'Pro', 'Pro N', 'Pro Education', 'Pro Education N', 'Pro for Workstations', 'Pro N for Workstations', 'Enterprise', 'Enterprise N', 'Enterprise 2016 LTSB', 'Enterprise N 2016 LTSB', 'Enterprise LTSC', 'Enterprise N LTSC', 'IoT Enterprise LTSC', 'IoT Enterprise N LTSC', 'Standard', 'Standard (Desktop Experience)', 'Datacenter', 'Datacenter (Desktop Experience)'. |
+70 -1
View File
@@ -327,10 +327,79 @@ If you want the technician to be prompted for the device name during deployment,
Now you're ready to deploy the FFU to your device. Now you're ready to deploy the FFU to your device.
## Deployment ## Deploying to a physical machine
Deployment should be fairly straight forward: boot off the USB device and the deployment of the FFU and drivers should happen automatically. If you selected **Prompt for Device Name** or another supported device naming option, that naming step will happen during deployment. Deployment should be fairly straight forward: boot off the USB device and the deployment of the FFU and drivers should happen automatically. If you selected **Prompt for Device Name** or another supported device naming option, that naming step will happen during deployment.
## Deploying to a Hyper-V Virtual Machine
You can test FFU deployment without a physical device by using a Hyper-V virtual machine. Instead of a USB drive, the VM uses two virtual hard disks: one as the target where Windows will be installed, and a second VHDX that mirrors the Deploy partition of your USB drive.
### VM Hard Disk Requirements
Your Hyper-V VM must have two hard disks attached:
- **Disk 0** — The target disk where the FFU will be applied (equivalent to the physical device being imaged)
- **Disk 1** — A VHDX containing your FFU and any other deployment files (equivalent to the Deploy partition on your USB drive)
The VM also needs to boot from the WinPE ISO created during the FFU build (`C:\FFUDevelopment\WinPE_FFU_Deploy_x64.iso`), which acts as the Boot partition on your USB drive.
### Creating the Deploy VHDX
1. Open **Disk Management** (right-click Start > **Disk Management**, or run `diskmgmt.msc`)
2. Click **Action** > **Create VHD**
3. Choose a location and file name for the VHDX (e.g. `C:\VMs\deploy.vhdx`)
4. Set the size large enough to hold your FFU file (at minimum a few gigabytes larger than the FFU)
5. Select **VHDX** for the format and **Dynamically expanding** for the type
6. Click **OK** — the new disk appears in Disk Management
7. Right-click the new disk and select **Initialize Disk**, then choose **GPT**
8. Right-click the unallocated space and select **New Simple Volume**
9. Follow the wizard to format the volume as **NTFS**
10. When prompted for the **Volume label**, enter **Deploy**
{: .note-title}
> Note
>
> The deployment script identifies the deploy partition by its volume label. The label must be **Deploy** (case-insensitive). If you have an existing VHDX whose volume isn't labeled correctly, open File Explorer, right-click the drive, select **Properties**, and update the label to **Deploy**.
### Populating the Deploy VHDX
With the VHDX mounted and labeled, copy your deployment files onto it:
1. Copy your FFU file from `C:\FFUDevelopment\FFU` to the Deploy volume
2. Optionally copy any of the following to match the layout of your USB drive's Deploy partition:
- **Drivers** folder — for automatic driver injection during deployment
- **Unattend** folder — containing `unattend.xml`
- **Autopilot** folder — your Autopilot JSON file
- **PPKG** folder — your provisioning package
{: .note-title}
> Note
>
> Copying a large FFU file onto a freshly created dynamic VHDX may take several minutes. The VHDX file needs to expand on disk to accommodate the data as it is copied.
Once populated, the Deploy VHDX contains exactly what a USB drive's Deploy partition would contain, and the deployment script treats it identically.
### Configuring and Starting the VM
1. In **Hyper-V Manager**, open the settings for your VM
2. Under **SCSI Controller** (Generation 2) or **IDE Controller** (Generation 1), add your deploy VHDX as a second hard disk
3. Add your WinPE ISO (`C:\FFUDevelopment\WinPE_FFU_Deploy_x64.iso`) as a DVD drive
{: .note-title}
> Note
>
> For ARM64 builds, the ISO will be named `WinPE_FFU_Deploy_arm64.iso`.
4. Set the boot order so the VM boots from the DVD drive first
5. Start the VM — WinPE will boot, locate the Deploy volume by its **Deploy** label, and apply the FFU to Disk 0 automatically
Deployment proceeds identically to a physical device. Drivers, unattend, and any other files present in the Deploy VHDX are applied just as they would be from the Deploy partition of a USB drive.
If you have any questions or run into any issues, [open a discussion in the Github repo](https://github.com/rbalsleyMSFT/FFU/discussions). If you have any questions or run into any issues, [open a discussion in the Github repo](https://github.com/rbalsleyMSFT/FFU/discussions).
{% include page_nav.html %} {% include page_nav.html %}