Compare commits

...

44 Commits

Author SHA1 Message Date
rbalsleyMSFT a0dc5a6ae9 Cleans up README formatting and announcements
Removes transient release announcements and outdated progress notes to streamline the document. Improves overall readability by splitting paragraphs and properly formatting section headers for older content.
2026-03-09 19:16:32 -07:00
rbalsleyMSFT 6b548a34e6 Updates changelog for version 2603.1
Adds comprehensive release notes for version 2603.1. Documents the finalization of the UI preview, various servicing improvements, ESD and VHDX caching optimizations, and multiple bug fixes related to updates, driver downloads, and file cleanup mechanisms.
2026-03-09 19:14:03 -07:00
rbalsleyMSFT 8c5629c9ce Merge pull request #422 from rbalsleyMSFT/UI
Merge UI into Main
2026-03-09 17:28:16 -07:00
rbalsleyMSFT e7589f6ceb Merge branch 'main' into UI 2026-03-09 17:20:20 -07:00
rbalsleyMSFT 711582ae71 Updates script versions to 2603.1
Bumps the internal version strings from the 2602.1 preview to the 2603.1 release across deployment scripts.
2026-03-09 16:33:54 -07:00
rbalsleyMSFT db22c1801d Simplifies products catalog request to use amd64
Always requests the amd64 products catalog regardless of the target architecture, as it already contains media entries for both x64 and arm64. This removes unnecessary architecture-specific branching.
2026-03-06 18:32:17 -08:00
rbalsleyMSFT 422bc33da7 Enhances file backup and cleanup for current runs
Improves the current-run cleanup mechanism by tracking file downloads explicitly. This ensures that files downloaded via BITS or preserving older timestamps are correctly identified and removed during cleanup. Extends the run manifest schema to support file backups, allowing for safe restoration of pre-existing scripts and configuration files modified during a run. Additional cleanup logic now correctly prunes residual empty directories after tracked files are removed.
2026-03-05 19:10:17 -08:00
rbalsleyMSFT 96603f025a Includes disk size in VHDX cache validation
Prevents reusing cached images when the requested disk size changes. Ensures the disk size property is properly saved and verified against existing cache items to maintain configuration accuracy.
2026-03-03 18:22:32 -08:00
rbalsleyMSFT a8fecd133e Improves VHDX optimization and caching logic
Extends the volume optimization helper to safely handle already mounted drives, resolve partition letters, and optionally execute a volume retrim. Replaces inline defragmentation steps with the updated helper prior to caching to ensure a fully compacted and optimized image is stored.
2026-03-03 17:36:44 -08:00
rbalsleyMSFT 7f10811c05 Introduces an option to retain downloaded ESD files
Adds a new configuration parameter and UI toggle to control whether downloaded Windows ESD files are removed after application. Updates the download process to check for and reuse existing ESD files that match the latest metadata filename, saving bandwidth and time on subsequent executions. Integrates the conditional deletion logic into the shared cleanup module.
2026-03-03 16:47:31 -08:00
rbalsleyMSFT f09c98906a Scopes select-all to visible filtered list items
Enhances grid view selection to optionally target only visible items when a filter is applied, preventing accidental selection or deselection of hidden rows via the header checkbox. Updates the save logic to process the unfiltered master dataset, ensuring that previously selected but currently hidden items are safely preserved during save operations.
2026-03-03 15:16:01 -08:00
rbalsleyMSFT 04dfb5f327 Normalizes Windows LTSC releases for OEM drivers
Adds logic to normalize LTSC/LTSB release years (such as 2016, 2019, 2021, and 2024) to their corresponding base Windows client versions (10 or 11). This ensures correct model retrieval and driver downloading, as OEM catalogs typically evaluate against base Windows versions rather than LTSC-specific release years.
2026-03-03 14:37:48 -08:00
rbalsleyMSFT dc801e9cc9 Adds dependency validation for copying drivers
Prevents an invalid configuration state by ensuring the build USB drive option is explicitly requested whenever the copy drivers option is specified.
2026-03-03 14:16:06 -08:00
rbalsleyMSFT a8e2ab941f Updates ADK detection to use executable paths
Improves the reliability of locating Windows ADK and WinPE add-on installations.
Updates registry searches to locate installer executables in the bundle cache path instead of relying on display names, avoiding potential mismatches.
Expands the registry queries to check both the WOW6432Node and standard uninstall paths to better support different installation contexts.
Validates the Windows Deployment Tools feature by verifying the existence of expected script files rather than checking the registry.
2026-03-03 14:02:52 -08:00
rbalsleyMSFT c83bc8c769 Updates script parameter documentation
Adds missing descriptions for cleanup, file copying, and download priority parameters to ensure complete help documentation.

Expands the documented supported values for Windows architectures, releases, and SKUs to accurately reflect expanded capabilities for ARM64, Windows Server, and LTSC deployments.

Corrects minor typos, updates default values to match the current implementation, and reorders parameters alphabetically to improve readability.
2026-02-25 14:25:47 -08:00
rbalsleyMSFT 0eb7f66c2b Updates documentation links with correct paths
Corrects internal markdown links by adding the proper directory path. Prevents broken navigation and ensures users are directed to the correct documentation pages.
2026-02-23 14:01:46 -08:00
rbalsleyMSFT d70615a32d Added Helper Scripts section of FFU Builder docs site
Added new docs for Create-PEMedia.ps1 and USBImagingToolCreator.ps1 files to help explain how to use the scripts.
2026-02-23 13:59:00 -08:00
rbalsleyMSFT 26694f30e3 More fixes for docs site indexing 2026-02-19 17:52:42 -08:00
rbalsleyMSFT 3563639ce0 Fixes for docs site indexing 2026-02-19 17:09:40 -08:00
rbalsleyMSFT a42f49e1fa site verification for docs site 2026-02-15 09:58:43 -08:00
rbalsleyMSFT 42b0b0c350 Fix: Windows 10 LTSB/LTSC Cumulative Update Installation
Since Windows 10 is out of support and only allows ESU updates, LTSB/LTSC builds are impacted by this and are unable to be offline serviced.

This commit fixes that by installing the CU in the VM, staging the update in a LTSCUpdate folder in the Apps folder.
2026-02-13 17:25:26 -08:00
rbalsleyMSFT 6e6abfe833 Clarifies script list comment
Removes misleading guidance about script order to keep documentation accurate.
2026-02-12 14:03:07 -08:00
rbalsleyMSFT baa696b880 Standardizes .NET update cache layout
Always stores .NET MSUs under a dedicated NET folder to keep cache structure consistent across OS variants

Migrates expected legacy root-stored .NET MSUs into the NET folder and prunes stale files from both locations to prevent buildup and mismatches
2026-02-12 13:19:05 -08:00
rbalsleyMSFT 27eebeb9cb Adds OS-scoped update cache folders
Prevents update packages for different Windows targets from mixing in a single cache, improving reuse across builds while avoiding DISM picking up stale MSUs.

Keeps pruning, download destinations, and update discovery confined to the OS/version-specific cache, including shared-branch handling for Windows 11 25H2/24H2 and LTSC mappings.
2026-02-11 23:17:55 -08:00
rbalsleyMSFT d349e5e4fb Prunes stale MSUs before servicing
Removes older update packages from the local update cache so servicing tools don’t pick up multiple stale MSUs as sources.

Keeps only the MSUs expected for the current run across Windows, .NET (including LTSC folder layout), and Microcode updates, while logging failures and continuing.
2026-02-11 00:06:03 -08:00
rbalsleyMSFT 53a47511d8 Updates README to highlight Quick Start
Improves first-time onboarding by pointing to the Quick Start Guide and the latest UI-based quickstart video.

Reorganizes video resources by moving legacy walkthroughs into an “Older” section and removes the outdated deep-dive chapter list.

Adds a brief note on UI maturity and upcoming consolidation toward the UI branch.
2026-02-09 17:45:22 -08:00
rbalsleyMSFT 518b4d4e62 Adds quickstart video walkthrough embed
Improves onboarding by embedding a privacy-enhanced YouTube walkthrough so readers can follow the setup steps visually.
2026-02-09 17:07:13 -08:00
rbalsleyMSFT a771136761 Adds external link handling script
Improves documentation UX by enabling consistent behavior for outbound links across pages
2026-02-09 16:59:42 -08:00
rbalsleyMSFT 36ee6f64bc added monitor.md 2026-02-09 16:33:51 -08:00
rbalsleyMSFT 2257b72255 Updates build page nav order
Keeps the docs sidebar ordering consistent after navigation reshuffling.
2026-02-09 16:21:36 -08:00
rbalsleyMSFT a65c9b5a18 Improves list indentation for better readability
Aligns list spacing with Learn-style docs by increasing indentation, preventing bullets/numbers from feeling cramped against content.
2026-02-05 14:11:40 -08:00
rbalsleyMSFT d38e461246 Adds responsive inline TOC with safe scrollspy
Improves navigation on narrow viewports by moving the TOC into the article and collapsing long lists with a show more/less toggle.

Prevents duplicate TOCs and reduces scroll “fighting” by disposing/reinitializing scrollspy on resize and keeping auto-scrolling behavior limited to the right-side desktop panel.
2026-02-05 13:19:39 -08:00
rbalsleyMSFT edc9901e7e Improves TOC layouts for long content
Keeps heading permalink icons visible by allowing horizontal overflow.

