mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 10:19:36 -06:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56a2597818 | |||
| 895728ebe8 | |||
| 9fb9e81701 | |||
| c32cb93434 | |||
| 7decd8f1ba | |||
| 75f4025cf1 | |||
| f750cd872a | |||
| 9de2b30c93 | |||
| 0153176278 | |||
| e1c6259021 | |||
| 8774fb4ef0 | |||
| 10a8887281 | |||
| f29319efc4 | |||
| 37a9572c97 | |||
| b21d20d414 | |||
| fedb45a419 | |||
| a3a2bce652 | |||
| cfdf0af878 | |||
| d0e17eb0f7 | |||
| e8aa1b5b4a | |||
| b21cb0bbf6 | |||
| 372806e5aa | |||
| fafe28baf7 |
@@ -1,5 +1,63 @@
|
||||
# Change Log
|
||||
|
||||
# 2604.1
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Fluent style
|
||||
|
||||
With the release of PowerShell 7.6 finally going to GA, I was able to release the Fluent UI styling refresh. This will bring significant improvement to the look and feel of FFU Builder. Note that you will want to make sure you're running **PowerShell 7.6**, otherwise the listviews for Drivers and Applications will be missing the column headers.
|
||||
|
||||
### Build tab reorganization
|
||||
|
||||
The build tab sections now have expanders for the settings within. This should help with organization of each setting.
|
||||
|
||||
### Home page build and release status
|
||||
|
||||
The Home page of FFU Builder will now tell you what build you're on and if there's a new build along with the release notes for the new build. You can also see disk space and hyper-v status, as well as the latest Github repo discussions and a list of resources.
|
||||
|
||||
### Fixed an issue with Surface and Lenovo driver downloads
|
||||
|
||||
Microsoft changed the Surface driver download support page. FFU Builder now uses the [Microsoft Learn page for Surface driver downloads ](https://learn.microsoft.com/en-us/surface/manage-surface-driver-and-firmware-updates) that's designed for IT Admins. It's an easier table to parse rather than trying to parse the updated support page that FFU Builder used to use.
|
||||
|
||||
Fixed an issue where retrieving Lenovo models was failing.
|
||||
|
||||
### Removed Capture ISO
|
||||
|
||||
FFU Builder no longer relies on booting to WinPE to capture builds done via the VM. Instead, FFU Builder will now just capture the VHDX directly. This improves FFU build times tremendously and reduces the need for the VM Switch. The switch is still necessary for those that want to add internet connectivity during the FFU build process.
|
||||
|
||||
### Refresh Windows SKU after fallback SKU selection
|
||||
|
||||
Fixed an issue grabbing the correct Windows SKU when the user incorrectly chose a SKU that wasn't in the media and had to later be prompted for an available SKU. In this situation the SKU that was provided earlier was chosen, which caused a variety of issues.
|
||||
|
||||
### Removed registry-based FFU file naming
|
||||
|
||||
Removed registry-based FFU file naming and now rely on parameters provided at build time and the custom FFU naming template. This will remove the hard coded wait times that had to do with loading/unloading of the registry.
|
||||
|
||||
### Added a checkbox to enable network connectivity during VM build
|
||||
|
||||
Add a checkbox to enable network connectivity during the VM build. I'm fairly confident that the build process should be able to withstand any sysprep-related issues being connected to the internet. The checkbox is flagged as experimental. Give it a try and let me know if you notice any issues.
|
||||
|
||||
### Added UI controls for Device Naming
|
||||
|
||||
Device naming now has an expander in the Build tab that will expose a number of new options available. Rather than writing up a whole thing here in the release notes, the UI should be intuitive enough to explain how it works. The docs have also been updated. I spent a lot of time testing the changes with both legacy naming scenarios and if you make changes in the UI. If you see something that doesn't work, open a discussion or issue.
|
||||
|
||||
### Fixed Office installation issues on ARM64 VMs
|
||||
|
||||
I actually didn't fix anything, but rather removed a restriction that was put in place due to Office requiring internet access to install on ARM64. It seems the PG has fixed the issue requiring internet access and office will now install. However there's a caveat that it will prompt with a compatibility assistant popup. I think we can disable the compatibility assistant service to prevent the pop up from happening in the orchestrator. Will look into this in a future release.
|
||||
|
||||
### Auto-generate ComputerName in Unattend.xml
|
||||
|
||||
Now you can provide your own Unattend.xml without a ComputerName element and FFU Builder will add it if you've chosen to include a computer name. If there's a ComputerName element already in the file, ApplyFFU.ps1 will find it and modify it as per your naming choices.
|
||||
|
||||
### Add custom unattend.xml paths
|
||||
|
||||
There's a new expander for Unattend.xml options in the Build tab which includes paths for the x64 and arm64 unattend.xml files. This means that you can have your unattend files in any location instead of in the FFUDevelopment\unattend folder. This should make upgrades easier for those that have custom unattend.xml files and copying new releases would overwrite your customized unattend files.
|
||||
|
||||
### Fixed an issue where CUs wouldn't service after the March 31, 2026 OOB update (KB5086672) was installed on your host machine
|
||||
|
||||
The KB5086672 CU which is rolled into the April 14, 2026 update (KB5083769) caused an issue with Add-WindowsPackage. Add-WindowsPackage uses the DISM API to service a Windows image. The native dism.exe doesn't have this issue. To keep things consistent, FFU Builder will now use the dism.exe from the installed Windows ADK. While this version of dism might be older than what's on your machine, it should be consistent and not be impacted by future CUs.
|
||||
|
||||
# 2603.2
|
||||
|
||||
## What's Changed
|
||||
|
||||
+195
-33
@@ -147,6 +147,15 @@ Path to the Windows 10/11 ISO file.
|
||||
.PARAMETER LogicalSectorSizeBytes
|
||||
UInt32 value of 512 or 4096. Useful for 4Kn drives or devices shipping with UFS drives. Default is 512.
|
||||
|
||||
.PARAMETER SystemPartitionDriveLetter
|
||||
Drive letter used for the System partition while building the FFU VHDX. Default is S.
|
||||
|
||||
.PARAMETER WindowsPartitionDriveLetter
|
||||
Drive letter used for the Windows partition while building the FFU VHDX. Default is W.
|
||||
|
||||
.PARAMETER RecoveryPartitionDriveLetter
|
||||
Drive letter used for the Recovery partition while building the FFU VHDX. Default is R.
|
||||
|
||||
.PARAMETER Make
|
||||
Make of the device to download drivers. Accepted values are: 'Microsoft', 'Dell', 'HP', 'Lenovo'.
|
||||
|
||||
@@ -411,6 +420,9 @@ param(
|
||||
[string]$MediaType = 'consumer',
|
||||
[ValidateSet(512, 4096)]
|
||||
[uint32]$LogicalSectorSizeBytes = 512,
|
||||
[string]$SystemPartitionDriveLetter = 'S',
|
||||
[string]$WindowsPartitionDriveLetter = 'W',
|
||||
[string]$RecoveryPartitionDriveLetter = 'R',
|
||||
[bool]$Optimize = $true,
|
||||
[string]$DriversJsonPath,
|
||||
[bool]$CompressDownloadedDriversToWim = $false,
|
||||
@@ -866,6 +878,9 @@ class VhdxCacheItem {
|
||||
[string]$VhdxFileName = ""
|
||||
[uint32]$LogicalSectorSizeBytes = ""
|
||||
[uint64]$Disksize = ""
|
||||
[string]$SystemPartitionDriveLetter = ""
|
||||
[string]$WindowsPartitionDriveLetter = ""
|
||||
[string]$RecoveryPartitionDriveLetter = ""
|
||||
[string]$WindowsSKU = ""
|
||||
[string]$WindowsRelease = ""
|
||||
[string]$WindowsVersion = ""
|
||||
@@ -2744,6 +2759,42 @@ function New-AppsISO {
|
||||
$AppsPath = '\\?\' + $AppsPath
|
||||
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 {
|
||||
#Mount ISO, get Wim file
|
||||
$mountResult = Mount-DiskImage -ImagePath $isoPath -PassThru
|
||||
@@ -3012,21 +3063,80 @@ function New-ScratchVhdx {
|
||||
Writelog "Done."
|
||||
return $toReturn
|
||||
}
|
||||
function Get-NormalizedPartitionDriveLetters {
|
||||
param(
|
||||
[string]$SystemPartitionDriveLetter,
|
||||
[string]$WindowsPartitionDriveLetter,
|
||||
[string]$RecoveryPartitionDriveLetter,
|
||||
[switch]$ValidateAvailable
|
||||
)
|
||||
|
||||
$requestedLetters = [ordered]@{
|
||||
SystemPartitionDriveLetter = $SystemPartitionDriveLetter
|
||||
WindowsPartitionDriveLetter = $WindowsPartitionDriveLetter
|
||||
RecoveryPartitionDriveLetter = $RecoveryPartitionDriveLetter
|
||||
}
|
||||
$normalizedLetters = [ordered]@{}
|
||||
|
||||
foreach ($entry in $requestedLetters.GetEnumerator()) {
|
||||
$driveLetter = ([string]$entry.Value).Trim().TrimEnd(':').ToUpperInvariant()
|
||||
if ([string]::IsNullOrWhiteSpace($driveLetter) -or $driveLetter -notmatch '^[A-Z]$') {
|
||||
throw "$($entry.Key) must be a single drive letter from A to Z without a colon."
|
||||
}
|
||||
|
||||
$normalizedLetters[$entry.Key] = $driveLetter
|
||||
}
|
||||
|
||||
$duplicateLetters = @($normalizedLetters.Values | Group-Object | Where-Object { $_.Count -gt 1 })
|
||||
if ($duplicateLetters.Count -gt 0) {
|
||||
$duplicateLetterList = ($duplicateLetters | ForEach-Object { $_.Name }) -join ', '
|
||||
throw "System, Windows, and Recovery partition drive letters must be unique. Duplicate value(s): $duplicateLetterList."
|
||||
}
|
||||
|
||||
if ($ValidateAvailable) {
|
||||
foreach ($entry in $normalizedLetters.GetEnumerator()) {
|
||||
$existingDrive = Get-PSDrive -Name $entry.Value -PSProvider FileSystem -ErrorAction SilentlyContinue
|
||||
if ($null -ne $existingDrive) {
|
||||
throw "$($entry.Key) uses drive letter $($entry.Value), but that letter is already assigned on this host. Choose an unused drive letter."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [pscustomobject]$normalizedLetters
|
||||
}
|
||||
function Get-PartitionDriveLetterCacheValue {
|
||||
param(
|
||||
[object]$DriveLetterValue
|
||||
)
|
||||
|
||||
$driveLetter = ([string]$DriveLetterValue).Trim().TrimEnd(':').ToUpperInvariant()
|
||||
if ($driveLetter -match '^[A-Z]$') {
|
||||
return $driveLetter
|
||||
}
|
||||
|
||||
$trailingDriveLetter = [regex]::Match($driveLetter, '(?i)(?:^|[^A-Z])([A-Z])$')
|
||||
if ($trailingDriveLetter.Success) {
|
||||
return $trailingDriveLetter.Groups[1].Value.ToUpperInvariant()
|
||||
}
|
||||
|
||||
return $driveLetter
|
||||
}
|
||||
#Add System Partition
|
||||
function New-SystemPartition {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ciminstance]$VhdxDisk,
|
||||
[string]$DriveLetter = 'S',
|
||||
[uint64]$SystemPartitionSize = 260MB
|
||||
)
|
||||
|
||||
WriteLog "Creating System partition..."
|
||||
|
||||
$sysPartition = $VhdxDisk | New-Partition -DriveLetter 'S' -Size $SystemPartitionSize -GptType "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" -IsHidden
|
||||
$sysPartition | Format-Volume -FileSystem FAT32 -Force -NewFileSystemLabel "System"
|
||||
$sysPartition = $VhdxDisk | New-Partition -DriveLetter $DriveLetter -Size $SystemPartitionSize -GptType "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" -IsHidden
|
||||
$sysPartition | Format-Volume -FileSystem FAT32 -Force -NewFileSystemLabel "System" | Out-Null
|
||||
|
||||
WriteLog 'Done.'
|
||||
return $sysPartition.DriveLetter
|
||||
return [string]$sysPartition.DriveLetter
|
||||
}
|
||||
#Add MSRPartition
|
||||
function New-MSRPartition {
|
||||
@@ -3052,16 +3162,17 @@ function New-OSPartition {
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$WimPath,
|
||||
[uint32]$WimIndex,
|
||||
[string]$DriveLetter = 'W',
|
||||
[uint64]$OSPartitionSize = 0
|
||||
)
|
||||
|
||||
WriteLog "Creating OS partition..."
|
||||
|
||||
if ($OSPartitionSize -gt 0) {
|
||||
$osPartition = $vhdxDisk | New-Partition -DriveLetter 'W' -Size $OSPartitionSize -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}"
|
||||
$osPartition = $vhdxDisk | New-Partition -DriveLetter $DriveLetter -Size $OSPartitionSize -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}"
|
||||
}
|
||||
else {
|
||||
$osPartition = $vhdxDisk | New-Partition -DriveLetter 'W' -UseMaximumSize -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}"
|
||||
$osPartition = $vhdxDisk | New-Partition -DriveLetter $DriveLetter -UseMaximumSize -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}"
|
||||
}
|
||||
|
||||
$osPartition | Format-Volume -FileSystem NTFS -Confirm:$false -Force -NewFileSystemLabel "Windows"
|
||||
@@ -3093,6 +3204,7 @@ function New-RecoveryPartition {
|
||||
[Parameter(Mandatory = $true)]
|
||||
$OsPartition,
|
||||
[uint64]$RecoveryPartitionSize = 0,
|
||||
[string]$DriveLetter = 'R',
|
||||
[ciminstance]$DataPartition
|
||||
)
|
||||
|
||||
@@ -3127,7 +3239,7 @@ function New-RecoveryPartition {
|
||||
WriteLog "OS partition shrunk by $calculatedRecoverySize bytes for Recovery partition."
|
||||
}
|
||||
|
||||
$recoveryPartition = $VhdxDisk | New-Partition -DriveLetter 'R' -UseMaximumSize -GptType "{de94bba4-06d1-4d40-a16a-bfd50179d6ac}" `
|
||||
$recoveryPartition = $VhdxDisk | New-Partition -DriveLetter $DriveLetter -UseMaximumSize -GptType "{de94bba4-06d1-4d40-a16a-bfd50179d6ac}" `
|
||||
| Format-Volume -FileSystem NTFS -Confirm:$false -Force -NewFileSystemLabel 'Recovery'
|
||||
|
||||
WriteLog "Done. Recovery partition at drive $($recoveryPartition.DriveLetter):"
|
||||
@@ -3248,7 +3360,13 @@ function New-FFUVM {
|
||||
& vmconnect localhost "$VMName"
|
||||
|
||||
#Start VM
|
||||
Start-VM -Name $VMName
|
||||
try{
|
||||
Start-VM -Name $VMName -ErrorAction Stop
|
||||
}
|
||||
catch{
|
||||
WriteLog "Error starting VM: $_"
|
||||
throw $_
|
||||
}
|
||||
|
||||
return $VM
|
||||
}
|
||||
@@ -5062,15 +5180,6 @@ function New-RunSession {
|
||||
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
|
||||
if ($OfficePath) {
|
||||
foreach ($n in @('DeployFFU.xml', 'DownloadFFU.xml')) {
|
||||
@@ -5669,6 +5778,7 @@ function Cleanup-CurrentRunDownloads {
|
||||
'Update-Edge.ps1',
|
||||
'Install-Office.ps1',
|
||||
'Install-LTSCUpdate.ps1',
|
||||
'WinGetWin32Apps.json',
|
||||
'AppsScriptVariables.json'
|
||||
)
|
||||
|
||||
@@ -5843,7 +5953,6 @@ if ($ExportConfigFile) {
|
||||
|
||||
####### End Generate Config File #######
|
||||
|
||||
|
||||
#Setting long path support - this prevents issues where some applications have deep directory structures
|
||||
#and oscdimg fails to create the Apps ISO
|
||||
try {
|
||||
@@ -5862,6 +5971,21 @@ if ($LongPathsEnabled -ne 1) {
|
||||
Set-Progress -Percentage 2 -Message "Validating parameters..."
|
||||
###PARAMETER VALIDATION
|
||||
|
||||
#Set build partition drive letters and validate they are available for use; this is required before any build steps that require drive access to ensure the expected drive letters are reserved and to fail fast if there are conflicts.
|
||||
try {
|
||||
$partitionDriveLetters = Get-NormalizedPartitionDriveLetters -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -ValidateAvailable
|
||||
$SystemPartitionDriveLetter = $partitionDriveLetters.SystemPartitionDriveLetter
|
||||
$WindowsPartitionDriveLetter = $partitionDriveLetters.WindowsPartitionDriveLetter
|
||||
$RecoveryPartitionDriveLetter = $partitionDriveLetters.RecoveryPartitionDriveLetter
|
||||
WriteLog "Using build partition drive letters: System=$SystemPartitionDriveLetter, Windows=$WindowsPartitionDriveLetter, Recovery=$RecoveryPartitionDriveLetter"
|
||||
}
|
||||
catch {
|
||||
$partitionDriveLetterValidationError = "Build validation failed: $($_.Exception.Message)"
|
||||
Set-Progress -Percentage 2 -Message $partitionDriveLetterValidationError
|
||||
WriteLog $partitionDriveLetterValidationError
|
||||
throw $partitionDriveLetterValidationError
|
||||
}
|
||||
|
||||
#Validate CopyDrivers dependency on BuildUSBDrive
|
||||
if ($CopyDrivers -and (-not $BuildUSBDrive)) {
|
||||
WriteLog "-CopyDrivers is set to `$true, but -BuildUSBDrive is not set to `$true"
|
||||
@@ -6455,24 +6579,47 @@ catch {
|
||||
#Create apps ISO for Office and/or 3rd party apps
|
||||
if ($InstallApps) {
|
||||
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"
|
||||
|
||||
# Refresh the Apps ISO when a BYO app list is present so the staged manifest
|
||||
# and AppInstallConfig.json stay in sync with the current build inputs.
|
||||
if (Test-Path -Path $UserAppListPath) {
|
||||
WriteLog "Configured BYO app list detected. Refreshing Apps ISO to include the latest BYO app list data."
|
||||
Sync-UserAppListForOrchestration -SourcePath $UserAppListPath -AppsPath $AppsPath -OrchestrationPath $OrchestrationPath -AppInstallConfigPath $appInstallConfigPath
|
||||
Remove-Item -Path $AppsISO -Force -ErrorAction SilentlyContinue
|
||||
New-AppsISO
|
||||
WriteLog "Apps ISO refreshed to include the latest BYO app list data."
|
||||
$appIsoInputPaths = @(
|
||||
$AppListPath,
|
||||
$UserAppListPath,
|
||||
$appInstallConfigPath,
|
||||
$wingetWin32jsonFile,
|
||||
$appsScriptVarsJsonPath,
|
||||
$OfficeDownloadXML,
|
||||
$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 {
|
||||
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
|
||||
if (Test-Path -Path $AppListPath) {
|
||||
$appList = Get-Content -Path $AppListPath -Raw | ConvertFrom-Json
|
||||
@@ -6924,6 +7071,9 @@ if ($InstallApps) {
|
||||
throw $_
|
||||
}
|
||||
}
|
||||
else {
|
||||
WriteLog "Will use existing ISO"
|
||||
}
|
||||
}
|
||||
|
||||
#Create VHDX
|
||||
@@ -7127,6 +7277,15 @@ try {
|
||||
[uint64]$cachedDisksize = 0
|
||||
if (-not [uint64]::TryParse([string]$vhdxCacheItem.Disksize, [ref]$cachedDisksize)) { WriteLog "Disksize invalid in cached config ($($vhdxCacheItem.Disksize)), continuing"; continue }
|
||||
if ($cachedDisksize -ne $Disksize) { WriteLog "Disksize mismatch (cached: $cachedDisksize, current: $Disksize), continuing"; continue }
|
||||
if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'SystemPartitionDriveLetter') { WriteLog 'SystemPartitionDriveLetter missing in cached config, continuing'; continue }
|
||||
if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'WindowsPartitionDriveLetter') { WriteLog 'WindowsPartitionDriveLetter missing in cached config, continuing'; continue }
|
||||
if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'RecoveryPartitionDriveLetter') { WriteLog 'RecoveryPartitionDriveLetter missing in cached config, continuing'; continue }
|
||||
$cachedSystemPartitionDriveLetter = Get-PartitionDriveLetterCacheValue -DriveLetterValue $vhdxCacheItem.SystemPartitionDriveLetter
|
||||
$cachedWindowsPartitionDriveLetter = Get-PartitionDriveLetterCacheValue -DriveLetterValue $vhdxCacheItem.WindowsPartitionDriveLetter
|
||||
$cachedRecoveryPartitionDriveLetter = Get-PartitionDriveLetterCacheValue -DriveLetterValue $vhdxCacheItem.RecoveryPartitionDriveLetter
|
||||
if ($cachedSystemPartitionDriveLetter -ne $SystemPartitionDriveLetter) { WriteLog "SystemPartitionDriveLetter mismatch (cached: $($vhdxCacheItem.SystemPartitionDriveLetter), current: $SystemPartitionDriveLetter), continuing"; continue }
|
||||
if ($cachedWindowsPartitionDriveLetter -ne $WindowsPartitionDriveLetter) { WriteLog "WindowsPartitionDriveLetter mismatch (cached: $($vhdxCacheItem.WindowsPartitionDriveLetter), current: $WindowsPartitionDriveLetter), continuing"; continue }
|
||||
if ($cachedRecoveryPartitionDriveLetter -ne $RecoveryPartitionDriveLetter) { WriteLog "RecoveryPartitionDriveLetter mismatch (cached: $($vhdxCacheItem.RecoveryPartitionDriveLetter), current: $RecoveryPartitionDriveLetter), continuing"; continue }
|
||||
|
||||
$cachedUpdateNames = @()
|
||||
if ($vhdxCacheItem.IncludedUpdates -and $vhdxCacheItem.IncludedUpdates.Count -gt 0) {
|
||||
@@ -7444,21 +7603,21 @@ try {
|
||||
|
||||
$vhdxDisk = New-ScratchVhdx -VhdxPath $VHDXPath -SizeBytes $disksize -LogicalSectorSizeBytes $LogicalSectorSizeBytes
|
||||
|
||||
$systemPartitionDriveLetter = New-SystemPartition -VhdxDisk $vhdxDisk
|
||||
$createdSystemPartitionDriveLetter = New-SystemPartition -VhdxDisk $vhdxDisk -DriveLetter $SystemPartitionDriveLetter
|
||||
|
||||
New-MSRPartition -VhdxDisk $vhdxDisk
|
||||
|
||||
Set-Progress -Percentage 16 -Message "Applying base Windows image to VHDX..."
|
||||
$osPartition = New-OSPartition -VhdxDisk $vhdxDisk -OSPartitionSize $OSPartitionSize -WimPath $WimPath -WimIndex $index
|
||||
$osPartition = New-OSPartition -VhdxDisk $vhdxDisk -OSPartitionSize $OSPartitionSize -WimPath $WimPath -WimIndex $index -DriveLetter $WindowsPartitionDriveLetter
|
||||
$osPartitionDriveLetter = $osPartition[1].DriveLetter
|
||||
$WindowsPartition = $osPartitionDriveLetter + ':\'
|
||||
|
||||
#$recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DataPartition $dataPartition
|
||||
$recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DataPartition $dataPartition
|
||||
$recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DriveLetter $RecoveryPartitionDriveLetter -DataPartition $dataPartition
|
||||
|
||||
WriteLog 'All necessary partitions created.'
|
||||
|
||||
Add-BootFiles -OsPartitionDriveLetter $osPartitionDriveLetter -SystemPartitionDriveLetter $systemPartitionDriveLetter[1] -AdkPath $adkPath -WindowsArch $WindowsArch
|
||||
Add-BootFiles -OsPartitionDriveLetter $osPartitionDriveLetter -SystemPartitionDriveLetter $createdSystemPartitionDriveLetter -AdkPath $adkPath -WindowsArch $WindowsArch
|
||||
|
||||
#Add Windows packages
|
||||
if ($UpdateLatestCU -or $UpdateLatestNet -or $UpdatePreviewCU ) {
|
||||
@@ -7629,6 +7788,9 @@ try {
|
||||
$cachedVHDXInfo.VhdxFileName = $("$VMName.vhdx")
|
||||
$cachedVHDXInfo.LogicalSectorSizeBytes = $LogicalSectorSizeBytes
|
||||
$cachedVHDXInfo.Disksize = $Disksize
|
||||
$cachedVHDXInfo.SystemPartitionDriveLetter = [string]$SystemPartitionDriveLetter
|
||||
$cachedVHDXInfo.WindowsPartitionDriveLetter = [string]$WindowsPartitionDriveLetter
|
||||
$cachedVHDXInfo.RecoveryPartitionDriveLetter = [string]$RecoveryPartitionDriveLetter
|
||||
$cachedVHDXInfo.WindowsSKU = $WindowsSKU
|
||||
$cachedVHDXInfo.WindowsRelease = $WindowsRelease
|
||||
$cachedVHDXInfo.WindowsVersion = $WindowsVersion
|
||||
|
||||
@@ -338,7 +338,6 @@ $script:uiState.Controls.btnRun.Add_Click({
|
||||
$script:uiState.Flags.isCleanupRunning = $true
|
||||
|
||||
$script:uiState.Data.pollTimer.Add_Tick({
|
||||
param($sender, $e)
|
||||
$currentProcess = $script:uiState.Data.currentBuildProcess
|
||||
|
||||
# Read new lines from log
|
||||
@@ -353,13 +352,13 @@ $script:uiState.Controls.btnRun.Add_Click({
|
||||
}
|
||||
|
||||
if ($null -eq $currentProcess -or $null -eq $script:uiState.Data.pollTimer) {
|
||||
if ($null -ne $sender) { $sender.Stop() }
|
||||
if ($null -ne $script:uiState.Data.pollTimer) { $script:uiState.Data.pollTimer.Stop() }
|
||||
$script:uiState.Data.pollTimer = $null
|
||||
return
|
||||
}
|
||||
|
||||
if ($currentProcess.HasExited) {
|
||||
if ($null -ne $sender) { $sender.Stop() }
|
||||
if ($null -ne $script:uiState.Data.pollTimer) { $script:uiState.Data.pollTimer.Stop() }
|
||||
$script:uiState.Data.pollTimer = $null
|
||||
|
||||
if ($null -ne $script:uiState.Data.logStreamReader) {
|
||||
@@ -621,7 +620,6 @@ $script:uiState.Controls.btnRun.Add_Click({
|
||||
|
||||
# Add the Tick event handler
|
||||
$script:uiState.Data.pollTimer.Add_Tick({
|
||||
param($sender, $e)
|
||||
# This scriptblock runs on the UI thread, so it can safely access script-scoped variables
|
||||
$currentProcess = $script:uiState.Data.currentBuildProcess
|
||||
|
||||
@@ -649,8 +647,8 @@ $script:uiState.Controls.btnRun.Add_Click({
|
||||
|
||||
# If process is somehow null or the timer has been nulled out, stop the timer
|
||||
if ($null -eq $currentProcess -or $null -eq $script:uiState.Data.pollTimer) {
|
||||
if ($null -ne $sender) {
|
||||
$sender.Stop()
|
||||
if ($null -ne $script:uiState.Data.pollTimer) {
|
||||
$script:uiState.Data.pollTimer.Stop()
|
||||
}
|
||||
$script:uiState.Data.pollTimer = $null
|
||||
return
|
||||
@@ -659,8 +657,8 @@ $script:uiState.Controls.btnRun.Add_Click({
|
||||
# Check if the build process has exited
|
||||
if ($currentProcess.HasExited) {
|
||||
# Stop the timer, we're done polling
|
||||
if ($null -ne $sender) {
|
||||
$sender.Stop()
|
||||
if ($null -ne $script:uiState.Data.pollTimer) {
|
||||
$script:uiState.Data.pollTimer.Stop()
|
||||
}
|
||||
$script:uiState.Data.pollTimer = $null
|
||||
|
||||
@@ -698,9 +696,25 @@ $script:uiState.Controls.btnRun.Add_Click({
|
||||
# Determine final status based on process exit code
|
||||
$finalStatusText = "FFU build completed successfully."
|
||||
if ($exitCode -ne 0) {
|
||||
$finalStatusText = "FFU build failed. Check FFUDevelopment.log for details."
|
||||
$failureDetail = $null
|
||||
if ($null -ne $script:uiState.Data.logData) {
|
||||
for ($logIndex = $script:uiState.Data.logData.Count - 1; $logIndex -ge 0; $logIndex--) {
|
||||
$logLine = [string]$script:uiState.Data.logData[$logIndex]
|
||||
if ($logLine -match '(?i)(Build validation failed|Exception|ERROR|already assigned|must be a single drive letter|must be unique)') {
|
||||
$failureDetail = $logLine
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$finalStatusText = if ($failureDetail) { "FFU build failed. $failureDetail" } else { "FFU build failed. Check FFUDevelopment.log for details." }
|
||||
WriteLog "BuildFFUVM.ps1 process failed with exit code: $exitCode"
|
||||
[System.Windows.MessageBox]::Show("The build process failed. Please check the $FFUDevelopmentPath\FFUDevelopment.log file for details.`n`nExit code: $exitCode", "Build Error", "OK", "Error") | Out-Null
|
||||
$buildErrorMessage = "The build process failed. Please check the $FFUDevelopmentPath\FFUDevelopment.log file for details."
|
||||
if ($failureDetail) {
|
||||
$buildErrorMessage += "`n`n$failureDetail"
|
||||
}
|
||||
$buildErrorMessage += "`n`nExit code: $exitCode"
|
||||
[System.Windows.MessageBox]::Show($buildErrorMessage, "Build Error", "OK", "Error") | Out-Null
|
||||
$script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed'
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -339,6 +339,96 @@
|
||||
<!-- VM Name Prefix -->
|
||||
<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."/>
|
||||
<!-- 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 -->
|
||||
<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.">
|
||||
|
||||
@@ -69,6 +69,11 @@ function Invoke-FFUPostBuildCleanup {
|
||||
WriteLog "CommonCleanup: Removing $store"
|
||||
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'
|
||||
if ((Test-Path -LiteralPath $office) -and $InstallOffice) {
|
||||
WriteLog "CommonCleanup: Checking for Office artifacts in $office"
|
||||
|
||||
@@ -26,6 +26,26 @@ function Compare-DellVendorVersion {
|
||||
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 {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
@@ -155,10 +175,7 @@ function Get-DellLatestDriverPackages {
|
||||
if ($ctype.GetAttribute('value') -ne 'DRVR') { continue }
|
||||
|
||||
# OS filtering (arch only – release filtering intentionally minimal for now)
|
||||
$osNodes = @($comp.SelectNodes("*[local-name()='SupportedOperatingSystems']/*[local-name()='OperatingSystem']"))
|
||||
if (-not $osNodes) { continue }
|
||||
$validOS = $osNodes | Where-Object { $_.GetAttribute('osArch') -eq $WindowsArch } | Select-Object -First 1
|
||||
if (-not $validOS) { continue }
|
||||
if (-not (Test-DellDriverComponentOsArch -Component $comp -WindowsArch $WindowsArch)) { continue }
|
||||
|
||||
$path = $comp.GetAttribute('path')
|
||||
if (-not $path) { continue }
|
||||
|
||||
@@ -1023,6 +1023,94 @@ function Install-WinGet {
|
||||
}
|
||||
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 {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
@@ -1032,12 +1120,11 @@ function Confirm-WinGetInstallation {
|
||||
|
||||
WriteLog 'Checking if WinGet is installed...'
|
||||
$minVersion = [version]"1.8.1911"
|
||||
$wingetStatus = Get-WinGetComponentStatus -MinimumVersion $minVersion
|
||||
|
||||
# Check WinGet PowerShell module
|
||||
$wingetModule = Get-InstalledModule -Name Microsoft.Winget.Client -ErrorAction SilentlyContinue
|
||||
$wingetModuleVersion = [version]$wingetModule.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...'
|
||||
if ($wingetStatus.ModuleNeedsUpdate) {
|
||||
WriteLog 'Microsoft.WinGet.Client module is not installed or is an older version. Installing the latest version...'
|
||||
|
||||
# Handle PSGallery trust settings
|
||||
$PSGalleryTrust = (Get-PSRepository -Name 'PSGallery').InstallationPolicy
|
||||
@@ -1046,30 +1133,49 @@ function Confirm-WinGetInstallation {
|
||||
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') {
|
||||
WriteLog 'Setting PSGallery back to untrusted repository...'
|
||||
Set-PSRepository -Name 'PSGallery' -InstallationPolicy Untrusted
|
||||
WriteLog 'Done'
|
||||
}
|
||||
|
||||
$wingetStatus = Get-WinGetComponentStatus -MinimumVersion $minVersion
|
||||
}
|
||||
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
|
||||
$wingetVersion = Get-WinGetVersion
|
||||
if (-not $wingetVersion) {
|
||||
if (-not $wingetStatus.WinGetInstalled) {
|
||||
WriteLog "WinGet is not installed. Installing WinGet..."
|
||||
Install-WinGet -Architecture $WindowsArch
|
||||
}
|
||||
elseif ($wingetVersion -match 'v?(\d+\.\d+\.\d+)' -and [version]$matches[1] -lt $minVersion) {
|
||||
WriteLog "The installed version of WinGet $($matches[1]) does not support downloading MSStore apps. Installing the latest version of WinGet..."
|
||||
elseif ($wingetStatus.WinGetNeedsUpdate) {
|
||||
WriteLog "The installed version of WinGet $($wingetStatus.WinGetVersion) does not support downloading MSStore apps. Installing the latest version of WinGet..."
|
||||
Install-WinGet -Architecture $WindowsArch
|
||||
}
|
||||
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-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
|
||||
ISOPath = $State.Controls.txtISOPath.Text
|
||||
WindowsMediaSource = if ($null -ne $State.Controls.rbProvideISO -and $State.Controls.rbProvideISO.IsChecked) { "Provide Windows ISO" } else { "Download Windows ESD" }
|
||||
SystemPartitionDriveLetter = $State.Controls.cmbSystemPartitionDriveLetter.SelectedItem.Content
|
||||
WindowsPartitionDriveLetter = $State.Controls.cmbWindowsPartitionDriveLetter.SelectedItem.Content
|
||||
RecoveryPartitionDriveLetter = $State.Controls.cmbRecoveryPartitionDriveLetter.SelectedItem.Content
|
||||
LogicalSectorSizeBytes = [int]$State.Controls.cmbLogicalSectorSize.SelectedItem.Content
|
||||
# Make = $null
|
||||
MediaType = $State.Controls.cmbMediaType.SelectedItem
|
||||
@@ -523,6 +526,9 @@ function Update-UIFromConfig {
|
||||
Set-UIValue -ControlName 'txtProcessors' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Processors' -State $State
|
||||
Set-UIValue -ControlName 'txtVMLocation' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'VMLocation' -State $State
|
||||
Set-UIValue -ControlName 'txtVMNamePrefix' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'FFUPrefix' -State $State
|
||||
Set-UIValue -ControlName 'cmbSystemPartitionDriveLetter' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'SystemPartitionDriveLetter' -TransformValue { param($val) ([string]$val).Trim().TrimEnd(':').ToUpperInvariant() } -State $State
|
||||
Set-UIValue -ControlName 'cmbWindowsPartitionDriveLetter' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'WindowsPartitionDriveLetter' -TransformValue { param($val) ([string]$val).Trim().TrimEnd(':').ToUpperInvariant() } -State $State
|
||||
Set-UIValue -ControlName 'cmbRecoveryPartitionDriveLetter' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'RecoveryPartitionDriveLetter' -TransformValue { param($val) ([string]$val).Trim().TrimEnd(':').ToUpperInvariant() } -State $State
|
||||
Set-UIValue -ControlName 'cmbLogicalSectorSize' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'LogicalSectorSizeBytes' -TransformValue { param($val) $val.ToString() } -State $State
|
||||
$State.Controls.spVMNetworkingSettings.IsEnabled = $true -eq $State.Controls.chkEnableVMNetworking.IsChecked
|
||||
if (-not ($true -eq $State.Controls.chkEnableVMNetworking.IsChecked)) {
|
||||
|
||||
@@ -255,6 +255,9 @@ function Initialize-UIControls {
|
||||
$State.Controls.txtProcessors = $window.FindName('txtProcessors')
|
||||
$State.Controls.txtVMLocation = $window.FindName('txtVMLocation')
|
||||
$State.Controls.txtVMNamePrefix = $window.FindName('txtVMNamePrefix')
|
||||
$State.Controls.cmbSystemPartitionDriveLetter = $window.FindName('cmbSystemPartitionDriveLetter')
|
||||
$State.Controls.cmbWindowsPartitionDriveLetter = $window.FindName('cmbWindowsPartitionDriveLetter')
|
||||
$State.Controls.cmbRecoveryPartitionDriveLetter = $window.FindName('cmbRecoveryPartitionDriveLetter')
|
||||
$State.Controls.cmbLogicalSectorSize = $window.FindName('cmbLogicalSectorSize')
|
||||
$State.Controls.txtProductKey = $window.FindName('txtProductKey')
|
||||
$State.Controls.txtOfficePath = $window.FindName('txtOfficePath')
|
||||
@@ -445,6 +448,9 @@ function Initialize-UIDefaults {
|
||||
$State.Controls.txtProcessors.Text = $State.Defaults.generalDefaults.Processors
|
||||
$State.Controls.txtVMLocation.Text = $State.Defaults.generalDefaults.VMLocation
|
||||
$State.Controls.txtVMNamePrefix.Text = $State.Defaults.generalDefaults.VMNamePrefix
|
||||
$State.Controls.cmbSystemPartitionDriveLetter.SelectedItem = ($State.Controls.cmbSystemPartitionDriveLetter.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.SystemPartitionDriveLetter })
|
||||
$State.Controls.cmbWindowsPartitionDriveLetter.SelectedItem = ($State.Controls.cmbWindowsPartitionDriveLetter.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.WindowsPartitionDriveLetter })
|
||||
$State.Controls.cmbRecoveryPartitionDriveLetter.SelectedItem = ($State.Controls.cmbRecoveryPartitionDriveLetter.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.RecoveryPartitionDriveLetter })
|
||||
$State.Controls.cmbLogicalSectorSize.SelectedItem = ($State.Controls.cmbLogicalSectorSize.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.LogicalSectorSize.ToString() })
|
||||
|
||||
# Populate Windows Release, Version, and SKU comboboxes
|
||||
|
||||
@@ -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 {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
@@ -339,19 +301,22 @@ function Confirm-WingetInstallationUI {
|
||||
try {
|
||||
# Initial Check
|
||||
WriteLog "Confirm-WingetInstallationUI: Starting checks..."
|
||||
$cliStatus = Test-WingetCLI
|
||||
$module = Get-InstalledModule -Name Microsoft.WinGet.Client -ErrorAction SilentlyContinue
|
||||
$wingetStatus = Get-WinGetComponentStatus -MinimumVersion $minVersion
|
||||
|
||||
$result.CliVersion = $cliStatus.Version
|
||||
$result.ModuleVersion = if ($null -ne $module) { $module.Version.ToString() } else { "Not installed" }
|
||||
$result.CliVersion = $wingetStatus.WinGetVersion
|
||||
$result.ModuleVersion = $wingetStatus.ModuleVersion
|
||||
|
||||
# Use callback for initial status display
|
||||
& $UiUpdateCallback $result.CliVersion $result.ModuleVersion
|
||||
|
||||
# Determine if install/update is needed
|
||||
$needsCliUpdate = $cliStatus.Status -notmatch '^\d+\.\d+\.\d+$' -or ([version]$cliStatus.Version -lt $minVersion)
|
||||
$needsModuleUpdate = ($null -eq $module) -or ([version]$module.Version -lt $minVersion)
|
||||
$result.NeedsUpdate = $needsCliUpdate -or $needsModuleUpdate
|
||||
$needsCliUpdate = $wingetStatus.WinGetNeedsUpdate
|
||||
$needsModuleUpdate = $wingetStatus.ModuleNeedsUpdate
|
||||
$result.NeedsUpdate = $wingetStatus.NeedsUpdate
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($wingetStatus.ErrorMessage)) {
|
||||
WriteLog "Confirm-WingetInstallationUI: WinGet status error - $($wingetStatus.ErrorMessage)"
|
||||
}
|
||||
|
||||
if ($result.NeedsUpdate) {
|
||||
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..."
|
||||
|
||||
# Attempt to install/update Winget CLI and module
|
||||
$installedModule = Install-WingetComponents -UiUpdateCallback $UiUpdateCallback
|
||||
Install-WingetComponents -UiUpdateCallback $UiUpdateCallback | Out-Null
|
||||
|
||||
# Re-check status after attempt
|
||||
WriteLog "Confirm-WingetInstallationUI: Re-checking status after update attempt..."
|
||||
$cliStatus = Test-WingetCLI
|
||||
$result.CliVersion = $cliStatus.Version
|
||||
$result.ModuleVersion = if ($null -ne $installedModule) { $installedModule.Version } else { "Install Failed" }
|
||||
$wingetStatus = Get-WinGetComponentStatus -MinimumVersion $minVersion
|
||||
$result.CliVersion = $wingetStatus.WinGetVersion
|
||||
$result.ModuleVersion = $wingetStatus.ModuleVersion
|
||||
# Use callback for final status display after update attempt
|
||||
& $UiUpdateCallback $result.CliVersion $result.ModuleVersion
|
||||
|
||||
# Check if update was successful
|
||||
$cliOk = $cliStatus.Status -match '^\d+\.\d+\.\d+$' -and ([version]$cliStatus.Version -ge $minVersion)
|
||||
$moduleOk = ($null -ne $installedModule) -and ([version]$installedModule.Version -ge $minVersion)
|
||||
$result.Success = $cliOk -and $moduleOk
|
||||
$result.Message = if ($result.Success) { "Winget components installed/updated successfully." } else { "Winget component installation/update failed or is incomplete." }
|
||||
$cliOk = $wingetStatus.WinGetInstalled -and -not $wingetStatus.WinGetNeedsUpdate
|
||||
$moduleOk = $wingetStatus.ModuleInstalled -and -not $wingetStatus.ModuleNeedsUpdate
|
||||
$result.Success = $cliOk -and $moduleOk -and [string]::IsNullOrWhiteSpace($wingetStatus.ErrorMessage)
|
||||
$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)"
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -159,6 +159,9 @@ function Get-GeneralDefaults {
|
||||
Processors = 4
|
||||
VMLocation = $vmLocationPath
|
||||
VMNamePrefix = "_FFU"
|
||||
SystemPartitionDriveLetter = 'S'
|
||||
WindowsPartitionDriveLetter = 'W'
|
||||
RecoveryPartitionDriveLetter = 'R'
|
||||
LogicalSectorSize = 512
|
||||
# Updates Tab Defaults
|
||||
UpdateLatestCU = $true
|
||||
|
||||
@@ -1092,13 +1092,24 @@ if (Test-Path -Path $PPKGFolder) {
|
||||
|
||||
#FindUnattend
|
||||
$UnattendFolder = $USBDrive + "unattend\"
|
||||
$UnattendFilePath = $UnattendFolder + "unattend.xml"
|
||||
$UnattendSourceFilePath = $UnattendFolder + "unattend.xml"
|
||||
$UnattendWorkingFilePath = Join-Path -Path $env:TEMP -ChildPath 'unattend.xml'
|
||||
$UnattendPrefixPath = $UnattendFolder + "prefixes.txt"
|
||||
$UnattendComputerNamePath = $UnattendFolder + "SerialComputerNames.csv"
|
||||
If (Test-Path -Path $UnattendFilePath) {
|
||||
$UnattendFile = Get-ChildItem -Path $UnattendFilePath
|
||||
If ($UnattendFile) {
|
||||
$Unattend = $true
|
||||
If (Test-Path -Path $UnattendSourceFilePath) {
|
||||
$UnattendSourceFile = Get-ChildItem -Path $UnattendSourceFilePath
|
||||
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
|
||||
}
|
||||
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) {
|
||||
|
||||
Binary file not shown.
@@ -1,9 +1,3 @@
|
||||
# Updates
|
||||
|
||||
## 2026-03-16 - [2603.2 Released](https://github.com/rbalsleyMSFT/FFU/releases)
|
||||
|
||||
Fixes an issue with devices not booting after applying an FFU. Highly recommended you update today.
|
||||
|
||||
# Using Full Flash Update (FFU) files to speed up Windows deployment
|
||||
|
||||
What if you could have a Windows image (Windows 10/11/Server/LTSC) that has:
|
||||
@@ -28,10 +22,12 @@ The Full-Flash update (FFU) process can automatically download the latest releas
|
||||
|
||||
If you're new to FFU Builder or new to the FFU Builder UI version, check out the [Quick Start Guide](https://rbalsleymsft.github.io/FFU/quickstart.html).
|
||||
|
||||
This will be the fastest way to create your first FFU. There's a new [FFU Builder Quickstart Youtube video](https://youtu.be/kOIK5OmDugc) based on the 2602.1 UI Preview release.
|
||||
This will be the fastest way to create your first FFU. There's a new [FFU Builder Quickstart Youtube video](https://youtu.be/38sUc3M5Yls) based on the 2604.1 release.
|
||||
|
||||
## Older Youtube Videos
|
||||
|
||||
[2602.1 UI Preview Quickstart Video](https://www.youtube.com/watch?v=kOIK5OmDugc) - Original quickstart video without the Fluent UI.
|
||||
|
||||
[2507.1 UI Preview Video](https://www.youtube.com/watch?v=oozG1aVcg9M) - First UI Preview release video. This goes deeper than the quick start video, but is missing some features that have been added since 2507.1 was released.
|
||||
|
||||
[2407.2 Video](https://www.youtube.com/watch?v=rqXRbgeeKSQ) - This was the main deep-dive video on FFU Builder (before it had that name). This is a good deep dive into how the BuildFFUVM.ps1 script works, but a lot has changed since that build.
|
||||
|
||||
@@ -96,9 +96,11 @@ s{
|
||||
"Processors": 4,
|
||||
"ProductKey": "",
|
||||
"PromptExternalHardDiskMedia": true,
|
||||
"RecoveryPartitionDriveLetter": "R",
|
||||
"RemoveApps": false,
|
||||
"RemoveFFU": false,
|
||||
"RemoveUpdates": false,
|
||||
"SystemPartitionDriveLetter": "S",
|
||||
"Threads": 5,
|
||||
"UpdateADK": true,
|
||||
"UpdateEdge": true,
|
||||
@@ -116,6 +118,7 @@ s{
|
||||
"VMLocation": "C:\\FFUDevelopment\\VM",
|
||||
"VMSwitchName": "External",
|
||||
"WindowsArch": "x64",
|
||||
"WindowsPartitionDriveLetter": "W",
|
||||
"WindowsLang": "en-us",
|
||||
"WindowsRelease": 11,
|
||||
"WindowsSKU": "Pro",
|
||||
|
||||
+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.
|
||||
|
||||
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
|
||||
|
||||
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 |
|
||||
| `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:
|
||||
|
||||
|
||||
@@ -43,6 +43,20 @@ Default is `$FFUDevelopmentPath\VM`. This is the location of the VHDX that gets
|
||||
|
||||
Prefix for the generated VM. Default is _FFU.
|
||||
|
||||
## System Partition Drive Letter
|
||||
|
||||
Drive letter used for the System partition while building the FFU VHDX. Default is `S`.
|
||||
|
||||
## Windows Partition Drive Letter
|
||||
|
||||
Drive letter used for the Windows partition while building the FFU VHDX. Default is `W`.
|
||||
|
||||
## Recovery Partition Drive Letter
|
||||
|
||||
Drive letter used for the Recovery partition while building the FFU VHDX. Default is `R`.
|
||||
|
||||
These settings only affect FFU creation. They do not change the hard-coded drive letters used by `ApplyFFU.ps1` during deployment.
|
||||
|
||||
## Logical Sector Size
|
||||
|
||||
Uint32 value of 512 or 4096. Useful for 4Kn drives or devices shipping with UFS drives. Default is 512.
|
||||
|
||||
@@ -75,10 +75,12 @@ This table lists all top-level parameters in BuildFFUVM.ps1.
|
||||
| -Processors | int | Processors | Number of virtual processors for the virtual machine. Recommended to use at least 4. |
|
||||
| -ProductKey | string | Product Key | Product key for the Windows edition specified in WindowsSKU. This will overwrite whatever SKU is entered for WindowsSKU. Recommended to use if you want to use a MAK or KMS key to activate Enterprise or Education. If using VL media instead of consumer media, you'll want to enter a MAK or KMS key here. |
|
||||
| -PromptExternalHardDiskMedia | bool | Prompt for External Hard Disk Media | When set to $true, will prompt the user to confirm the use of media identified as External Hard Disk media via WMI class Win32_DiskDrive. Default is $true. |
|
||||
| -RecoveryPartitionDriveLetter | string | Recovery Partition Drive Letter | Drive letter used for the Recovery partition while building the FFU VHDX. Default is R. |
|
||||
| -RemoveApps | bool | Remove Apps Folder Content | When set to $true, will remove the application content in the Apps folder after the FFU has been captured. Default is $true. |
|
||||
| -RemoveDownloadedESD | bool | Remove Downloaded ESD file(s) | When set to $true, downloaded Windows ESD files are automatically deleted after they have been applied. Default is $true. |
|
||||
| -RemoveFFU | bool | Remove FFU | When set to $true, will remove the FFU file from the $FFUDevelopmentPath\FFU folder after it has been copied to the USB drive. Default is $false. |
|
||||
| -RemoveUpdates | bool | Remove Downloaded Update Files | When set to $true, will remove the downloaded CU, MSRT, Defender, Edge, OneDrive, and .NET files downloaded. Default is $true. |
|
||||
| -SystemPartitionDriveLetter | string | System Partition Drive Letter | Drive letter used for the System partition while building the FFU VHDX. Default is S. |
|
||||
| -Threads | int | Threads | Controls the throttle applied to parallel tasks inside the script. Default is 5, matching the UI Threads field, and applies to driver downloads invoked through Invoke-ParallelProcessing. |
|
||||
| -UnattendArm64FilePath | string | arm64 Unattend File Path | Path to the arm64 unattend XML source file used by Copy Unattend.xml and Inject Unattend.xml. Default is $FFUDevelopmentPath\Unattend\unattend_arm64.xml. |
|
||||
| -UnattendX64FilePath | string | x64 Unattend File Path | Path to the x64 unattend XML source file used by Copy Unattend.xml and Inject Unattend.xml. Default is $FFUDevelopmentPath\Unattend\unattend_x64.xml. |
|
||||
@@ -98,6 +100,7 @@ This table lists all top-level parameters in BuildFFUVM.ps1.
|
||||
| -VMLocation | string | VM Location | Default is $FFUDevelopmentPath\VM. This is the location of the VHDX that gets created where Windows will be installed to. |
|
||||
| -VMSwitchName | string | VM Switch Name + Custom VM Switch Name (when Other selected) | Name of the Hyper-V virtual switch used when -EnableVMNetworking is set to $true. |
|
||||
| -WindowsArch | string | Windows Architecture | String value of 'x86', 'x64', or 'arm64'. This is used to identify which architecture of Windows to download. Default is 'x64'. |
|
||||
| -WindowsPartitionDriveLetter | string | Windows Partition Drive Letter | Drive letter used for the Windows partition while building the FFU VHDX. Default is W. |
|
||||
| -WindowsLang | string | Windows Language | String value in language-region format (e.g., 'en-us'). This is used to identify which language of media to download. Default is 'en-us'. |
|
||||
| -WindowsRelease | int | Windows Release | Integer value of 10, 11, 2016, 2019, 2021, 2022, 2024, or 2025. This is used to identify which Windows client/LTSC/server release to use. Default is 11. |
|
||||
| -WindowsSKU | string | Windows SKU | Edition/SKU to install. Accepted values are: 'Home', 'Home N', 'Home Single Language', 'Education', 'Education N', 'Pro', 'Pro N', 'Pro Education', 'Pro Education N', 'Pro for Workstations', 'Pro N for Workstations', 'Enterprise', 'Enterprise N', 'Enterprise 2016 LTSB', 'Enterprise N 2016 LTSB', 'Enterprise LTSC', 'Enterprise N LTSC', 'IoT Enterprise LTSC', 'IoT Enterprise N LTSC', 'Standard', 'Standard (Desktop Experience)', 'Datacenter', 'Datacenter (Desktop Experience)'. |
|
||||
|
||||
@@ -33,7 +33,13 @@ PowerShell 7.6+ is required as of releases 2507+ onward.
|
||||
|
||||
Recommended to use winget to install
|
||||
|
||||
`winget install --id Microsoft.PowerShell --source winget`
|
||||
`winget install --id Microsoft.PowerShell --source winget --installer-type wix`
|
||||
|
||||
{: .note-title}
|
||||
|
||||
> Note
|
||||
>
|
||||
> As of PowerShell 7.6, the default winget installer uses the MSIX version, which is the store version of PowerShell 7.6. Adding `--installer-type wix` will install the MSI version.
|
||||
|
||||
If you can't use winget, [download the MSI](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows#installing-the-msi-package)
|
||||
|
||||
|
||||
+71
-2
@@ -35,7 +35,7 @@ After following this guide, you will have a USB drive with an FFU that contains
|
||||
|
||||
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/kOIK5OmDugc"
|
||||
src="https://www.youtube-nocookie.com/embed/38sUc3M5Yls"
|
||||
title="YouTube video player"
|
||||
style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
@@ -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.
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
## 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).
|
||||
|
||||
{% include page_nav.html %}
|
||||
|
||||
Reference in New Issue
Block a user