mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 10:19:36 -06:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56a2597818 | |||
| 895728ebe8 | |||
| 9fb9e81701 | |||
| c32cb93434 | |||
| 7decd8f1ba | |||
| 75f4025cf1 | |||
| f750cd872a | |||
| 9de2b30c93 | |||
| e1c6259021 | |||
| d0e17eb0f7 | |||
| e8aa1b5b4a | |||
| b21cb0bbf6 | |||
| 372806e5aa | |||
| fafe28baf7 |
+196
-34
@@ -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
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else {
|
if (Test-Path -Path $wingetWin32jsonFile -PathType Leaf) {
|
||||||
try {
|
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 $_ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($appsIsoRefreshRequired) {
|
||||||
|
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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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,14 +1092,25 @@ 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) {
|
||||||
|
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
|
$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) {
|
||||||
$UnattendPrefixFile = Get-ChildItem -Path $UnattendPrefixPath
|
$UnattendPrefixFile = Get-ChildItem -Path $UnattendPrefixPath
|
||||||
|
|||||||
Binary file not shown.
@@ -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
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user