Prevents long code blocks and tables from overlapping the page TOC by enabling horizontal scrolling where needed.
2026-02-05 12:59:27 -08:00
rbalsleyMSFT 48b55df18e Update release date in README 2026-02-04 14:29:06 -08:00
rbalsleyMSFT cf9c605c34 Update release date and version in README 2026-01-13 11:26:28 -08:00
rbalsleyMSFT d6e7fd314f Update release date in README.md 2026-01-05 14:25:38 -08:00
rbalsleyMSFT a193c283f3 Revise release notes and download instructions
Updated release information and recommendations in the README.
2025-12-09 17:57:11 -08:00
rbalsleyMSFT 477d51fbbb Update release date in README.md 2025-10-01 17:42:35 -07:00
rbalsleyMSFT 3e3492bbab Update latest release information in README 2025-08-27 15:48:02 -07:00
rbalsleyMSFT 97e1ec2be4 Update UI Preview release information in README 2025-08-26 17:30:36 -07:00
rbalsleyMSFT 6e95ff92b1 fix: update script version to 2505.2 in BuildFFUVM.ps1 and ApplyFFU.ps1 for consistency across files 2025-08-12 17:47:25 -07:00
rbalsleyMSFT d4274d54d2 fix: Microsoft Update Catalog now includes the windows version information in the KB article title. This caused an issue where parsing the KB article was failing. Fixed the regex to accomdate this. 2025-08-12 17:33:26 -07:00
rbalsleyMSFT aee33a6a4b Update README.md 2025-08-01 20:30:35 -07:00
rbalsleyMSFT 25a0928195 Update README.md 2025-07-31 18:17:43 -07:00
31 changed files with 2054 additions and 382 deletions
+62
View File
@@ -1,5 +1,67 @@
# Change Log
# 2603.1
## What's Changed
### UI Out of Preview
The UI is finally out of preview and the code from the UI branch has been pushed to main.
### Remove old MSU files before servicing
Fixes an issue where older cumulative update MSU files were mixed with newer files. In Windows 11 24H2, dism treats all MSU files in the same folder as possible update sources, causing servicing issues.
This will primarily be an issue for those of you who use ISO files that don't have the latest updates, or those of you who create your FFUs with the ESD media shortly after patch Tuesday.
### Adds OS-scoped update KB folders
The KB folder where the CU and .NET updates downloads to will now have sub-folders for each Windows version. You may notice that if selecting 25H2, that the KB sub-folder for Windows 11 will show 24H2, this is to keep consistent with Windows 11 LTSC 2024 and Windows Server 2025, which all use the same 24H2 source OS (25H2 is just an eKB, not a full OS Swap Windows release like 24H2 was).
### .NET Updates stored under their own dedicated KB folder
This is more so to keep things clean, rather than fixing any sort of technical issue
### Reworked Windows 10 LTSB\LTSC Cumulative Update Installation
Since Windows 10 is out of support, offline servicing Windows 10 LTSC builds that are still supported fails due to extended security update requirements. The workaround to this is to install the updates in audit mode. FFU Builder now creates an Apps\LTSCUpdate folder and copies the LCU for Windows 10 LTSC builds still in support to the folder and installs the update in audit mode.
### Fixes an issue with Update ADK failing for non-English languages
Fixed a bug where updating the ADK would fail on non-English installations of Windows due to an assumption that the add/remove programs display information would be in English. Update ADK should correctly identify if the latest release of the ADK is installed and work as expected.
### Added dependency validation when selecting Build USB drive and Copy Drivers
Fixed an issue where a build would begin even though Copy Drivers to USB was set to true but no USB was inserted. FFU Builder will now check before the build gets started and inform the user if no USB drive is inserted.
### Normalizes Windows LTSC release versions to handle driver downloads
Driver downloads would fail if you were building certain LTSC releases due to FFU Builder incorrectly using the LTSC release year instead of the base Windows client version information. When build LTSC FFUs, the drivers should now download as expected.
### Scopes select-all to visible filtered list items in drivers listview
When filtering the drivers listview and selecting all driver models using the select all header checkbox, the select all behavior was selecting everything, even the hidden models in the list. If then selecting Download Selected, FFU Builder would download all models, even hidden ones. This now fixes that issue to only select all visible models and download those selected models.
### Retain downloaded ESD files
FFU Builder will now allow you to retain a downloaded ESD file. There's a new option on the Build tab to Remove Downloaded ESD File(s) which is checked by default to keep with the previous behavior. The intent here is to prevent from having to re-download the ESD file every time you're doing a build. This gives you another option along with Allow VHDX Caching to reduce the need of redownloading media.
### Reduce the size of cached VHDX files
Added some code to reduce the size of the cached VHDX files.
### Include disk size in VHDX cache validation
Prevents reusing cached images when the requested disk size changes. Ensures the disk size property is properly saved and verified against existing cache items to maintain configuration accuracy. This makes it so that if you create a new build with a larger disk size and have Allow VHDX caching selected, it won't use a cached VHDX with a smaller size.
### Enhances file backup and cleanup for cancelled builds
Improves the current-run cleanup mechanism by tracking file downloads explicitly. This ensures that files downloaded via BITS or preserving older timestamps are correctly identified and removed during cleanup. Extends the run manifest schema to support file backups, allowing for safe restoration of pre-existing scripts and configuration files modified during a run. Additional cleanup logic now correctly prunes residual empty directories after tracked files are removed.
### Fixed an issue with arm64 ESD downloads
With the change to how ESD downloads work with 25H2 and the Media Creation Tool, arm64 was broken. This was fixed.
# 2602.1 UI Preview
## What's Changed
@@ -28,8 +28,9 @@ Write-Host "---------------------------------------------------" -ForegroundColo
# Define the path to the scripts
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
# Define the list of scripts to run, order doesn't matter - if you have a custom script, add it here
# Define the list of scripts to run
$scriptList = @(
"Install-LTSCUpdate.ps1",
"Update-Defender.ps1",
"Install-Office.ps1",
"Update-MSRT.ps1",
File diff suppressed because it is too large Load Diff
+1
View File
@@ -837,6 +837,7 @@
<CheckBox x:Name="chkRemoveFFU" Content="Remove FFU" Margin="5" VerticalAlignment="Center" Tag="Remove FFU after copying to USB drive."/>
<CheckBox x:Name="chkRemoveApps" Content="Remove Apps Folder Content" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will remove the application content in the Apps folder after the FFU has been captured."/>
<CheckBox x:Name="chkRemoveUpdates" Content="Remove Downloaded Update Files" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will remove downloaded CU, .NET, MSRT, Defender, Edge, and OneDrive files after being applied/included."/>
<CheckBox x:Name="chkRemoveDownloadedESD" Content="Remove Downloaded ESD file(s)" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will remove downloaded Windows ESD file(s) after they are used."/>
</StackPanel>
</Grid>
</ScrollViewer>
@@ -16,12 +16,13 @@ function Invoke-FFUPostBuildCleanup {
[bool]$RemoveDrivers = $false,
[bool]$RemoveFFU = $false,
[bool]$RemoveApps = $false,
[bool]$RemoveUpdates = $false
[bool]$RemoveUpdates = $false,
[bool]$RemoveDownloadedESD = $false
)
$originalProgressPreference = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
try {
WriteLog "CommonCleanup: Starting cleanup (CaptureISO=$RemoveCaptureISO DeployISO=$RemoveDeployISO AppsISO=$RemoveAppsISO Drivers=$RemoveDrivers FFU=$RemoveFFU Apps=$RemoveApps Updates=$RemoveUpdates KBPath=$KBPath)."
WriteLog "CommonCleanup: Starting cleanup (CaptureISO=$RemoveCaptureISO DeployISO=$RemoveDeployISO AppsISO=$RemoveAppsISO Drivers=$RemoveDrivers FFU=$RemoveFFU Apps=$RemoveApps Updates=$RemoveUpdates RemoveDownloadedESD=$RemoveDownloadedESD KBPath=$KBPath)."
# Primary ISO paths (new naming/location)
if ($RemoveCaptureISO -and -not [string]::IsNullOrWhiteSpace($CaptureISOPath) -and (Test-Path -LiteralPath $CaptureISOPath)) {
@@ -95,6 +96,15 @@ function Invoke-FFUPostBuildCleanup {
}
}
# Always remove LTSC update staging folder (out-of-band cleanup exception)
if (-not [string]::IsNullOrWhiteSpace($AppsPath) -and (Test-Path -LiteralPath $AppsPath -PathType Container)) {
$ltscUpdateFolder = Join-Path $AppsPath 'LTSCUpdate'
if (Test-Path -LiteralPath $ltscUpdateFolder) {
WriteLog "CommonCleanup: Removing LTSC update staging folder $ltscUpdateFolder"
try { Remove-Item -LiteralPath $ltscUpdateFolder -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $ltscUpdateFolder : $($_.Exception.Message)" }
}
}
if ($RemoveUpdates) {
if (-not [string]::IsNullOrWhiteSpace($AppsPath) -and (Test-Path -LiteralPath $AppsPath)) {
# Remove per-run app update payloads stored under Apps
@@ -114,6 +124,20 @@ function Invoke-FFUPostBuildCleanup {
}
}
# Remove downloaded ESD files from the root path when requested
if ($RemoveDownloadedESD -and -not [string]::IsNullOrWhiteSpace($RootPath) -and (Test-Path -LiteralPath $RootPath -PathType Container)) {
WriteLog "CommonCleanup: Removing downloaded ESD files in $RootPath"
Get-ChildItem -LiteralPath $RootPath -Filter *.esd -File -ErrorAction SilentlyContinue | ForEach-Object {
try {
WriteLog "CommonCleanup: Removing ESD $($_.FullName)"
Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop
}
catch {
WriteLog "CommonCleanup: Failed removing ESD $($_.FullName) : $($_.Exception.Message)"
}
}
}
WriteLog "CommonCleanup: Completed."
}
catch {
@@ -157,6 +157,79 @@ function Invoke-Process {
return $cmd
}
function Get-RunManifestPathForDownloadTarget {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Destination
)
try {
$currentPath = Split-Path -Path $Destination -Parent
if ([string]::IsNullOrWhiteSpace($currentPath)) { return $null }
while ($currentPath) {
$manifestPath = Join-Path -Path $currentPath -ChildPath '.session\currentRun.json'
if (Test-Path -LiteralPath $manifestPath -PathType Leaf) {
return $manifestPath
}
$parentPath = Split-Path -Path $currentPath -Parent
if ([string]::IsNullOrWhiteSpace($parentPath) -or $parentPath -eq $currentPath) {
break
}
$currentPath = $parentPath
}
}
catch {
WriteLog "Get-RunManifestPathForDownloadTarget failed for '$Destination': $($_.Exception.Message)"
}
return $null
}
function Register-CurrentRunDownloadTarget {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Destination
)
if ([string]::IsNullOrWhiteSpace($Destination)) { return }
$manifestPath = Get-RunManifestPathForDownloadTarget -Destination $Destination
if ([string]::IsNullOrWhiteSpace($manifestPath)) { return }
$mutexName = 'Global\FFUCurrentRunDownloadTargetsMutex'
$mutex = New-Object System.Threading.Mutex($false, $mutexName)
try {
$null = $mutex.WaitOne()
$manifest = Get-Content -LiteralPath $manifestPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
if ($null -eq $manifest) { return }
if ($null -eq $manifest.PSObject.Properties['DownloadTargets']) {
Add-Member -InputObject $manifest -MemberType NoteProperty -Name DownloadTargets -Value @()
}
$downloadTargets = @($manifest.DownloadTargets)
if ($Destination -notin $downloadTargets) {
$downloadTargets += $Destination
$manifest.DownloadTargets = $downloadTargets
$manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
WriteLog "Registered current-run download target: $Destination"
}
}
catch {
WriteLog "Register-CurrentRunDownloadTarget failed for '$Destination': $($_.Exception.Message)"
}
finally {
try { $mutex.ReleaseMutex() | Out-Null } catch {}
$mutex.Dispose()
}
}
# Function to download a file using BITS with retry and error handling
function Start-BitsTransferWithRetry {
param (
@@ -181,6 +254,10 @@ function Start-BitsTransferWithRetry {
}
}
# Register destination so cancel cleanup can remove this run's downloaded files
# even when file timestamps are inherited from the source.
Register-CurrentRunDownloadTarget -Destination $Destination
$attempt = 0
$lastError = $null
$notLoggedOnHResult = [int]0x800704dd
@@ -82,6 +82,7 @@ function Get-UIConfig {
RemoveApps = $State.Controls.chkRemoveApps.IsChecked
RemoveFFU = $State.Controls.chkRemoveFFU.IsChecked
RemoveUpdates = $State.Controls.chkRemoveUpdates.IsChecked
RemoveDownloadedESD = $State.Controls.chkRemoveDownloadedESD.IsChecked
ShareName = $State.Controls.txtShareName.Text
UpdateADK = $State.Controls.chkUpdateADK.IsChecked
UpdateEdge = $State.Controls.chkUpdateEdge.IsChecked
@@ -461,6 +462,7 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'chkRemoveFFU' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'RemoveFFU' -State $State
Set-UIValue -ControlName 'chkRemoveApps' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'RemoveApps' -State $State
Set-UIValue -ControlName 'chkRemoveUpdates' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'RemoveUpdates' -State $State
Set-UIValue -ControlName 'chkRemoveDownloadedESD' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'RemoveDownloadedESD' -State $State
# Hyper-V Settings
Select-VMSwitchFromConfig -State $State -ConfigContent $ConfigContent
@@ -908,7 +910,8 @@ function Invoke-RestoreDefaults {
-RemoveDrivers:$true `
-RemoveFFU:$true `
-RemoveApps:$true `
-RemoveUpdates:$true
-RemoveUpdates:$true `
-RemoveDownloadedESD:$true
# Clear UI lists / state
if ($null -ne $State.Data.allDriverModels) { $State.Data.allDriverModels.Clear() }
@@ -212,6 +212,14 @@ function Save-DellDriversTask {
Remove-Item $modelCabPath -Force -ErrorAction SilentlyContinue
if (-not (Test-Path $modelXmlPath)) { throw "Model XML not found after extraction: $modelXmlPath" }
# Track extracted model XML so cancel cleanup can remove it even if file timestamps are preserved from source metadata.
try {
Register-CurrentRunDownloadTarget -Destination $modelXmlPath
}
catch {
WriteLog "Failed to register Dell model XML for current-run cleanup ($modelXmlPath): $($_.Exception.Message)"
}
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Selecting latest drivers...' }
$packages = Get-DellLatestDriverPackages -ModelXmlPath $modelXmlPath -WindowsArch $WindowsArch -WindowsRelease $WindowsRelease
}
@@ -35,10 +35,41 @@ function Get-DriverDisplayName {
return $BaseName.Trim()
}
return "$($BaseName.Trim()) ($($Identifier.Trim()))"
}
return "$($BaseName.Trim()) ($($Identifier.Trim()))"
}
function Convert-DriverItemToJsonModel {
function Get-EffectiveDriverWindowsRelease {
param(
[Parameter(Mandatory = $true)]
[int]$WindowsRelease,
[string]$WindowsReleaseDisplay,
[string]$WindowsSku
)
# Normalize LTSC/LTSB UI release selections to client driver releases for OEM catalogs.
if (-not [string]::IsNullOrWhiteSpace($WindowsReleaseDisplay)) {
if (($WindowsReleaseDisplay -like 'Windows 10*') -and (($WindowsReleaseDisplay -like '*LTSB*') -or ($WindowsReleaseDisplay -like '*LTSC*'))) {
return 10
}
if (($WindowsReleaseDisplay -like 'Windows 11*') -and ($WindowsReleaseDisplay -like '*LTSC*')) {
return 11
}
}
# Use SKU-based fallback when display text is unavailable.
if (-not [string]::IsNullOrWhiteSpace($WindowsSku) -and $WindowsSku -like '*LTS*') {
if ($WindowsRelease -in 2016, 2019, 2021) {
return 10
}
if ($WindowsRelease -eq 2024) {
return 11
}
}
return $WindowsRelease
}
function Convert-DriverItemToJsonModel {
param(
[Parameter(Mandatory = $true)]
[pscustomobject]$DriverItem
@@ -154,8 +185,20 @@ function Convert-DriverItemToJsonModel {
# Get necessary values from UI or script scope
$localDriversFolder = $State.Controls.txtDriversFolder.Text
$localWindowsRelease = $null
$localWindowsReleaseDisplay = $null
if ($null -ne $State.Controls.cmbWindowsRelease.SelectedItem) {
$localWindowsRelease = $State.Controls.cmbWindowsRelease.SelectedItem.Value
$localWindowsReleaseDisplay = $State.Controls.cmbWindowsRelease.SelectedItem.Display
}
# Resolve effective release used specifically for OEM driver operations.
$localWindowsSku = if ($null -ne $State.Controls.cmbWindowsSKU.SelectedItem) { [string]$State.Controls.cmbWindowsSKU.SelectedItem } else { $null }
$localDriverWindowsRelease = $localWindowsRelease
if ($null -ne $localWindowsRelease) {
$localDriverWindowsRelease = Get-EffectiveDriverWindowsRelease -WindowsRelease $localWindowsRelease -WindowsReleaseDisplay $localWindowsReleaseDisplay -WindowsSku $localWindowsSku
if ($localDriverWindowsRelease -ne $localWindowsRelease) {
WriteLog "Normalized WindowsRelease for model retrieval from $localWindowsRelease to $localDriverWindowsRelease (Display='$localWindowsReleaseDisplay', SKU='$localWindowsSku')."
}
}
# Get headers and user agent from Get-CoreStaticVariables
@@ -173,7 +216,7 @@ function Convert-DriverItemToJsonModel {
$rawModels = Get-MicrosoftDriversModelList -Headers $Headers -UserAgent $UserAgent -DriversFolder $localDriversFolder
}
'Dell' {
$rawModels = Get-DellDriversModelList -WindowsRelease $localWindowsRelease -DriversFolder $localDriversFolder -Make $SelectedMake
$rawModels = Get-DellDriversModelList -WindowsRelease $localDriverWindowsRelease -DriversFolder $localDriversFolder -Make $SelectedMake
}
'HP' {
$rawModels = Get-HPDriversModelList -DriversFolder $localDriversFolder -Make $SelectedMake
@@ -344,7 +387,10 @@ function Save-DriversJson {
[psobject]$State
)
WriteLog "Save-DriversJson function called."
$selectedDrivers = @($State.Controls.lstDriverModels.Items | Where-Object { $_.IsSelected })
# Save from the master model list so filtered-out selected rows are preserved.
$driverSelectionSource = if ($null -ne $State.Data.allDriverModels) { $State.Data.allDriverModels } else { $State.Controls.lstDriverModels.Items }
$selectedDrivers = @($driverSelectionSource | Where-Object { $_.IsSelected })
if (-not $selectedDrivers) {
[System.Windows.MessageBox]::Show("No drivers selected to save.", "Save Drivers", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
@@ -831,6 +877,12 @@ function Invoke-DownloadSelectedDrivers {
$localDriversFolder = $State.Controls.txtDriversFolder.Text
$localWindowsRelease = $State.Controls.cmbWindowsRelease.SelectedItem.Value
$localWindowsReleaseDisplay = $State.Controls.cmbWindowsRelease.SelectedItem.Display
$localWindowsSku = if ($null -ne $State.Controls.cmbWindowsSKU.SelectedItem) { [string]$State.Controls.cmbWindowsSKU.SelectedItem } else { $null }
$localDriverWindowsRelease = Get-EffectiveDriverWindowsRelease -WindowsRelease $localWindowsRelease -WindowsReleaseDisplay $localWindowsReleaseDisplay -WindowsSku $localWindowsSku
if ($localDriverWindowsRelease -ne $localWindowsRelease) {
WriteLog "Normalized WindowsRelease for driver download from $localWindowsRelease to $localDriverWindowsRelease (Display='$localWindowsReleaseDisplay', SKU='$localWindowsSku')."
}
$localWindowsArch = $State.Controls.cmbWindowsArch.SelectedItem
$localWindowsVersion = if ($null -ne $State.Controls.cmbWindowsVersion -and $null -ne $State.Controls.cmbWindowsVersion.SelectedItem) { $State.Controls.cmbWindowsVersion.SelectedItem } else { $null }
$coreStaticVars = Get-CoreStaticVariables
@@ -848,10 +900,10 @@ function Invoke-DownloadSelectedDrivers {
WriteLog "Dell drivers selected. Ensuring Dell Catalog is up-to-date..."
try {
$dellDriversFolder = Join-Path -Path $localDriversFolder -ChildPath "Dell"
$catalogBaseName = if ($localWindowsRelease -le 11) { "CatalogIndexPC" } else { "Catalog" }
$catalogBaseName = if ($localDriverWindowsRelease -le 11) { "CatalogIndexPC" } else { "Catalog" }
$dellCabFile = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).cab"
$dellCatalogXML = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).xml"
$catalogUrl = if ($localWindowsRelease -le 11) { "https://downloads.dell.com/catalog/CatalogIndexPC.cab" } else { "https://downloads.dell.com/catalog/Catalog.cab" }
$catalogUrl = if ($localDriverWindowsRelease -le 11) { "https://downloads.dell.com/catalog/CatalogIndexPC.cab" } else { "https://downloads.dell.com/catalog/Catalog.cab" }
$downloadCatalog = $true
if (Test-Path -Path $dellCatalogXML -PathType Leaf) {
@@ -891,7 +943,7 @@ function Invoke-DownloadSelectedDrivers {
$preserveSource = ($State.Controls.chkUseDriversAsPEDrivers.IsChecked -and $State.Controls.chkCompressDriversToWIM.IsChecked)
$taskArguments = @{
DriversFolder = $localDriversFolder
WindowsRelease = $localWindowsRelease
WindowsRelease = $localDriverWindowsRelease
WindowsArch = $localWindowsArch
WindowsVersion = $localWindowsVersion
Headers = $localHeaders
@@ -317,6 +317,9 @@ function Register-EventHandlers {
Update-WindowsVersionCombo -selectedRelease $selectedReleaseValue -isoPath $localState.Controls.txtISOPath.Text -State $localState
Update-WindowsSkuCombo -State $localState
Update-WindowsArchCombo -State $localState
# Re-evaluate Install Apps dependency when Windows release changes
Update-InstallAppsState -State $localState
})
$State.Controls.cmbWindowsVersion.Add_SelectionChanged({
@@ -369,6 +372,8 @@ function Register-EventHandlers {
$State.Controls.chkUpdateOneDrive.Add_Unchecked($updateCheckboxHandler)
$State.Controls.chkUpdateLatestMSRT.Add_Checked($updateCheckboxHandler)
$State.Controls.chkUpdateLatestMSRT.Add_Unchecked($updateCheckboxHandler)
$State.Controls.chkUpdateLatestCU.Add_Checked($updateCheckboxHandler)
$State.Controls.chkUpdateLatestCU.Add_Unchecked($updateCheckboxHandler)
# Also attach the handler to the Office checkbox
$State.Controls.chkInstallOffice.Add_Checked($updateCheckboxHandler)
@@ -135,6 +135,7 @@ function Initialize-UIControls {
$State.Controls.chkCleanupDeployISO = $window.FindName('chkCleanupDeployISO')
$State.Controls.chkCleanupDrivers = $window.FindName('chkCleanupDrivers')
$State.Controls.chkRemoveFFU = $window.FindName('chkRemoveFFU')
$State.Controls.chkRemoveDownloadedESD = $window.FindName('chkRemoveDownloadedESD')
$State.Controls.txtDiskSize = $window.FindName('txtDiskSize')
$State.Controls.txtMemory = $window.FindName('txtMemory')
$State.Controls.txtProcessors = $window.FindName('txtProcessors')
@@ -258,6 +259,7 @@ function Initialize-UIDefaults {
$State.Controls.chkRemoveFFU.IsChecked = $State.Defaults.generalDefaults.RemoveFFU
$State.Controls.chkRemoveApps.IsChecked = $State.Defaults.generalDefaults.RemoveApps
$State.Controls.chkRemoveUpdates.IsChecked = $State.Defaults.generalDefaults.RemoveUpdates
$State.Controls.chkRemoveDownloadedESD.IsChecked = $State.Defaults.generalDefaults.RemoveDownloadedESD
$State.Controls.chkVerbose.IsChecked = $State.Defaults.generalDefaults.Verbose
$State.Controls.usbSection.Visibility = if ($State.Controls.chkBuildUSBDriveEnable.IsChecked) { 'Visible' } else { 'Collapsed' }
$State.Controls.usbSelectionPanel.Visibility = if ($State.Controls.chkSelectSpecificUSBDrives.IsChecked) { 'Visible' } else { 'Collapsed' }
@@ -366,8 +368,8 @@ function Initialize-DynamicUIElements {
$driverModelsGridView = New-Object System.Windows.Controls.GridView
$State.Controls.lstDriverModels.View = $driverModelsGridView # Assign GridView to ListView first
# Add the selectable column using the new function
Add-SelectableGridViewColumn -ListView $State.Controls.lstDriverModels -State $State -HeaderCheckBoxKeyName "chkSelectAllDriverModels" -ColumnWidth 70
# Add the selectable column and scope header select-all to visible filtered rows.
Add-SelectableGridViewColumn -ListView $State.Controls.lstDriverModels -State $State -HeaderCheckBoxKeyName "chkSelectAllDriverModels" -ColumnWidth 70 -HeaderSelectionAffectsVisibleItemsOnly
# Add other sortable columns with left-aligned headers
Add-SortableColumn -gridView $driverModelsGridView -header "Make" -binding "Make" -width 100 -headerHorizontalAlignment Left
@@ -329,7 +329,8 @@ function Add-SelectableGridViewColumn {
[string]$HeaderCheckBoxKeyName,
[Parameter(Mandatory)]
[double]$ColumnWidth,
[string]$IsSelectedPropertyName = "IsSelected"
[string]$IsSelectedPropertyName = "IsSelected",
[switch]$HeaderSelectionAffectsVisibleItemsOnly
)
# Ensure the ListView has a GridView
@@ -343,10 +344,11 @@ function Add-SelectableGridViewColumn {
$headerCheckBox = New-Object System.Windows.Controls.CheckBox
$headerCheckBox.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Center
# MODIFICATION: Store the actual ListView object in the header's Tag
# Store header metadata, including whether select-all should only affect visible rows.
$headerTagObject = [PSCustomObject]@{
PropertyName = $IsSelectedPropertyName
ListViewControl = $ListView
PropertyName = $IsSelectedPropertyName
ListViewControl = $ListView
HeaderSelectionAffectsVisibleItemsOnly = [bool]$HeaderSelectionAffectsVisibleItemsOnly
}
$headerCheckBox.Tag = $headerTagObject
@@ -356,8 +358,24 @@ function Add-SelectableGridViewColumn {
$localPropertyName = $tagData.PropertyName
$actualListView = $tagData.ListViewControl
$collectionToUpdate = if ($null -ne $actualListView.ItemsSource) { $actualListView.ItemsSource } else { $actualListView.Items }
if ($null -ne $collectionToUpdate) {
# Select either visible view items only (filtered scope) or the full backing list.
$collectionToUpdate = @()
if ($tagData.HeaderSelectionAffectsVisibleItemsOnly -and $null -ne $actualListView.ItemsSource) {
$collectionView = [System.Windows.Data.CollectionViewSource]::GetDefaultView($actualListView.ItemsSource)
if ($null -ne $collectionView) {
foreach ($visibleItem in $collectionView) {
$collectionToUpdate += $visibleItem
}
}
}
elseif ($null -ne $actualListView.ItemsSource) {
$collectionToUpdate = @($actualListView.ItemsSource)
}
elseif ($actualListView.HasItems) {
$collectionToUpdate = @($actualListView.Items)
}
if ($collectionToUpdate.Count -gt 0) {
foreach ($item in $collectionToUpdate) { $item.$($localPropertyName) = $true }
$actualListView.Items.Refresh()
}
@@ -370,8 +388,24 @@ function Add-SelectableGridViewColumn {
$localPropertyName = $tagData.PropertyName
$actualListView = $tagData.ListViewControl
$collectionToUpdate = if ($null -ne $actualListView.ItemsSource) { $actualListView.ItemsSource } else { $actualListView.Items }
if ($null -ne $collectionToUpdate) {
# Clear either visible view items only (filtered scope) or the full backing list.
$collectionToUpdate = @()
if ($tagData.HeaderSelectionAffectsVisibleItemsOnly -and $null -ne $actualListView.ItemsSource) {
$collectionView = [System.Windows.Data.CollectionViewSource]::GetDefaultView($actualListView.ItemsSource)
if ($null -ne $collectionView) {
foreach ($visibleItem in $collectionView) {
$collectionToUpdate += $visibleItem
}
}
}
elseif ($null -ne $actualListView.ItemsSource) {
$collectionToUpdate = @($actualListView.ItemsSource)
}
elseif ($actualListView.HasItems) {
$collectionToUpdate = @($actualListView.Items)
}
if ($collectionToUpdate.Count -gt 0) {
foreach ($item in $collectionToUpdate) { $item.$($localPropertyName) = $false }
$actualListView.Items.Refresh()
}
@@ -446,24 +480,38 @@ function Update-SelectAllHeaderCheckBoxState {
[System.Windows.Controls.CheckBox]$HeaderCheckBox
)
$collectionToInspect = $null
if ($null -ne $ListView.ItemsSource) {
# Determine whether this header should evaluate only visible (filtered) rows.
$inspectVisibleItemsOnly = $false
if ($null -ne $HeaderCheckBox.Tag -and $null -ne $HeaderCheckBox.Tag.PSObject.Properties['HeaderSelectionAffectsVisibleItemsOnly']) {
$inspectVisibleItemsOnly = [bool]$HeaderCheckBox.Tag.HeaderSelectionAffectsVisibleItemsOnly
}
# Build the collection to inspect based on scope (visible view vs full source).
$collectionToInspect = @()
if ($inspectVisibleItemsOnly -and $null -ne $ListView.ItemsSource) {
$collectionView = [System.Windows.Data.CollectionViewSource]::GetDefaultView($ListView.ItemsSource)
if ($null -ne $collectionView) {
foreach ($visibleItem in $collectionView) {
$collectionToInspect += $visibleItem
}
}
}
elseif ($null -ne $ListView.ItemsSource) {
$collectionToInspect = @($ListView.ItemsSource)
}
elseif ($ListView.HasItems) {
# Check if Items collection has items and ItemsSource is null
$collectionToInspect = @($ListView.Items)
}
# If no items to inspect (either ItemsSource was null and Items was empty, or ItemsSource was empty)
if ($null -eq $collectionToInspect -or $collectionToInspect.Count -eq 0) {
# If no items are available in the selected scope, force unchecked.
if ($collectionToInspect.Count -eq 0) {
$HeaderCheckBox.IsChecked = $false
return
}
$selectedCount = ($collectionToInspect | Where-Object { $_.IsSelected }).Count
WriteLog "Update-SelectAllHeaderCheckBoxState: Selected count is $selectedCount for ListView '$($ListView.Name)'."
$totalItemCount = $collectionToInspect.Count # Get the total count from the collection being inspected
$totalItemCount = $collectionToInspect.Count
WriteLog "Update-SelectAllHeaderCheckBoxState: Total item count is $totalItemCount for ListView '$($ListView.Name)'."
if ($totalItemCount -eq 0) {
@@ -597,7 +645,7 @@ function Invoke-ListViewSort {
$val = $_.$property
if ($null -eq $val) { '' } else { $val }
}
Ascending = $State.Flags.lastSortAscending
Ascending = $State.Flags.lastSortAscending
}
$sortCriteria = [System.Collections.Generic.List[hashtable]]::new()
@@ -644,7 +692,7 @@ function Invoke-ListViewSort {
$val = Invoke-Command -ScriptBlock $expressionScriptBlock -ArgumentList $_
if ($null -eq $val) { '' } else { $val }
}
Ascending = $true
Ascending = $true
}
$sortCriteria.Add($secondarySortDefinition)
}
+24 -1
View File
@@ -141,6 +141,7 @@ function Get-GeneralDefaults {
RemoveFFU = $false
RemoveApps = $false
RemoveUpdates = $false
RemoveDownloadedESD = $true
# Hyper-V Settings Defaults
VMHostIPAddress = ""
DiskSizeGB = 50
@@ -320,6 +321,23 @@ function Update-ApplicationPanelVisibility {
}
}
# Function to identify whether current Windows release selection is Windows 10 LTSB/LTSC
function Test-IsWindows10LtscReleaseSelection {
param([PSCustomObject]$State)
$releaseItem = $State.Controls.cmbWindowsRelease.SelectedItem
if ($null -eq $releaseItem) {
return $false
}
$releaseDisplay = [string]$releaseItem.Display
if ([string]::IsNullOrWhiteSpace($releaseDisplay)) {
return $false
}
return (($releaseDisplay -like 'Windows 10*') -and (($releaseDisplay -like '*LTSB*') -or ($releaseDisplay -like '*LTSC*')))
}
# Function to manage the state of the main "Install Apps" checkbox based on selections in Updates/Office
function Update-InstallAppsState {
param([PSCustomObject]$State)
@@ -327,11 +345,16 @@ function Update-InstallAppsState {
$installAppsChk = $State.Controls.chkInstallApps
$installOfficeChk = $State.Controls.chkInstallOffice
# Determine if Windows 10 LTSB/LTSC + Update Latest CU is selected
$isWindows10LtscRelease = Test-IsWindows10LtscReleaseSelection -State $State
$isLtscCuChecked = $State.Controls.chkUpdateLatestCU.IsChecked -and $isWindows10LtscRelease
# Determine if any checkbox that forces "Install Apps" is checked
$anyUpdateChecked = $State.Controls.chkUpdateLatestDefender.IsChecked -or `
$State.Controls.chkUpdateEdge.IsChecked -or `
$State.Controls.chkUpdateOneDrive.IsChecked -or `
$State.Controls.chkUpdateLatestMSRT.IsChecked
$State.Controls.chkUpdateLatestMSRT.IsChecked -or `
$isLtscCuChecked
$isForced = $anyUpdateChecked -or $installOfficeChk.IsChecked
@@ -835,7 +835,7 @@ $LogFileName = 'ScriptLog.txt'
$USBDrive = Get-USBDrive
New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null
$LogFile = $USBDrive + $LogFilename
$version = '2602.1Preview'
$version = '2603.1'
WriteLog 'Begin Logging'
WriteLog "Script version: $version"
+5 -79
View File
@@ -18,88 +18,14 @@ And the best part: **it takes less than two minutes** to apply the image, even w
The Full-Flash update (FFU) process can automatically download the latest release of Windows 11, the updates mentioned above, and creates a USB drive that can be used to quickly reimage a machine.
# Updates
2509.1 has been released to preview! This is a major update that brings a new user interface to preview.
Docs are coming, but will take a bit to write them. The youtube video is a must watch for a complete demo on how to use the UI and the changes made to apps (InstallAppsAndSysprep.cmd is gone) and drivers. I'll be recording a more formalized deep dive with slides that go a bit deeper into how things work, but the UI walkthrough should get most people going.
# Getting Started
- Download the latest [release](https://github.com/rbalsleyMSFT/FFU/releases)
- Extract the FFUDevelopment folder from the ZIP file (recommend to C:\FFUDevelopment)
- Watch the Youtube video (updated docs for the UI coming soon)
If you're new, check out the [Quick Start Guide](https://rbalsleymsft.github.io/FFU/quickstart.html).
## YouTube Detailed Walkthrough
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.
Here's a detailed overview of the new UI process.
## Older Youtube Videos
[![Reimage Windows Fast with FFU Builder 2507.1 Preview](https://img.youtube.com/vi/oozG1aVcg9M/maxresdefault.jpg)](https://youtu.be/oozG1aVcg9M "Reimage Windows Fast with FFU Builder 2507.1 Preview")
[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.
Chapters:
[00:00](https://www.youtube.com/watch?v=oozG1aVcg9M&t=0s) Begin
[01:07](https://www.youtube.com/watch?v=oozG1aVcg9M&t=67s) Prereqs
[06:32](https://www.youtube.com/watch?v=oozG1aVcg9M&t=392s) Demo Begins
[07:16](https://www.youtube.com/watch?v=oozG1aVcg9M&t=436s) Running the BuildFFUVM_UI.ps1 script
[08:15](https://www.youtube.com/watch?v=oozG1aVcg9M&t=495s) UI Overview
[10:13](https://www.youtube.com/watch?v=oozG1aVcg9M&t=613s) Hyper-V Settings
[16:04](https://www.youtube.com/watch?v=oozG1aVcg9M&t=964s) Windows Settings
[22:35](https://www.youtube.com/watch?v=oozG1aVcg9M&t=1355s) Updates
[24:49](https://www.youtube.com/watch?v=oozG1aVcg9M&t=1489s) Applications
[29:39](https://www.youtube.com/watch?v=oozG1aVcg9M&t=1779s) Install Winget Applications
[45:29](https://www.youtube.com/watch?v=oozG1aVcg9M&t=2729s) Bring Your Own Applications
[54:14](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3254s) Apps Script Variables
[57:43](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3463s) M365 Apps/Office
[59:01](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3541s) Drivers
[01:01:22](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3682s) Drivers.json example
[01:02:07](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3727s) DriverMapping.json explanation
[01:06:08](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3968s) Driver WIM Compression
[01:10:50](https://www.youtube.com/watch?v=oozG1aVcg9M&t=4250s) Build
[01:12:41](https://www.youtube.com/watch?v=oozG1aVcg9M&t=4361s) Build USB Drive
[01:20:07](https://www.youtube.com/watch?v=oozG1aVcg9M&t=4807s) Monitor
[01:20:32](https://www.youtube.com/watch?v=oozG1aVcg9M&t=4832s) Setting up the Demo Build
[01:24:10](https://www.youtube.com/watch?v=oozG1aVcg9M&t=5050s) Save/Load Config Files
[01:25:11](https://www.youtube.com/watch?v=oozG1aVcg9M&t=5111s) Kicking off the Demo Build/Going over the monitor tab
[01:32:26](https://www.youtube.com/watch?v=oozG1aVcg9M&t=5546s) Demoing the new FFU Builder Orchestrator
[01:35:25](https://www.youtube.com/watch?v=oozG1aVcg9M&t=5725s) New captureffu.ps1 console output
[01:42:29](https://www.youtube.com/watch?v=oozG1aVcg9M&t=6149s) Demo Build Complete
[01:42:42](https://www.youtube.com/watch?v=oozG1aVcg9M&t=6162s) How to configure a VM to test your newly built FFU
[01:48:58](https://www.youtube.com/watch?v=oozG1aVcg9M&t=6538s) The moment of truth: What does the new deployment experience look like?
[01:53:13](https://www.youtube.com/watch?v=oozG1aVcg9M&t=6793s) How to bypass OOBE using a provisioning package
[01:55:49](https://www.youtube.com/watch?v=oozG1aVcg9M&t=6949s) Preview Focus Areas
[02:04:04](https://www.youtube.com/watch?v=oozG1aVcg9M&t=7444s) Known Issues/Things to fix before GA
[02:05:38](https://www.youtube.com/watch?v=oozG1aVcg9M&t=7538s) Providing Feedback
[02:06:43](https://www.youtube.com/watch?v=oozG1aVcg9M&t=7603s) Thank you
[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.
+3 -1
View File
@@ -4,10 +4,12 @@ remote_theme: just-the-docs/just-the-docs@v0.10.1
plugins:
- jekyll-remote-theme
- jekyll-seo-tag
- jekyll-sitemap
search_enabled: true
# Canonical host for absolute URLs (sitemap/robots/canonical tags)
url: "https://rbalsleymsft.github.io"
# Because youll publish as a project site at /FFU
baseurl: "/FFU"
+103 -10
View File
@@ -1,3 +1,9 @@
{% if page.url == '/' or page.url == '/index.html' %}
<meta name="google-site-verification" content="2O8GqDcQF_fvyvZjeTz-YlTaN2p62FfWd9w-xHU4Zbc" />
{% endif %}
{% if page.url == '/' or page.url == '/index.html' %}
<meta name="google-site-verification" content="2O8GqDcQF_fvyvZjeTz-YlTaN2p62FfWd9w-xHU4Zbc" />
{% endif %}
<!-- docs/_includes/head_custom.html -->
<style>
/* Layout: remove Just-the-Docs "centered narrow" constraints on wide screens */
@@ -57,6 +63,13 @@
line-height: 1.65;
}
/* Lists: increase indentation (Learn-like) */
/* Just-the-Docs draws bullets/numbers via ::before with negative offsets */
.main-content ul,
.main-content ol {
padding-left: 2.25em;
}
.main-content h1,
.main-content h2,
.main-content h3 {
@@ -75,7 +88,7 @@
word-break: break-word;
}
.main-content :not(pre) > code {
.main-content :not(pre)>code {
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
@@ -132,8 +145,16 @@
margin-right: 0;
justify-self: stretch;
/* Safety net: if anything still overflows, don't let it render under the TOC */
overflow-x: hidden;
/* Keep heading permalink icons visible */
/* (Just-the-Docs positions .anchor-heading to the left at desktop widths) */
overflow-x: visible;
}
/* Safety net: prevent long code/tables from overlapping the TOC */
.main-content-wrap.has-page-toc .main-content pre,
.main-content-wrap.has-page-toc .main-content .highlighter-rouge,
.main-content-wrap.has-page-toc .main-content .table-wrapper {
overflow-x: auto;
}
/* TOC always stays in the right column */
@@ -184,16 +205,88 @@
text-decoration: underline;
}
.page-toc__link.is-active {
font-weight: 600;
color: #1a1a1a;
border-left-color: #2563eb;
}
}
</style>
.page-toc__link.is-active {
font-weight: 600;
color: #1a1a1a;
border-left-color: #2563eb;
}
}
/* Inline "In this article" TOC (narrow viewports; Learn-like) */
.page-toc.page-toc--inline {
margin: 1.5rem 0;
padding-left: 1rem;
border-left: 1px solid #eeebee;
font-size: 0.875rem;
}
.page-toc--inline .page-toc__title {
font-weight: 600;
color: #27262b;
margin-bottom: 0.75rem;
}
.page-toc--inline .page-toc__list {
list-style: none !important;
padding-left: 0;
margin: 0;
}
.page-toc--inline .page-toc__item {
list-style: none !important;
margin: 0.4rem 0;
}
.page-toc--inline .page-toc__item::marker {
content: "";
}
.page-toc--inline .page-toc__item--h3 {
padding-left: 0.75rem;
}
.page-toc--inline .page-toc__link {
color: inherit;
text-decoration: none;
display: block;
padding: 0.125rem 0 0.125rem 0.75rem;
border-left: 3px solid transparent;
}
.page-toc--inline .page-toc__link:hover {
text-decoration: underline;
}
.page-toc__item.is-hidden {
display: none;
}
/* Just-the-Docs renders UL bullets via li::before; disable for inline TOC */
.page-toc--inline ul > li::before,
.page-toc--inline .page-toc__list > li::before,
.page-toc--inline .page-toc__item::before {
content: "" !important;
display: none !important;
}
.page-toc__toggle {
background: none;
border: 0;
padding: 0.25rem 0 0 0.75rem;
font: inherit;
color: inherit;
cursor: pointer;
text-decoration: none;
}
.page-toc__toggle:hover {
text-decoration: underline;
}
</style>
<meta name="ffu-right-toc" content="{% if page.right_toc == false %}false{% else %}true{% endif %}">
<script src="{{ '/assets/js/vendor/medium-zoom.min.js' | relative_url }}" defer></script>
<script src="{{ '/assets/js/image-zoom.js' | relative_url }}" defer></script>
<script src="{{ '/assets/js/page-toc.js' | relative_url }}" defer></script>
<script src="{{ '/assets/js/external-links.js' | relative_url }}" defer></script>
+91
View File
@@ -0,0 +1,91 @@
(function () {
'use strict';
function HasToken(tokens, token) {
for (var i = 0; i < tokens.length; i++) {
if (tokens[i] === token) {
return true;
}
}
return false;
}
function AddRelToken(anchor, token) {
var rel = (anchor.getAttribute('rel') || '').trim();
var tokens = rel ? rel.split(/\s+/) : [];
if (!HasToken(tokens, token)) {
tokens.push(token);
}
anchor.setAttribute('rel', tokens.join(' ').trim());
}
function IsExternalHttpLink(url) {
if (!url) {
return false;
}
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
return false;
}
return url.origin !== window.location.origin;
}
function InitExternalLinksNewTab() {
var mainContent = document.querySelector('.main-content');
if (!mainContent) {
return;
}
var anchors = mainContent.querySelectorAll('a[href]');
for (var i = 0; i < anchors.length; i++) {
var anchor = anchors[i];
var href = (anchor.getAttribute('href') || '').trim();
if (!href) {
continue;
}
if (href.charAt(0) === '#') {
continue;
}
if (href.indexOf('mailto:') === 0 || href.indexOf('tel:') === 0 || href.indexOf('javascript:') === 0) {
continue;
}
var url = null;
try {
url = new URL(href, window.location.href);
} catch (e) {
continue;
}
if (!IsExternalHttpLink(url)) {
continue;
}
var target = (anchor.getAttribute('target') || '').trim();
if (!target) {
anchor.setAttribute('target', '_blank');
target = '_blank';
}
if (target === '_blank') {
AddRelToken(anchor, 'noopener');
AddRelToken(anchor, 'noreferrer');
}
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', InitExternalLinksNewTab);
return;
}
InitExternalLinksNewTab();
})();
+165 -48
View File
@@ -1,6 +1,10 @@
(function () {
'use strict';
var scrollSpyDispose = null;
var resizeReinitTimerId = null;
var inlineMaxVisibleItems = 4;
function IsRightTocEnabled() {
var meta = document.querySelector('meta[name="ffu-right-toc"]');
if (meta && meta.content && meta.content.toLowerCase() === 'false') {
@@ -18,6 +22,47 @@
}
}
function RemoveExistingToc() {
if (scrollSpyDispose) {
scrollSpyDispose();
scrollSpyDispose = null;
}
var existingTocs = document.querySelectorAll('.page-toc');
for (var i = 0; i < existingTocs.length; i++) {
existingTocs[i].remove();
}
var wrap = document.querySelector('.main-content-wrap');
if (wrap) {
wrap.classList.remove('has-page-toc');
}
}
function InsertInlineToc(main, toc) {
if (!main || !toc) {
return;
}
var title = main.querySelector('h1');
if (title && title.parentNode === main) {
if (title.nextSibling) {
main.insertBefore(toc, title.nextSibling);
return;
}
main.appendChild(toc);
return;
}
if (main.firstChild) {
main.insertBefore(toc, main.firstChild);
return;
}
main.appendChild(toc);
}
function GetHeadings(container) {
var headings = container.querySelectorAll('h2, h3');
var results = [];
@@ -49,9 +94,13 @@
return results;
}
function BuildToc(headings) {
function BuildToc(headings, options) {
var variant = (options && options.variant) ? options.variant : 'right';
var maxVisible = (options && options.maxVisible) ? options.maxVisible : 0;
var isInline = 'inline' === variant;
var nav = document.createElement('nav');
nav.className = 'page-toc';
nav.className = 'page-toc' + (isInline ? ' page-toc--inline' : '');
nav.setAttribute('aria-label', 'On this page');
var title = document.createElement('div');
@@ -61,6 +110,7 @@
var list = document.createElement('ul');
list.className = 'page-toc__list';
list.id = 'page-toc-list';
for (var i = 0; i < headings.length; i++) {
var item = headings[i];
@@ -75,13 +125,61 @@
li.appendChild(a);
list.appendChild(li);
if (isInline && maxVisible > 0 && i >= maxVisible) {
li.classList.add('is-hidden');
}
}
nav.appendChild(list);
if (isInline && maxVisible > 0 && headings.length > maxVisible) {
var hiddenCount = headings.length - maxVisible;
var isExpanded = false;
var toggle = document.createElement('button');
toggle.type = 'button';
toggle.className = 'page-toc__toggle';
toggle.setAttribute('aria-controls', list.id);
toggle.setAttribute('aria-expanded', 'false');
function SetToggleText() {
if (isExpanded) {
toggle.textContent = 'Show less';
} else {
toggle.textContent = 'Show ' + hiddenCount + ' more';
}
}
function SetHiddenState() {
var items = list.querySelectorAll('.page-toc__item');
for (var j = 0; j < items.length; j++) {
if (j >= maxVisible) {
if (isExpanded) {
items[j].classList.remove('is-hidden');
} else {
items[j].classList.add('is-hidden');
}
}
}
toggle.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
SetToggleText();
}
toggle.addEventListener('click', function () {
isExpanded = !isExpanded;
SetHiddenState();
});
SetHiddenState();
nav.appendChild(toggle);
}
return nav;
}
function SetActiveTocLink(toc, activeId) {
function SetActiveTocLink(toc, activeId, keepVisibleInPanel) {
if (!toc) {
return;
}
@@ -95,11 +193,13 @@
if (isActive) {
link.classList.add('is-active');
/* Keep the active item visible inside the TOC panel */
try {
link.scrollIntoView({ block: 'nearest' });
} catch (e) {
link.scrollIntoView();
if (keepVisibleInPanel) {
/* Keep the active item visible inside the TOC panel (desktop/right TOC only) */
try {
link.scrollIntoView({ block: 'nearest' });
} catch (e) {
link.scrollIntoView();
}
}
} else {
link.classList.remove('is-active');
@@ -109,12 +209,12 @@
function SetupScrollSpy(main, toc, headings) {
if (!main || !toc || !headings || headings.length < 1) {
return;
return null;
}
/* Scrollspy is desktop-only; on mobile it can cause "fighting" scroll behavior */
/* Scrollspy is desktop-only */
if (!IsDesktopViewport()) {
return;
return null;
}
var headingElements = [];
@@ -126,7 +226,7 @@
}
if (headingElements.length < 1) {
return;
return null;
}
var activeId = null;
@@ -143,7 +243,7 @@
}
function GetCurrentHeadingId() {
/* If we're at the bottom, force the last heading active (Learn-like behavior) */
/* If we're at the bottom, force the last heading active */
if (IsNearBottomOfPage()) {
return headingElements[headingElements.length - 1].getAttribute('id');
}
@@ -177,6 +277,11 @@
function Update() {
ticking = false;
/* If the viewport becomes narrow after load, avoid scroll fighting */
if (!IsDesktopViewport()) {
return;
}
if (Date.now() < lockActiveUntilMs) {
return;
}
@@ -187,7 +292,7 @@
}
activeId = currentId;
SetActiveTocLink(toc, activeId);
SetActiveTocLink(toc, activeId, true);
}
function OnScrollOrResize() {
@@ -199,11 +304,7 @@
window.requestAnimationFrame(Update);
}
window.addEventListener('scroll', OnScrollOrResize, { passive: true });
window.addEventListener('resize', OnScrollOrResize);
/* Update immediately and also when clicking TOC links */
toc.addEventListener('click', function (evt) {
function OnTocClick(evt) {
var target = evt.target;
if (!target || !target.classList || !target.classList.contains('page-toc__link')) {
return;
@@ -223,29 +324,25 @@
lockActiveUntilMs = Date.now() + 800;
activeId = id;
SetActiveTocLink(toc, activeId);
});
SetActiveTocLink(toc, activeId, true);
}
window.addEventListener('scroll', OnScrollOrResize, { passive: true });
window.addEventListener('resize', OnScrollOrResize);
toc.addEventListener('click', OnTocClick);
Update();
return function DisposeScrollSpy() {
window.removeEventListener('scroll', OnScrollOrResize);
window.removeEventListener('resize', OnScrollOrResize);
toc.removeEventListener('click', OnTocClick);
};
}
function InitRightToc() {
if (!IsRightTocEnabled()) {
return;
}
/* Desktop-only TOC: on mobile it interferes with scrolling */
if (!IsDesktopViewport()) {
var existingWrap = document.querySelector('.main-content-wrap');
if (existingWrap) {
var existingToc = existingWrap.querySelector('.page-toc');
if (existingToc) {
existingToc.remove();
}
existingWrap.classList.remove('has-page-toc');
}
RemoveExistingToc();
return;
}
@@ -256,31 +353,51 @@
var headings = GetHeadings(main);
if (headings.length < 2) {
RemoveExistingToc();
return;
}
var wrap = document.querySelector('.main-content-wrap');
var content = document.querySelector('.main-content');
if (!wrap || !content) {
if (IsDesktopViewport()) {
RemoveExistingToc();
var wrap = document.querySelector('.main-content-wrap');
if (!wrap) {
return;
}
wrap.classList.add('has-page-toc');
var toc = BuildToc(headings, { variant: 'right' });
wrap.appendChild(toc);
scrollSpyDispose = SetupScrollSpy(main, toc, headings);
return;
}
if (wrap.querySelector('.page-toc')) {
return;
/* Narrow viewports: place TOC at the top of the article (Learn-like) */
RemoveExistingToc();
var inlineToc = BuildToc(headings, { variant: 'inline', maxVisible: inlineMaxVisibleItems });
InsertInlineToc(main, inlineToc);
}
function OnViewportResize() {
if (null !== resizeReinitTimerId) {
window.clearTimeout(resizeReinitTimerId);
}
wrap.classList.add('has-page-toc');
var toc = BuildToc(headings);
wrap.appendChild(toc);
SetupScrollSpy(main, toc, headings);
resizeReinitTimerId = window.setTimeout(InitRightToc, 150);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', InitRightToc);
document.addEventListener('DOMContentLoaded', function () {
InitRightToc();
window.addEventListener('resize', OnViewportResize);
});
return;
}
InitRightToc();
window.addEventListener('resize', OnViewportResize);
})();
+2 -2
View File
@@ -1,6 +1,6 @@
---
title: Build
nav_order: 9
nav_order: 10
prev_url: /drivers.html
prev_label: Drivers
next_url: /monitor.html
@@ -620,7 +620,7 @@ During the build process, when **Build Deploy ISO** is enabled, the script creat
You may want to disable Cleanup Deploy ISO in the following scenarios:
* **Creating deployment media separately**: When you want to create USB deployment drives at a later time (e.g. using `.\FFUDevelopment\USBImagingToolCreator.ps1`)
* **Creating deployment media separately**: When you want to create USB deployment drives at a later time, see [USB Imaging Tool Creator](/FFU/usb_imaging_tool_creator.html) for a staged workflow using `USBImagingToolCreator.ps1` with a deploy ISO, `FFU`, and `Drivers` folder (local path or network share).
* **Testing in Hyper-V**: When deploying FFU images to Hyper-V VMs for testing, you can attach the deploy ISO directly to a VM as a DVD drive
## Cleanup Drivers
+91
View File
@@ -0,0 +1,91 @@
---
title: Create PE Media
nav_order: 1
prev_url: /helper_scripts.html
prev_label: Helper Scripts
next_url: /usb_imaging_tool_creator.html
next_label: USB Imaging Tool Creator
parent: Helper Scripts
---
# Create PE Media
`Create-PEMedia.ps1` is a standalone helper script that creates WinPE capture or deployment ISO files outside the main build flow.
This is useful when admins need to quickly generate a deploy ISO for a share (or local staging folder) that technicians will use with `USBImagingToolCreator.ps1`.
## Common use case
If your staging location does not already have a deployment ISO, run `Create-PEMedia.ps1` to generate one, then copy that ISO to the staging folder used by your technicians.
## Prerequisites
- Run from an elevated PowerShell session.
- Windows ADK + WinPE add-on must be installed (default path: `C:\Program Files (x86)\Windows Kits\10\`).
- Script should be run from the `FFUDevelopment` folder (or provide explicit paths via parameters).
## Quick start (deploy ISO)
From `FFUDevelopment`, this creates a deploy ISO by default:
```powershell
.\Create-PEMedia.ps1
```
Default output file:
- `.\WinPE_FFU_Deploy_x64.iso`
## Useful commands
Create deploy ISO for x64:
```powershell
.\Create-PEMedia.ps1 -Deploy $true -WindowsArch 'x64'
```
Create deploy ISO for ARM64:
```powershell
.\Create-PEMedia.ps1 -Deploy $true -WindowsArch 'arm64' -DeployISO "$PSScriptRoot\WinPE_FFU_Deploy_arm64.iso"
```
Create capture ISO only:
```powershell
.\Create-PEMedia.ps1 -Capture $true -Deploy $false
```
Create deploy ISO and include PE drivers from `.\PEDrivers`:
```powershell
.\Create-PEMedia.ps1 -Deploy $true -CopyPEDrivers $true
```
## Stage output for USB imaging
After creating the deploy ISO, place it in the same staging root used for USB media creation.
Example:
```text
\\Server\FFUStaging\
WinPE_FFU_Deploy_x64.iso
FFU\
<image files>.ffu
Drivers\
<optional driver content>
```
Then technicians can run:
```powershell
.\USBImagingToolCreator.ps1 -DeployISOPath "\\Server\FFUStaging\WinPE_FFU_Deploy_x64.iso" -DisableAutoPlay
```
## Logging
`Create-PEMedia.ps1` writes log output to:
- `.\Create-PEMedia.log` (or custom path via `-LogFile`)
{% include page_nav.html %}
+20
View File
@@ -0,0 +1,20 @@
---
title: Helper Scripts
nav_order: 4
prev_url: /ui_overview.html
prev_label: UI Overview
next_url: /create_pemedia.html
next_label: Create PE Media
has_children: true
has_toc: false
---
# Helper Scripts
This section documents standalone helper scripts used outside the primary UI-driven build workflow.
## Available helper scripts
- [Create PE Media](/create_pemedia.html)
- [USB Imaging Tool Creator](/usb_imaging_tool_creator.html)
{% include page_nav.html %}
Binary file not shown.

After

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

+18
View File
@@ -0,0 +1,18 @@
---
title: Monitor
nav_order: 11
prev_url: /build.html
prev_label: Build
parent: UI Overview
---
# Monitor
![1770683109457](image/monitor/1770683109457.png)
The monitor tab parses the `.\FFUDevelopment\FFUDevelopment.log` file. This makes it easy to track what's happening during the FFU build process.
You can click into the monitor and select one or multiple lines (either single click, or shift+click). Doing so will prevent the log file from continuous scrolling, allowing you time to read what has transpired up to that point.
You can also copy your selection using ctrl+C. This makes it easy to copy and paste parts of the log file used for troubleshooting.
{% include page_nav.html %}
+102
View File
@@ -0,0 +1,102 @@
---
title: Parameters Reference
nav_order: 1
parent: Reference
prev_url: /reference.html
prev_label: Reference
---
# BuildFFUVM.ps1 Parameter Reference
This table lists all top-level parameters in BuildFFUVM.ps1.
<style>
.parameters-reference-table th:first-child,
.parameters-reference-table td:first-child {
white-space: nowrap;
}
</style>
| Parameter | Type | UI Control | Description |
| --- | --- | --- | --- |
| -AdditionalFFUFiles | string[] | Copy Additional FFU Files + Additional FFU Files list | Array of full file paths to existing FFU files that should also be copied to the deployment USB when -CopyAdditionalFFUFiles is set to $true. |
| -AllowExternalHardDiskMedia | bool | Allow External Hard Disk Media | When set to $true, will allow the use of media identified as External Hard Disk media via WMI class Win32_DiskDrive. Default is not defined. |
| -AllowVHDXCaching | bool | Allow VHDX Caching | When set to $true, will cache the VHDX file to the $FFUDevelopmentPath\VHDXCache folder and create a config json file that will keep track of the Windows build information, the updates installed, and the logical sector byte size information. Default is $false. |
| -AppListPath | string | AppList.json Path | Path to a JSON file containing a list of applications to install using WinGet. Default is $FFUDevelopmentPath\Apps\AppList.json. |
| -AppsScriptVariables | hashtable | Define Apps Script Variables + Apps Script Variables list | When passed a hashtable, the script will create an AppsScriptVariables.json file in the OrchestrationPath. This file will be used to pass variables to the Apps script. The hashtable should contain key-value pairs where the key is the variable name and the value is the variable value. |
| -BitsPriority | string | BITS Priority | BITS transfer priority used for downloads. Accepted values are 'Foreground', 'High', 'Normal', and 'Low'. Default is 'Normal'. |
| -BuildUSBDrive | bool | Build USB Drive | When set to $true, will partition and format a USB drive and copy the captured FFU to the drive. |
| -Cleanup | switch | Monitor cancel build action (no direct control) | Switch to run cleanup-only mode. When specified, the script performs cleanup and exits without starting a new build. |
| -CleanupAppsISO | bool | Cleanup Apps ISO | When set to $true, will remove the Apps ISO after the FFU has been captured. Default is $true. |
| -CleanupCaptureISO | bool | Cleanup Capture ISO | When set to $true, will remove the WinPE capture ISO after the FFU has been captured. Default is $true. |
| -CleanupCurrentRunDownloads | bool | Monitor cancel prompt option (no direct control) | When set to $true, cleanup mode will remove downloads created during the current run and restore backed up run JSON files. Default is $false. |
| -CleanupDeployISO | bool | Cleanup Deploy ISO | When set to $true, will remove the WinPE deployment ISO after the FFU has been captured. Default is $true. |
| -CleanupDrivers | bool | Cleanup Drivers | When set to $true, will remove the drivers folder after the FFU has been captured. Default is $true. |
| -CompactOS | bool | Compact OS | When set to $true, will compact the OS when building the FFU. Default is $true. |
| -CompressDownloadedDriversToWim | bool | Compress Driver Model Folder to WIM | When set to $true, compresses downloaded drivers into a WIM file. Default is $false. |
| -ConfigFile | string | Load Config File | Path to a JSON file containing parameters to use for the script. Default is $null. |
| -CopyAdditionalFFUFiles | bool | Copy Additional FFU Files | When set to $true, enables copying additional FFU files from $FFUDevelopmentPath\FFU to the deployment USB alongside the current build output. |
| -CopyAutopilot | bool | Copy Autopilot Profile | When set to $true, will copy the $FFUDevelopmentPath\Autopilot folder to the Deployment partition of the USB drive. Default is $false. |
| -CopyDrivers | bool | Copy Drivers to USB drive | When set to $true, will copy the drivers from the $FFUDevelopmentPath\Drivers folder to the Drivers folder on the deploy partition of the USB drive. Default is $false. |
| -CopyPEDrivers | bool | Copy PE Drivers | When set to $true, enables adding WinPE drivers. By default copies drivers from $FFUDevelopmentPath\PEDrivers to the WinPE deployment media unless -UseDriversAsPEDrivers is also $true. |
| -CopyPPKG | bool | Copy Provisioning Package | When set to $true, will copy the provisioning package from the $FFUDevelopmentPath\PPKG folder to the Deployment partition of the USB drive. Default is $false. |
| -CopyUnattend | bool | Copy Unattend.xml | When set to $true, will copy the $FFUDevelopmentPath\Unattend folder to the Deployment partition of the USB drive. Default is $false. |
| -CreateCaptureMedia | bool | Create Capture Media | When set to $true, this will create WinPE capture media for use when $InstallApps is set to $true. This capture media will be automatically attached to the VM, and the boot order will be changed to automate the capture of the FFU. |
| -CreateDeploymentMedia | bool | Create Deployment Media | When set to $true, this will create WinPE deployment media for use when deploying to a physical device. |
| -CustomFFUNameTemplate | string | Custom FFU Name Template | Sets a custom FFU output name with placeholders. Allowed placeholders are: {WindowsRelease}, {WindowsVersion}, {SKU}, {BuildDate}, {yyyy}, {MM}, {dd}, {H}, {hh}, {mm}, {tt}. |
| -Disksize | uint64 | Disk Size (GB) | Size of the virtual hard disk for the virtual machine. Default is a 50GB dynamic disk. |
| -DriversFolder | string | Drivers Folder | Path to the drivers folder. Default is $FFUDevelopmentPath\Drivers. |
| -DriversJsonPath | string | Drivers.json Path | Path to a JSON file that specifies which drivers to download. |
| -ExportConfigFile | string | Save Config File | Path to a JSON file to export the parameters used for the script. |
| -FFUCaptureLocation | string | FFU Capture Location | Path to the folder where the captured FFU will be stored. Default is $FFUDevelopmentPath\FFU. |
| -FFUDevelopmentPath | string | FFU Development Path | Path to the FFU development folder. Default is $PSScriptRoot. |
| -FFUPrefix | string | VM Name Prefix | Prefix for the generated FFU file. Default is _FFU. |
| -Headers | hashtable | CLI only (no UI control) | Headers to use when downloading files. Not recommended to modify. |
| -InjectUnattend | bool | Inject Unattend.xml | When set to $true and InstallApps is also $true, copies unattend_[arch].xml from $FFUDevelopmentPath\unattend to $FFUDevelopmentPath\Apps\Unattend\Unattend.xml so sysprep can use it inside the VM. Default is $false. |
| -InstallApps | bool | Install Applications | When set to $true, the script will create an Apps.iso file from the $FFUDevelopmentPath\Apps folder. It will also create a VM, mount the Apps.iso, install the apps, sysprep, and capture the VM. When set to $false, the FFU is created from a VHDX file, and no VM is created. |
| -InstallDrivers | bool | Install Drivers to FFU | Install device drivers from the specified $FFUDevelopmentPath\Drivers folder if set to $true. Download the drivers and put them in the Drivers folder. The script will recurse the drivers folder and add the drivers to the FFU. |
| -InstallOffice | bool | Install Office | Install Microsoft Office if set to $true. The script will download the latest ODT and Office files in the $FFUDevelopmentPath\Apps\Office folder and install Office in the FFU via VM. |
| -ISOPath | string | Windows ISO Path | Path to the Windows 10/11 ISO file. |
| -LogicalSectorSizeBytes | uint32 | Logical Sector Size | UInt32 value of 512 or 4096. Useful for 4Kn drives or devices shipping with UFS drives. Default is 512. |
| -Make | string | Make | Make of the device to download drivers. Accepted values are: 'Microsoft', 'Dell', 'HP', 'Lenovo'. |
| -MaxUSBDrives | int | Max USB Drives | Maximum number of USB drives to build in parallel. Default is 5. Set to 0 to process all discovered drives (or all selected drives when USBDriveList or selection is used). Actual throttle will never exceed the number of drives discovered. |
| -MediaType | string | Media Type | String value of either 'business' or 'consumer'. This is used to identify which media type to download. Default is 'consumer'. |
| -Memory | uint64 | Memory (GB) | Amount of memory to allocate for the virtual machine. Recommended to use 8GB if possible, especially for Windows 11. Default is 4GB. |
| -Model | string | Driver Models list | Model of the device to download drivers. This is required if Make is set. |
| -OfficeConfigXMLFile | string | Office Configuration XML File | Path to a custom Office configuration XML file to use for installation. |
| -Optimize | bool | Optimize | When set to $true, will optimize the FFU file. Default is $true. |
| -OptionalFeatures | string | Optional Features | Provide a semicolon-separated list of Windows optional features you want to include in the FFU (e.g., netfx3;TFTP). |
| -OrchestrationPath | string | Application Path (derived Orchestration path) | Path to the orchestration folder containing scripts that run inside the VM. Default is $FFUDevelopmentPath\Apps\Orchestration. |
| -PEDriversFolder | string | PE Drivers Folder | Path to the folder containing drivers to be injected into the WinPE deployment media. Default is $FFUDevelopmentPath\PEDrivers. |
| -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. |
| -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. |
| -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. |
| -ShareName | string | Share Name | Name of the shared folder for FFU capture. The default is FFUCaptureShare. This share will be created with rights for the user account. When finished, the share will be removed. |
| -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. |
| -UpdateADK | bool | Update ADK | When set to $true, the script will check for and install the latest Windows ADK and WinPE add-on if they are not already installed or up-to-date. Default is $true. |
| -UpdateEdge | bool | Update Edge | When set to $true, will download and install the latest Microsoft Edge. Default is $false. |
| -UpdateLatestCU | bool | Update Latest Cumulative Update | When set to $true, will download and install the latest cumulative update. Default is $false. |
| -UpdateLatestDefender | bool | Update Defender | When set to $true, will download and install the latest Windows Defender definitions and Defender platform update. Default is $false. |
| -UpdateLatestMicrocode | bool | Update Latest Microcode (for LTSC/Server 2016/2019) | When set to $true, will download and install the latest microcode updates for applicable Windows releases (e.g., Windows Server 2016/2019, Windows 10 LTSC 2016/2019) into the FFU. Default is $false. |
| -UpdateLatestMSRT | bool | Update Microsoft Software Removal Tool (MSRT) | When set to $true, will download and install the latest Windows Malicious Software Removal Tool. Default is $false. |
| -UpdateLatestNet | bool | Update .NET | When set to $true, will download and install the latest .NET Framework. Default is $false. |
| -UpdateOneDrive | bool | Update OneDrive (Per-Machine) | When set to $true, will download and install the latest OneDrive and install it as a per-machine installation instead of per-user. Default is $false. |
| -UpdatePreviewCU | bool | Update Preview Cumulative Update | When set to $true, will download and install the latest Preview cumulative update. Default is $false. |
| -USBDriveList | hashtable | USB Drives list | A hashtable containing USB drives from win32_diskdrive where:<br>- Key: USB drive model name (partial match supported)<br>- Value: USB drive UniqueId string, or an array of UniqueIds (to support selecting multiple drives with the same model)<br><br>Examples:<br>@{ "SanDisk Ultra" = "1234567890" }<br>@{ "SanDisk Ultra" = @("1234567890", "ABCDEFG"); "Kingston DataTraveler" = "0987654321" } |
| -UseDriversAsPEDrivers | bool | Use Drivers Folder as PE Drivers Source | When set to $true (and -CopyPEDrivers is also $true), bypasses the contents of $FFUDevelopmentPath\PEDrivers and instead builds the WinPE driver set dynamically from the $DriversFolder path, copying only the required WinPE drivers. Has no effect if -CopyPEDrivers is not specified. Default is $false. |
| -UserAgent | string | CLI only (no UI control) | User agent string to use when downloading files. |
| -UserAppListPath | string | Application Path (derived UserAppList.json) | Path to a JSON file containing a list of user-defined applications to install. Default is $FFUDevelopmentPath\Apps\UserAppList.json. |
| -Username | string | Username | Username for accessing the shared folder. The default is ffu_user. The script will auto-create the account and password. When finished, it will remove the account. |
| -VMHostIPAddress | string | VM Host IP Address | IP address of the Hyper-V host for FFU capture. If $InstallApps is set to $true, this parameter must be configured. You must manually configure this, or use the UI to auto-detect. |
| -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. If $InstallApps is set to $true, this must be set to capture the FFU from the VM. |
| -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'. |
| -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)'. |
| -WindowsVersion | string | Windows Version | String value of the Windows version to download. This is used to identify which version of Windows to download. Default is '25h2'. |
{: .parameters-reference-table }
{% include page_nav.html %}
+12
View File
@@ -25,6 +25,18 @@ After following this guide, you will have a USB drive with an FFU that contains
* Drivers (Optional)
* In some cases drivers aren't necessary and you can get away with Windows Update providing drivers. We'll go over how to add drivers via the UI for Microsoft, HP, Lenovo, or Dell devices
## Video Walkthrough
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;">
<iframe
src="https://www.youtube-nocookie.com/embed/kOIK5OmDugc"
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"
allowfullscreen>
</iframe>
</div>
## Prerequisites
Follow the [prerequisites](/FFU/prerequisites.html) documentation before getting started.
+19
View File
@@ -0,0 +1,19 @@
---
title: Reference
nav_order: 5
prev_url: /helper_scripts.html
prev_label: Helper Scripts
next_url: /parameters_reference.html
next_label: Parameters Reference
has_children: true
has_toc: false
---
# Reference
This section contains deep-dive documentation that explains how FFU Builder works behind the UI.
## Available reference guides
- [BuildFFUVM Parameter Reference](/FFU/parameters_reference.html)
{% include page_nav.html %}
+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://rbalsleymsft.github.io/FFU/sitemap.xml
+17
View File
@@ -0,0 +1,17 @@
---
layout: null
---
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% assign sortedPages = site.pages | sort: "url" %}
{% for page in sortedPages %}
{% if page.sitemap != false and page.url != "/404.html" %}
{% if page.url == "/" or page.url contains ".html" %}
<url>
<loc>{{ site.url }}{{ site.baseurl }}{{ page.url | replace: "index.html", "" }}</loc>
<lastmod>{{ site.time | date_to_xmlschema }}</lastmod>
</url>
{% endif %}
{% endif %}
{% endfor %}
</urlset>
+76
View File
@@ -0,0 +1,76 @@
---
title: USB Imaging Tool Creator
nav_order: 2
prev_url: /create_pemedia.html
prev_label: Create PE Media
parent: Helper Scripts
---
# USB Imaging Tool Creator
`USBImagingToolCreator.ps1` is a standalone helper for creating one or more deployment USB drives from a deploy ISO, FFU files, and optional drivers. This is best used when you want to provide remote technicians the FFU file(s) you've built and optionally a drivers folder that contains the drivers for the models they will need to manage. They can also provide their own drivers (using Drivers\Make\Model format (e.g Drivers\Dell\Optiplex 7060 (085D))
## How the script works
- `-DeployISOPath` is required and should point to the deploy ISO file.
- The script uses the parent folder of that ISO as its working root.
- FFU files are copied from `<ISO parent>\FFU` (all `.ffu` files, recursive).
- Drivers are copied from `<ISO parent>\Drivers` (recursive) when present.
- If drivers are not found, the script creates an empty `Drivers` folder on each deploy partition.
- `-DisableAutoPlay` is optional and temporarily disables AutoPlay for the current user during media creation. This is useful in situations where you see File Explorer pop ups as it's building the USB drive.
## Network share workflow (admin/technician)
For a shared workflow, stage one folder on a share with the deploy ISO and content that technicians should copy to USB drives.
If you do not already have a deployment ISO in the staging location, create one first using [Create PE Media](/FFU/create_pemedia.html). This lets admins quickly generate the deploy ISO and then stage it for technicians using `USBImagingToolCreator.ps1`.
Example layout:
```text
\\Server\FFUStaging\
WinPE_FFU_Deploy.iso
FFU\
<image files>.ffu
Drivers\
<optional driver content>
```
Run from an elevated PowerShell session:
```powershell
.\USBImagingToolCreator.ps1 -DeployISOPath "\\Server\FFUStaging\WinPE_FFU_Deploy.iso" -DisableAutoPlay
```
The script passes `-DeployISOPath` directly to `Mount-DiskImage`, so use a path the local Windows host can mount.
## Example folder structure
In this example a folder named USBCreator was made and the Drivers and FFU folders as well as the WinPE_FFU_Deploy_x64.iso and USBImagingToolCreator.ps1 files were copied from the FFUDevelopment folder to this new folder.
![1771882385742](image/usb_imaging_tool_creator/1771882385742.png)
## What happens when you run it
1. Detects disks with media type **Removable media** or **External hard disk media**.
2. Prompts for a single drive selection or an all-drives selection.
3. Stops `mmc` and `diskpart` processes to reduce drive lock issues.
4. Erases each selected disk and rebuilds it as MBR with:
- `Boot` partition (2 GB, FAT32, active)
- `Deploy` partition (remaining space, NTFS)
5. Mounts the deploy ISO and copies all ISO content to every `Boot` partition.
6. Copies FFU content to every `Deploy` partition.
7. Copies driver content into `Deploy\Drivers` (or creates an empty `Drivers` folder).
8. Dismounts the ISO and reports completion.
{: .warning-title}
> Warning
>
> Selected disks are fully erased (`Clear-Disk -RemoveData -RemoveOEM`), so verify drive selection carefully, especially when using the all-drives option.
## Logging and progress
- Progress is shown in the PowerShell progress UI.
- `Script.log` is written in the same folder as the deploy ISO (the working root folder).
{% include page_nav.html %}