Captures FFU directly from host-mounted VHDX

By optimizing and mounting the VHDX directly on the host for image capture, the build process no longer needs to boot the VM into WinPE, create SMB network shares, generate temporary local accounts, or rely on complex Hyper-V switch IP configurations. This streamlines the workflow and eliminates multiple networking and permission-related points of failure.

This change also removes the need to generate and attach WinPE capture media. All related parameters (`ShareName`, `Username`, `VMHostIPAddress`, `CreateCaptureMedia`, `CleanupCaptureISO`), UI controls, capture scripts, and documentation references have been removed or updated to reflect the simplified architecture.
This commit is contained in:
rbalsleyMSFT
2026-03-26 22:31:08 -07:00
parent 6db0f8c905
commit c135ad0fba
19 changed files with 170 additions and 811 deletions
+133 -345
View File
@@ -36,9 +36,6 @@ Switch to run cleanup-only mode. When specified, the script performs cleanup and
.PARAMETER CleanupAppsISO
When set to $true, will remove the Apps ISO after the FFU has been captured. Default is $true.
.PARAMETER CleanupCaptureISO
When set to $true, will remove the WinPE capture ISO after the FFU has been captured. Default is $true.
.PARAMETER CleanupCurrentRunDownloads
When set to $true, cleanup mode will remove downloads created during the current run and restore backed up run JSON files. Default is $false.
@@ -75,9 +72,6 @@ When set to $true, will copy the provisioning package from the $FFUDevelopmentPa
.PARAMETER CopyUnattend
When set to $true, will copy the $FFUDevelopmentPath\Unattend folder to the Deployment partition of the USB drive. Default is $false.
.PARAMETER CreateCaptureMedia
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.
.PARAMETER CreateDeploymentMedia
When set to $true, this will create WinPE deployment media for use when deploying to a physical device.
@@ -177,9 +171,6 @@ When set to $true, will remove the downloaded CU, MSRT, Defender, Edge, OneDrive
.PARAMETER RemoveDownloadedESD
When set to $true, will remove downloaded Windows ESD files after they have been applied. Default is $true.
.PARAMETER ShareName
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.
.PARAMETER 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.
@@ -228,17 +219,11 @@ User agent string to use when downloading files.
.PARAMETER UserAppListPath
Path to a JSON file containing a list of user-defined applications to install. Default is $FFUDevelopmentPath\Apps\UserAppList.json.
.PARAMETER 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.
.PARAMETER VMHostIPAddress
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.
.PARAMETER VMLocation
Default is $FFUDevelopmentPath\VM. This is the location of the VHDX that gets created where Windows will be installed to.
.PARAMETER VMSwitchName
Name of the Hyper-V virtual switch. If $InstallApps is set to $true, this must be set to capture the FFU from the VM.
Name of the Hyper-V virtual switch. Optional when building with InstallApps. Provide it only if the VM needs network connectivity during provisioning.
.PARAMETER WindowsArch
String value of 'x86', 'x64', or 'arm64'. This is used to identify which architecture of Windows to download. Default is 'x64'.
@@ -257,25 +242,25 @@ String value of the Windows version to download. This is used to identify which
.EXAMPLE
Command line for most people who want to download the latest Windows 11 Pro x64 media in English (US) with the latest Windows Cumulative Update, .NET Framework, Defender platform and definition updates, Edge, OneDrive, and Office/M365 Apps. It will also copy drivers to the FFU. This can take about 40 minutes to create the FFU due to the time it takes to download and install the updates.
.\BuildFFUVM.ps1 -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -VMSwitchName 'Name of your VM Switch in Hyper-V' -VMHostIPAddress 'Your IP Address' -CreateCaptureMedia $true -CreateDeploymentMedia $true -BuildUSBDrive $true -UpdateLatestCU $true -UpdateLatestNet $true -UpdateLatestDefender $true -UpdateEdge $true -UpdateOneDrive $true -verbose
.\BuildFFUVM.ps1 -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -CreateDeploymentMedia $true -BuildUSBDrive $true -UpdateLatestCU $true -UpdateLatestNet $true -UpdateLatestDefender $true -UpdateEdge $true -UpdateOneDrive $true -verbose
Command line for most people who want to create an FFU with Office and drivers and have downloaded their own ISO. This assumes you have copied this script and associated files to the C:\FFUDevelopment folder. If you need to use another drive or folder, change the -FFUDevelopment parameter (e.g. -FFUDevelopment 'D:\FFUDevelopment')
.\BuildFFUVM.ps1 -ISOPath 'C:\path_to_iso\Windows.iso' -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -VMSwitchName 'Name of your VM Switch in Hyper-V' -VMHostIPAddress 'Your IP Address' -CreateCaptureMedia $true -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose
.\BuildFFUVM.ps1 -ISOPath 'C:\path_to_iso\Windows.iso' -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose
Command line for those who just want a FFU with no drivers, apps, or Office and have downloaded their own ISO.
.\BuildFFUVM.ps1 -ISOPath 'C:\path_to_iso\Windows.iso' -WindowsSKU 'Pro' -Installapps $false -InstallOffice $false -InstallDrivers $false -CreateCaptureMedia $false -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose
.\BuildFFUVM.ps1 -ISOPath 'C:\path_to_iso\Windows.iso' -WindowsSKU 'Pro' -Installapps $false -InstallOffice $false -InstallDrivers $false -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose
Command line for those who just want a FFU with Apps and drivers, no Office and have downloaded their own ISO.
.\BuildFFUVM.ps1 -ISOPath 'C:\path_to_iso\Windows.iso' -WindowsSKU 'Pro' -Installapps $true -InstallOffice $false -InstallDrivers $true -VMSwitchName 'Name of your VM Switch in Hyper-V' -VMHostIPAddress 'Your IP Address' -CreateCaptureMedia $true -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose
.\BuildFFUVM.ps1 -ISOPath 'C:\path_to_iso\Windows.iso' -WindowsSKU 'Pro' -Installapps $true -InstallOffice $false -InstallDrivers $true -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose
Command line for those who want to download the latest Windows 11 Pro x64 media in English (US) and install the latest version of Office and drivers.
.\BuildFFUVM.ps1 -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -VMSwitchName 'Name of your VM Switch in Hyper-V' -VMHostIPAddress 'Your IP Address' -CreateCaptureMedia $true -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose
.\BuildFFUVM.ps1 -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose
Command line for those who want to download the latest Windows 11 Pro x64 media in French (CA) and install the latest version of Office and drivers.
.\BuildFFUVM.ps1 -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -VMSwitchName 'Name of your VM Switch in Hyper-V' -VMHostIPAddress 'Your IP Address' -CreateCaptureMedia $true -CreateDeploymentMedia $true -BuildUSBDrive $true -WindowsRelease 11 -WindowsArch 'x64' -WindowsLang 'fr-ca' -MediaType 'consumer' -verbose
.\BuildFFUVM.ps1 -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -CreateDeploymentMedia $true -BuildUSBDrive $true -WindowsRelease 11 -WindowsArch 'x64' -WindowsLang 'fr-ca' -MediaType 'consumer' -verbose
Command line for those who want to download the latest Windows 11 Pro x64 media in English (US) and install the latest version of Office and drivers.
.\BuildFFUVM.ps1 -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -VMSwitchName 'Name of your VM Switch in Hyper-V' -VMHostIPAddress 'Your IP Address' -CreateCaptureMedia $true -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose
.\BuildFFUVM.ps1 -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose
.NOTES
Additional notes about your script.
@@ -336,12 +321,7 @@ param(
[string]$VMLocation,
[string]$FFUPrefix = '_FFU',
[string]$FFUCaptureLocation,
[string]$ShareName = "FFUCaptureShare",
[string]$Username = "ffu_user",
[string]$CustomFFUNameTemplate,
[Parameter(Mandatory = $false)]
[string]$VMHostIPAddress,
[bool]$CreateCaptureMedia = $true,
[bool]$CreateDeploymentMedia,
[ValidateScript({
$allowedFeatures = @("Windows-Defender-Default-Definitions", "Printing-PrintToPDFServices-Features", "Printing-XPSServices-Features", "TelnetClient", "TFTP",
@@ -425,7 +405,6 @@ param(
[bool]$CopyUnattend,
[bool]$CopyAutopilot,
[bool]$CompactOS = $true,
[bool]$CleanupCaptureISO = $true,
[bool]$CleanupDeployISO = $true,
[bool]$CleanupAppsISO = $true,
[bool]$RemoveUpdates = $true,
@@ -647,7 +626,6 @@ if (-not $LtscCUStagePath) { $LtscCUStagePath = "$AppsPath\LTSCUpdate" }
if (-not $AppsScriptVarsJsonPath) { $AppsScriptVarsJsonPath = "$OrchestrationPath\AppsScriptVariables.json" }
if (-not $DeployISO) { $DeployISO = "$FFUDevelopmentPath\WinPE_FFU_Deploy_$WindowsArch.iso" }
if (-not $CaptureISO) { $CaptureISO = "$FFUDevelopmentPath\WinPE_FFU_Capture_$WindowsArch.iso" }
if (-not $OfficePath) { $OfficePath = "$AppsPath\Office" }
if (-not $OfficeDownloadXML) { $OfficeDownloadXML = "$OfficePath\DownloadFFU.xml" }
if (-not $OfficeInstallXML) { $OfficeInstallXML = "DeployFFU.xml" }
@@ -2848,69 +2826,6 @@ function New-FFUVM {
return $VM
}
Function Set-CaptureFFU {
$CaptureFFUScriptPath = "$FFUDevelopmentPath\WinPECaptureFFUFiles\CaptureFFU.ps1"
# Workaround for PowerShell 7 issue on Windows 11 23H2 and earlier
# https://github.com/PowerShell/PowerShell/issues/21645
$osBuild = (Get-CimInstance -ClassName Win32_OperatingSystem).BuildNumber
if ($osBuild -le 22631) {
WriteLog "Applying workaround for PowerShell 7 LocalAccounts module issue on Windows 11 build $osBuild"
Import-Module Microsoft.PowerShell.LocalAccounts -UseWindowsPowerShell
}
If (-not (Test-Path -Path $FFUCaptureLocation)) {
WriteLog "Creating FFU capture location at $FFUCaptureLocation"
New-Item -Path $FFUCaptureLocation -ItemType Directory -Force
WriteLog "Successfully created FFU capture location at $FFUCaptureLocation"
}
# Create a standard user
$UserExists = Get-LocalUser -Name $UserName -ErrorAction SilentlyContinue
if (-not $UserExists) {
WriteLog "Creating FFU_User account as standard user"
New-LocalUser -Name $UserName -AccountNeverExpires -NoPassword | Out-null
WriteLog "Successfully created FFU_User account"
}
# Create a random password for the standard user
$Password = New-Guid | Select-Object -ExpandProperty Guid
$SecurePassword = ConvertTo-SecureString -String $Password -AsPlainText -Force
Set-LocalUser -Name $UserName -Password $SecurePassword -PasswordNeverExpires:$true
# Create a share of the $FFUCaptureLocation variable
$ShareExists = Get-SmbShare -Name $ShareName -ErrorAction SilentlyContinue
if (-not $ShareExists) {
WriteLog "Creating $ShareName and giving access to $UserName"
New-SmbShare -Name $ShareName -Path $FFUCaptureLocation -FullAccess $UserName | Out-Null
WriteLog "Share created"
}
# Return the share path in the format of \\<IPAddress>\<ShareName> /user:<UserName> <password>
$SharePath = "\\$VMHostIPAddress\$ShareName /user:$UserName $Password"
$SharePath = "net use W: " + $SharePath + ' 2>&1'
# Update CaptureFFU.ps1 script
if (Test-Path -Path $CaptureFFUScriptPath) {
$ScriptContent = Get-Content -Path $CaptureFFUScriptPath
#Update variables in CaptureFFU.ps1 script ($VMHostIPAddress, $ShareName, $UserName, $Password)
WriteLog 'Updating CaptureFFU.ps1 script with new share information'
$ScriptContent = $ScriptContent -replace '(\$VMHostIPAddress = ).*', "`$1'$VMHostIPAddress'"
$ScriptContent = $ScriptContent -replace '(\$ShareName = ).*', "`$1'$ShareName'"
$ScriptContent = $ScriptContent -replace '(\$UserName = ).*', "`$1'$UserName'"
$ScriptContent = $ScriptContent -replace '(\$Password = ).*', "`$1'$Password'"
if (![string]::IsNullOrEmpty($CustomFFUNameTemplate)) {
$ScriptContent = $ScriptContent -replace '(\$CustomFFUNameTemplate = ).*', "`$1'$CustomFFUNameTemplate'"
WriteLog 'Updating CaptureFFU.ps1 script with new ffu name template information'
}
Set-Content -Path $CaptureFFUScriptPath -Value $ScriptContent
WriteLog 'Update complete'
}
else {
throw "CaptureFFU.ps1 script not found at $CaptureFFUScriptPath"
}
}
function Get-PrivateProfileString {
param (
[Parameter()]
@@ -3371,12 +3286,7 @@ function Copy-Drivers {
}
function New-PEMedia {
param (
[Parameter()]
[bool]$Capture,
[Parameter()]
[bool]$Deploy
)
param ()
#Need to use the Demployment and Imaging tools environment to create winPE media
$DandIEnv = "$adkPath`Assessment and Deployment Kit\Deployment Tools\DandISetEnv.bat"
$WinPEFFUPath = "$FFUDevelopmentPath\WinPE"
@@ -3431,64 +3341,49 @@ function New-PEMedia {
Add-WindowsPackage -Path "$WinPEFFUPath\mount" -PackagePath $PackagePath | Out-Null
WriteLog "Adding package complete"
}
If ($Capture) {
WriteLog "Copying $FFUDevelopmentPath\WinPECaptureFFUFiles\* to WinPE capture media"
Copy-Item -Path "$FFUDevelopmentPath\WinPECaptureFFUFiles\*" -Destination "$WinPEFFUPath\mount" -Recurse -Force | out-null
WriteLog "Copy complete"
#Remove Bootfix.bin - for BIOS systems, shouldn't be needed, but doesn't hurt to remove for our purposes
#Remove-Item -Path "$WinPEFFUPath\media\boot\bootfix.bin" -Force | Out-null
# $WinPEISOName = 'WinPE_FFU_Capture.iso'
$WinPEISOFile = $CaptureISO
# $Capture = $false
}
If ($Deploy) {
WriteLog "Copying $FFUDevelopmentPath\WinPEDeployFFUFiles\* to WinPE deploy media"
Copy-Item -Path "$FFUDevelopmentPath\WinPEDeployFFUFiles\*" -Destination "$WinPEFFUPath\mount" -Recurse -Force | Out-Null
WriteLog 'Copy complete'
#If $CopyPEDrivers = $true, add drivers to WinPE media using dism
if ($CopyPEDrivers) {
if ($UseDriversAsPEDrivers) {
WriteLog "UseDriversAsPEDrivers is set. Building WinPE driver set from Drivers folder (bypassing PEDrivers folder contents)."
if (Test-Path -Path $PEDriversFolder) {
try {
Remove-Item -Path (Join-Path $PEDriversFolder '*') -Recurse -Force -ErrorAction SilentlyContinue | Out-Null
}
catch {
WriteLog "Warning: Failed clearing existing PEDriversFolder contents: $($_.Exception.Message)"
}
WriteLog "Copying $FFUDevelopmentPath\WinPEDeployFFUFiles\* to WinPE deploy media"
Copy-Item -Path "$FFUDevelopmentPath\WinPEDeployFFUFiles\*" -Destination "$WinPEFFUPath\mount" -Recurse -Force | Out-Null
WriteLog 'Copy complete'
#If $CopyPEDrivers = $true, add drivers to WinPE media using dism
if ($CopyPEDrivers) {
if ($UseDriversAsPEDrivers) {
WriteLog "UseDriversAsPEDrivers is set. Building WinPE driver set from Drivers folder (bypassing PEDrivers folder contents)."
if (Test-Path -Path $PEDriversFolder) {
try {
Remove-Item -Path (Join-Path $PEDriversFolder '*') -Recurse -Force -ErrorAction SilentlyContinue | Out-Null
}
else {
try {
New-Item -Path $PEDriversFolder -ItemType Directory -Force | Out-Null
}
catch {
WriteLog "Error: Failed to create PEDriversFolder at $PEDriversFolder - continuing may fail when adding drivers."
}
catch {
WriteLog "Warning: Failed clearing existing PEDriversFolder contents: $($_.Exception.Message)"
}
WriteLog "Copying required WinPE drivers from Drivers folder"
Copy-Drivers -Path $DriversFolder -Output $PEDriversFolder
}
else {
WriteLog "Copying PE drivers from PEDrivers folder"
try {
New-Item -Path $PEDriversFolder -ItemType Directory -Force | Out-Null
}
catch {
WriteLog "Error: Failed to create PEDriversFolder at $PEDriversFolder - continuing may fail when adding drivers."
}
}
WriteLog "Adding drivers to WinPE media"
try {
$WinPEMount = "$WinPEFFUPath\Mount"
# Inject drivers using deep SUBST mapping (reuse one drive letter and loop each INF folder)
Invoke-DismDriverInjectionWithSubstLoop -ImagePath $WinPEMount -DriverRoot $PEDriversFolder
}
catch {
WriteLog 'Some drivers failed to be added. This can be expected. Continuing.'
}
WriteLog "Adding drivers complete"
WriteLog "Copying required WinPE drivers from Drivers folder"
Copy-Drivers -Path $DriversFolder -Output $PEDriversFolder
}
# $WinPEISOName = 'WinPE_FFU_Deploy.iso'
$WinPEISOFile = $DeployISO
else {
WriteLog "Copying PE drivers from PEDrivers folder"
}
WriteLog "Adding drivers to WinPE media"
try {
$WinPEMount = "$WinPEFFUPath\Mount"
# $Deploy = $false
# Inject drivers using deep SUBST mapping (reuse one drive letter and loop each INF folder)
Invoke-DismDriverInjectionWithSubstLoop -ImagePath $WinPEMount -DriverRoot $PEDriversFolder
}
catch {
WriteLog 'Some drivers failed to be added. This can be expected. Continuing.'
}
WriteLog "Adding drivers complete"
}
$WinPEISOFile = $DeployISO
WriteLog 'Dismounting WinPE media'
Dismount-WindowsImage -Path "$WinPEFFUPath\mount" -Save | Out-Null
WriteLog 'Dismount complete'
@@ -3503,21 +3398,10 @@ function New-PEMedia {
WriteLog "Creating WinPE ISO at $WinPEISOFile"
# & "$OSCDIMG" -m -o -u2 -udfver102 -bootdata:2`#p0,e,b$OSCDIMGPath\etfsboot.com`#pEF,e,b$OSCDIMGPath\Efisys_noprompt.bin $WinPEFFUPath\media $FFUDevelopmentPath\$WinPEISOName | Out-null
if ($WindowsArch -eq 'x64') {
if ($Capture) {
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:2`#p0,e,b`"$OSCDIMGPath\etfsboot.com`"`#pEF,e,b`"$OSCDIMGPath\Efisys_noprompt.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
}
if ($Deploy) {
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:2`#p0,e,b`"$OSCDIMGPath\etfsboot.com`"`#pEF,e,b`"$OSCDIMGPath\Efisys.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
}
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:2`#p0,e,b`"$OSCDIMGPath\etfsboot.com`"`#pEF,e,b`"$OSCDIMGPath\Efisys.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
}
elseif ($WindowsArch -eq 'arm64') {
if ($Capture) {
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:1`#pEF,e,b`"$OSCDIMGPath\Efisys_noprompt.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
}
if ($Deploy) {
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:1`#pEF,e,b`"$OSCDIMGPath\Efisys.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
}
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:1`#pEF,e,b`"$OSCDIMGPath\Efisys.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
}
Invoke-Process $OSCDIMG $OSCDIMGArgs | Out-Null
WriteLog "ISO created successfully"
@@ -3525,7 +3409,7 @@ function New-PEMedia {
Remove-Item -Path "$WinPEFFUPath" -Recurse -Force
WriteLog 'Cleanup complete'
# Deferred cleanup of preserved driver model folders (only after WinPE Deploy media is created)
if ($UseDriversAsPEDrivers -and $CompressDownloadedDriversToWim -and $Deploy -and $CopyPEDrivers) {
if ($UseDriversAsPEDrivers -and $CompressDownloadedDriversToWim -and $CopyPEDrivers) {
WriteLog "Beginning deferred cleanup of preserved driver model folders (UseDriversAsPEDrivers + compression scenario)."
$removedCount = 0
$skippedCount = 0
@@ -3616,6 +3500,40 @@ function Optimize-FFUCaptureDrive {
}
}
function Get-CaptureVhdContext {
param(
[Parameter(Mandatory = $true)]
[string]$VhdxPath
)
WriteLog 'Resolving VHDX context for host-side FFU capture'
$vhdInfo = Get-VHD -Path $VhdxPath
if ($vhdInfo.Attached) {
WriteLog 'VHDX is already mounted for capture'
$captureDisk = Get-Disk -Number $vhdInfo.DiskNumber
}
else {
WriteLog 'Mounting VHDX for capture'
$captureDisk = Mount-VHD -Path $VhdxPath -Passthru | Get-Disk
}
$captureOsPartition = $captureDisk | Get-Partition | Where-Object { $_.GptType -eq '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' } | Select-Object -First 1
if ($null -eq $captureOsPartition) {
throw 'Unable to resolve Windows partition for FFU capture.'
}
if ([string]::IsNullOrWhiteSpace($captureOsPartition.DriveLetter)) {
throw 'Unable to resolve Windows partition drive letter for FFU capture.'
}
return [pscustomobject]@{
Disk = $captureDisk
OsPartition = $captureOsPartition
OsPartitionDriveLetter = $captureOsPartition.DriveLetter
WindowsPartition = "$($captureOsPartition.DriveLetter):\"
}
}
function Get-ShortenedWindowsSKU {
param (
[string]$WindowsSKU
@@ -3703,89 +3621,53 @@ function New-FFUFileName {
}
function New-FFU {
param (
[Parameter(Mandatory = $false)]
[string]$VMName
)
#If $InstallApps = $true, configure the VM
If ($InstallApps) {
WriteLog 'Creating FFU from VM'
WriteLog "Setting $CaptureISO as first boot device"
$VMDVDDrive = Get-VMDvdDrive -VMName $VMName
Set-VMFirmware -VMName $VMName -FirstBootDevice $VMDVDDrive
Set-VMDvdDrive -VMName $VMName -Path $CaptureISO
$VMSwitch = Get-VMSwitch -name $VMSwitchName
WriteLog "Setting $($VMSwitch.Name) as VMSwitch"
get-vm $VMName | Get-VMNetworkAdapter | Connect-VMNetworkAdapter -SwitchName $VMSwitch.Name
WriteLog "Configuring VM complete"
$captureContext = Get-CaptureVhdContext -VhdxPath $VHDXPath
$captureDisk = $captureContext.Disk
$osPartitionDriveLetter = $captureContext.OsPartitionDriveLetter
$WindowsPartition = $captureContext.WindowsPartition
#Start VM
Set-Progress -Percentage 68 -Message "Capturing FFU from VM..."
WriteLog "Starting VM"
Start-VM -Name $VMName
# Wait for the VM to turn off
do {
$FFUVM = Get-VM -Name $VMName
Start-Sleep -Seconds 5
} while ($FFUVM.State -ne 'Off')
WriteLog "VM Shutdown"
# Check for .ffu files in the FFUDevelopment folder
WriteLog "Checking for FFU Files"
$FFUFiles = Get-ChildItem -Path $FFUCaptureLocation -Filter "*.ffu" -File
# If there's more than one .ffu file, get the most recent and store its path in $FFUFile
if ($FFUFiles.Count -gt 0) {
WriteLog 'Getting the most recent FFU file'
$FFUFile = ($FFUFiles | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1).FullName
WriteLog "Most recent .ffu file: $FFUFile"
}
else {
WriteLog "No .ffu files found in $FFUCaptureLocation"
throw $_
}
}
elseif (-not $InstallApps -and (-not $AllowVHDXCaching)) {
#Get Windows Version Information from the VHDX
$winverinfo = Get-WindowsVersionInfo
WriteLog 'Creating FFU File Name'
if ($CustomFFUNameTemplate) {
$FFUFileName = New-FFUFileName
}
else {
$FFUFileName = "$($winverinfo.Name)`_$($winverinfo.DisplayVersion)`_$($shortenedWindowsSKU)`_$($winverinfo.BuildDate).ffu"
}
WriteLog "FFU file name: $FFUFileName"
$FFUFile = "$FFUCaptureLocation\$FFUFileName"
#Capture the FFU
try {
Set-Progress -Percentage 68 -Message "Capturing FFU from VHDX..."
WriteLog 'Capturing FFU'
Invoke-Process cmd "/c ""$DandIEnv"" && dism /Capture-FFU /ImageFile:$FFUFile /CaptureDrive:\\.\PhysicalDrive$($vhdxDisk.DiskNumber) /Name:$($winverinfo.Name)$($winverinfo.DisplayVersion)$($shortenedWindowsSKU) /Compress:Default" | Out-Null
WriteLog 'FFU Capture complete'
Dismount-ScratchVhdx -VhdxPath $VHDXPath
}
elseif (-not $InstallApps -and $AllowVHDXCaching) {
# Make $FFUFileName based on values in the config.json file
WriteLog 'Creating FFU File Name'
if ($CustomFFUNameTemplate) {
$FFUFileName = New-FFUFileName
}
else {
$BuildDate = Get-Date -UFormat %b%Y
# Get Windows Information to make the FFU file name from the cachedVHDXInfo file
if ($installationType -eq 'Client') {
$FFUFileName = "Win$($cachedVHDXInfo.WindowsRelease)`_$($cachedVHDXInfo.WindowsVersion)`_$($shortenedWindowsSKU)`_$BuildDate.ffu"
if ($InstallApps -or (-not $AllowVHDXCaching)) {
# Resolve live Windows metadata from the mounted VHDX when the image was customized in a VM.
$winverinfo = Get-WindowsVersionInfo
WriteLog 'Creating FFU File Name'
if ($CustomFFUNameTemplate) {
$FFUFileName = New-FFUFileName
}
else {
$FFUFileName = "Server$($cachedVHDXInfo.WindowsRelease)`_$($cachedVHDXInfo.WindowsVersion)`_$($shortenedWindowsSKU)`_$BuildDate.ffu"
}
$FFUFileName = "$($winverinfo.Name)`_$($winverinfo.DisplayVersion)`_$($shortenedWindowsSKU)`_$($winverinfo.BuildDate).ffu"
}
WriteLog "FFU file name: $FFUFileName"
$FFUFile = "$FFUCaptureLocation\$FFUFileName"
WriteLog 'Capturing FFU from mounted VHDX on host'
Invoke-Process cmd "/c ""$DandIEnv"" && dism /Capture-FFU /ImageFile:$FFUFile /CaptureDrive:\\.\PhysicalDrive$($captureDisk.DiskNumber) /Name:$($winverinfo.Name)$($winverinfo.DisplayVersion)$($shortenedWindowsSKU) /Compress:Default" | Out-Null
}
WriteLog "FFU file name: $FFUFileName"
$FFUFile = "$FFUCaptureLocation\$FFUFileName"
#Capture the FFU
WriteLog 'Capturing FFU'
Invoke-Process cmd "/c ""$DandIEnv"" && dism /Capture-FFU /ImageFile:$FFUFile /CaptureDrive:\\.\PhysicalDrive$($vhdxDisk.DiskNumber) /Name:$($cachedVHDXInfo.WindowsRelease)$($cachedVHDXInfo.WindowsVersion)$($shortenedWindowsSKU) /Compress:Default" | Out-Null
else {
# Use cached Windows metadata only when the VHDX contents were reused without VM customization.
WriteLog 'Creating FFU File Name'
if ($CustomFFUNameTemplate) {
$FFUFileName = New-FFUFileName
}
else {
$BuildDate = Get-Date -UFormat %b%Y
if ($installationType -eq 'Client') {
$FFUFileName = "Win$($cachedVHDXInfo.WindowsRelease)`_$($cachedVHDXInfo.WindowsVersion)`_$($shortenedWindowsSKU)`_$BuildDate.ffu"
}
else {
$FFUFileName = "Server$($cachedVHDXInfo.WindowsRelease)`_$($cachedVHDXInfo.WindowsVersion)`_$($shortenedWindowsSKU)`_$BuildDate.ffu"
}
}
WriteLog "FFU file name: $FFUFileName"
$FFUFile = "$FFUCaptureLocation\$FFUFileName"
WriteLog 'Capturing FFU from mounted VHDX on host'
Invoke-Process cmd "/c ""$DandIEnv"" && dism /Capture-FFU /ImageFile:$FFUFile /CaptureDrive:\\.\PhysicalDrive$($captureDisk.DiskNumber) /Name:$($cachedVHDXInfo.WindowsRelease)$($cachedVHDXInfo.WindowsVersion)$($shortenedWindowsSKU) /Compress:Default" | Out-Null
}
WriteLog 'FFU Capture complete'
}
finally {
Dismount-ScratchVhdx -VhdxPath $VHDXPath
}
@@ -3893,15 +3775,6 @@ function Remove-FFUVM {
Invoke-Process cmd "/c mountvol /r" | Out-Null
WriteLog 'Removal complete'
}
Function Remove-FFUUserShare {
WriteLog "Removing $ShareName"
Remove-SmbShare -Name $ShareName -Force | Out-null
WriteLog 'Removal complete'
WriteLog "Removing $Username"
Remove-LocalUser -Name $Username | Out-Null
WriteLog 'Removal complete'
}
Function Get-WindowsVersionInfo {
#This sleep prevents CBS/CSI corruption which causes issues with Windows update after deployment. Capturing from very fast disks (NVME) can cause the capture to happen faster than Windows is ready for. This seems to affect VHDX-only captures, not VM captures.
WriteLog 'Sleep 60 seconds before opening registry to grab Windows version info '
@@ -4441,16 +4314,8 @@ function Get-FFUEnvironment {
Invoke-Process reg "unload HKLM\FFU" | Out-Null
}
#Remove FFU User and Share
$UserExists = Get-LocalUser -Name $UserName -ErrorAction SilentlyContinue
if ($UserExists) {
WriteLog "Removing FFU User and Share"
Remove-FFUUserShare
WriteLog 'Removal complete'
}
#Run shared cleanup to avoid duplicated logic
Invoke-FFUPostBuildCleanup -RootPath $FFUDevelopmentPath -AppsPath $AppsPath -DriversPath $DriversFolder -FFUCapturePath $FFUCaptureLocation -CaptureISOPath $CaptureISO -DeployISOPath $DeployISO -AppsISOPath $AppsISO -RemoveCaptureISO:$CleanupCaptureISO -RemoveDeployISO:$CleanupDeployISO -RemoveAppsISO:$CleanupAppsISO -RemoveDrivers:$CleanupDrivers -RemoveFFU:$RemoveFFU -RemoveApps:$RemoveApps -RemoveUpdates:$RemoveUpdates -RemoveDownloadedESD:$RemoveDownloadedESD -KBPath:$KBPath
Invoke-FFUPostBuildCleanup -RootPath $FFUDevelopmentPath -AppsPath $AppsPath -DriversPath $DriversFolder -FFUCapturePath $FFUCaptureLocation -DeployISOPath $DeployISO -AppsISOPath $AppsISO -RemoveDeployISO:$CleanupDeployISO -RemoveAppsISO:$CleanupAppsISO -RemoveDrivers:$CleanupDrivers -RemoveFFU:$RemoveFFU -RemoveApps:$RemoveApps -RemoveUpdates:$RemoveUpdates -RemoveDownloadedESD:$RemoveDownloadedESD -KBPath:$KBPath
# Remove existing Apps.iso
if (Test-Path -Path $AppsISO) {
@@ -5576,14 +5441,6 @@ if ($CopyUnattend) {
WriteLog 'Unattend validation complete'
}
# If InstallApps is true, we need capture media.
if ($InstallApps) {
if (-not $CreateCaptureMedia) {
WriteLog "InstallApps is true, but CreateCaptureMedia is false. Forcing to true to allow for VM capture to FFU."
$CreateCaptureMedia = $true
}
}
#Override $InstallApps value if using ESD to build FFU. This is due to a strange issue where building the FFU
#from vhdx doesn't work (you get an older style OOBE screen and get stuck in an OOBE reboot loop when hitting next).
#This behavior doesn't happen with WIM files.
@@ -5595,45 +5452,14 @@ if ($InstallApps) {
if (($InstallOffice -eq $true) -and ($InstallApps -eq $false)) {
throw "If variable InstallOffice is set to `$true, InstallApps must also be set to `$true."
}
if (($InstallApps -and ($VMSwitchName -eq ''))) {
throw "If variable InstallApps is set to `$true, VMSwitchName must also be set to capture the FFU. Please set -VMSwitchName and try again."
}
if (($InstallApps -and ($VMHostIPAddress -eq ''))) {
throw "If variable InstallApps is set to `$true, VMHostIPAddress must also be set to capture the FFU. Please set -VMHostIPAddress and try again."
}
if (($VMHostIPAddress) -and ($VMSwitchName)) {
WriteLog "Validating -VMSwitchName $VMSwitchName and -VMHostIPAddress $VMHostIPAddress"
if ($VMSwitchName) {
WriteLog "Validating -VMSwitchName $VMSwitchName"
#Check $VMSwitchName by using Get-VMSwitch
$VMSwitch = Get-VMSwitch -Name $VMSwitchName -ErrorAction SilentlyContinue
if (-not $VMSwitch) {
throw "-VMSwitchName $VMSwitchName not found. Please check the -VMSwitchName parameter and try again."
}
#Find the IP address of $VMSwitch and check if it matches $VMHostIPAddress
$interfaceAlias = "vEthernet ($VMSwitchName)"
$VMSwitchIPAddress = (Get-NetIPAddress -InterfaceAlias $interfaceAlias -AddressFamily 'IPv4' -ErrorAction SilentlyContinue).IPAddress
if (-not $VMSwitchIPAddress) {
throw "IP address for -VMSwitchName $VMSwitchName not found. Please check the -VMSwitchName parameter and try again."
}
if ($VMSwitchIPAddress -ne $VMHostIPAddress) {
try {
# Bypass the check for systems that could have a Hyper-V NAT switch
$null = Get-NetNat -ErrorAction Stop
$NetNat = @(Get-NetNat -ErrorAction Stop)
}
catch {
throw "IP address for -VMSwitchName $VMSwitchName is $VMSwitchIPAddress, which does not match the -VMHostIPAddress $VMHostIPAddress. Please check the -VMHostIPAddress parameter and try again."
}
if ($NetNat.Count -gt 0) {
WriteLog "IP address for -VMSwitchName $VMSwitchName is $VMSwitchIPAddress, which does not match the -VMHostIPAddress $VMHostIPAddress!"
WriteLog "NAT setup detected, remember to configure NATing if the FFU image can't be captured to the network share on the host."
}
else {
throw "IP address for -VMSwitchName $VMSwitchName is $VMSwitchIPAddress, which does not match the -VMHostIPAddress $VMHostIPAddress. Please check the -VMHostIPAddress parameter and try again."
}
}
WriteLog '-VMSwitchName and -VMHostIPAddress validation complete'
WriteLog '-VMSwitchName validation complete'
}
if (-not ($ISOPath) -and ($OptionalFeatures -like '*netfx3*')) {
@@ -7455,32 +7281,6 @@ if ($InstallApps) {
throw $_
}
#Create ffu user and share to capture FFU to
try {
Set-CaptureFFU
}
catch {
Write-Host 'Set-CaptureFFU function failed'
WriteLog "Set-CaptureFFU function failed with error $_"
Remove-FFUVM -VMName $VMName
throw $_
}
If ($CreateCaptureMedia) {
#Create Capture Media
try {
Set-Progress -Percentage 45 -Message "Creating WinPE capture media..."
#This should happen while the FFUVM is building
New-PEMedia -Capture $true
}
catch {
Write-Host 'Creating capture media failed'
WriteLog "Creating capture media failed with error $_"
Remove-FFUVM -VMName $VMName
throw $_
}
}
}
#Capture FFU file
try {
@@ -7490,6 +7290,10 @@ try {
New-Item -Path $FFUCaptureLocation -ItemType Directory -Force
WriteLog "Successfully created FFU capture location at $FFUCaptureLocation"
}
#Shorten Windows SKU for use in FFU file name to remove spaces and long names
WriteLog "Shortening Windows SKU: $WindowsSKU for FFU file name"
$shortenedWindowsSKU = Get-ShortenedWindowsSKU -WindowsSKU $WindowsSKU
WriteLog "Shortened Windows SKU: $shortenedWindowsSKU"
#Check if VM is done provisioning
If ($InstallApps) {
Set-Progress -Percentage 50 -Message "Installing applications in VM; please wait for VM to shut down..."
@@ -7502,13 +7306,9 @@ try {
Set-Progress -Percentage 65 -Message "Optimizing VHDX before capture..."
Optimize-FFUCaptureDrive -VhdxPath $VHDXPath
#Capture FFU file
New-FFU $FFUVM.Name
New-FFU
}
else {
#Shorten Windows SKU for use in FFU file name to remove spaces and long names
WriteLog "Shortening Windows SKU: $WindowsSKU for FFU file name"
$shortenedWindowsSKU = Get-ShortenedWindowsSKU -WindowsSKU $WindowsSKU
WriteLog "Shortened Windows SKU: $shortenedWindowsSKU"
#Create FFU file
New-FFU
}
@@ -7526,18 +7326,6 @@ Catch {
throw $_
}
#Clean up ffu_user and Share and clean up apps
If ($InstallApps) {
try {
Remove-FFUUserShare
}
catch {
Write-Host 'Cleaning up FFU User and/or share failed'
WriteLog "Cleaning up FFU User and/or share failed with error $_"
Remove-FFUVM -VMName $VMName
throw $_
}
}
#Clean up VM or VHDX
try {
Remove-FFUVM
@@ -7554,7 +7342,7 @@ catch {
If ($CreateDeploymentMedia) {
Set-Progress -Percentage 91 -Message "Creating deployment media..."
try {
New-PEMedia -Deploy $true
New-PEMedia
}
catch {
Write-Host 'Creating deployment media failed'
@@ -7611,7 +7399,7 @@ If ($BuildUSBDrive) {
Set-Progress -Percentage 99 -Message "Finalizing and cleaning up..."
# Delegated post-build cleanup to common module
Invoke-FFUPostBuildCleanup -RootPath $FFUDevelopmentPath -AppsPath $AppsPath -DriversPath $DriversFolder -FFUCapturePath $FFUCaptureLocation -CaptureISOPath $CaptureISO -DeployISOPath $DeployISO -AppsISOPath $AppsISO -RemoveCaptureISO:$CleanupCaptureISO -RemoveDeployISO:$CleanupDeployISO -RemoveAppsISO:$CleanupAppsISO -RemoveDrivers:$CleanupDrivers -RemoveFFU:$RemoveFFU -RemoveApps:$RemoveApps -RemoveUpdates:$RemoveUpdates -RemoveDownloadedESD:$RemoveDownloadedESD -KBPath:$KBPath
Invoke-FFUPostBuildCleanup -RootPath $FFUDevelopmentPath -AppsPath $AppsPath -DriversPath $DriversFolder -FFUCapturePath $FFUCaptureLocation -DeployISOPath $DeployISO -AppsISOPath $AppsISO -RemoveDeployISO:$CleanupDeployISO -RemoveAppsISO:$CleanupAppsISO -RemoveDrivers:$CleanupDrivers -RemoveFFU:$RemoveFFU -RemoveApps:$RemoveApps -RemoveUpdates:$RemoveUpdates -RemoveDownloadedESD:$RemoveDownloadedESD -KBPath:$KBPath
# Remove WinGetWin32Apps.json so it is always rebuilt next run
+4 -19
View File
@@ -320,9 +320,6 @@
<ComboBox x:Name="cmbVMSwitchName" HorizontalAlignment="Stretch" Margin="0,0,0,20" ToolTip="Name of the Hyper-V virtual switch. If $InstallApps is set to $true, this must be set. This is required to capture the FFU from the VM. The default is '*external*', but you will likely need to change this."/>
<!-- Custom VM Switch Name -->
<TextBox x:Name="txtCustomVMSwitchName" HorizontalAlignment="Stretch" Visibility="Collapsed" Margin="0,0,0,20" ToolTip="Enter your custom VM Switch Name if 'Other' is selected."/>
<!-- VM Host IP Address -->
<TextBlock Text="VM Host IP Address" Margin="0,0,0,8" ToolTip="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. The script will not auto-detect your IP (depending on your network adapters, it may not find the correct IP)."/>
<TextBox x:Name="txtVMHostIPAddress" HorizontalAlignment="Stretch" Margin="0,0,0,20" ToolTip="IP address of the Hyper-V host for FFU capture."/>
<!-- Disk Size (GB) -->
<TextBlock Text="Disk Size (GB)" Margin="0,0,0,8" ToolTip="Size of the virtual hard disk for the virtual machine. Default is a 50GB dynamic disk."/>
<TextBox x:Name="txtDiskSize" HorizontalAlignment="Stretch" Text="50" Margin="0,0,0,20" ToolTip="Size of the virtual hard disk for the virtual machine. Default is a 50GB dynamic disk."/>
@@ -823,13 +820,13 @@
<RowDefinition Height="Auto"/>
<!-- Row 3: FFU Capture Location -->
<RowDefinition Height="Auto"/>
<!-- Row 4: Share Name -->
<!-- Row 4: Threads -->
<RowDefinition Height="Auto"/>
<!-- Row 5: Username -->
<!-- Row 5: BITS Priority -->
<RowDefinition Height="Auto"/>
<!-- Row 6: Threads -->
<!-- Row 6: Max USB Drives -->
<RowDefinition Height="Auto"/>
<!-- Row 7: BITS Priority -->
<!-- Row 7: Build USB Drive -->
<RowDefinition Height="Auto"/>
<!-- Row 8: General Build Options Header -->
<RowDefinition Height="Auto"/>
@@ -874,16 +871,6 @@
<Button x:Name="btnBrowseFFUCaptureLocation" Grid.Column="1" Content="Browse..." Padding="12,4" Margin="8,0,0,0" VerticalAlignment="Center"/>
</Grid>
</StackPanel>
<!-- Row 4: Share Name -->
<StackPanel Grid.Row="4" Margin="0,0,0,20">
<TextBlock Text="Share Name" Margin="0,0,0,8" ToolTip="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."/>
<TextBox x:Name="txtShareName" VerticalAlignment="Center" ToolTip="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."/>
</StackPanel>
<!-- Row 5: Username -->
<StackPanel Grid.Row="5" Margin="0,0,0,20">
<TextBlock Text="Username" Margin="0,0,0,8" ToolTip="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."/>
<TextBox x:Name="txtUsername" VerticalAlignment="Center" ToolTip="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."/>
</StackPanel>
<!-- Row 6: Threads -->
<StackPanel Grid.Row="6" Margin="0,0,0,20">
<TextBlock Text="Threads" Margin="0,0,0,8" ToolTip="Controls the number of parallel threads used by ForEach-Object -Parallel and sets the value of the -ThrottleLimit parameter. Default is 5. Used in Winget, Application Copy, and driver downloads"/>
@@ -906,7 +893,6 @@
<CheckBox x:Name="chkUpdateADK" Content="Update ADK" Margin="0,0,0,8" VerticalAlignment="Center" Tag="When set to $true, the script will check for and install/update to the latest Windows ADK and WinPE add-on."/>
<CheckBox x:Name="chkOptimize" Content="Optimize" Margin="0,0,0,8" VerticalAlignment="Center" Tag="When set to $true, will optimize the OS when building the FFU."/>
<CheckBox x:Name="chkAllowVHDXCaching" Content="Allow VHDX Caching" Margin="0,0,0,8" VerticalAlignment="Center" Tag="When set to $true, will cache the VHDX file to cache folder and create a config json file to track Windows build information."/>
<CheckBox x:Name="chkCreateCaptureMedia" Content="Create Capture Media" Margin="0,0,0,8" VerticalAlignment="Center" Tag="When set to $true, this will create WinPE capture media for use when InstallApps is set to $true."/>
<CheckBox x:Name="chkCreateDeploymentMedia" Content="Create Deployment Media" Margin="0,0,0,8" VerticalAlignment="Center" Tag="When set to $true, this will create WinPE deployment media for use when deploying to a physical device."/>
<CheckBox x:Name="chkInjectUnattend" Content="Inject Unattend.xml" Margin="0,0,0,8" VerticalAlignment="Center" Tag="When set to $true and Install Apps is enabled, copies unattend_[arch].xml from $FFUDevelopmentPath\unattend into Apps\Unattend\Unattend.xml to be used by sysprep."/>
<CheckBox x:Name="chkVerbose" Content="Verbose" Margin="0" VerticalAlignment="Center" Tag="When set to $true, will enable write-verbose output to the console for the build script."/>
@@ -983,7 +969,6 @@
<Expander Grid.Row="11" Header="Post-Build Cleanup" IsExpanded="False" Margin="0" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
<StackPanel Margin="0,8,0,0">
<CheckBox x:Name="chkCleanupAppsISO" Content="Cleanup Apps ISO" Margin="0,0,0,8" VerticalAlignment="Center" Tag="Remove Apps ISO after FFU capture."/>
<CheckBox x:Name="chkCleanupCaptureISO" Content="Cleanup Capture ISO" Margin="0,0,0,8" VerticalAlignment="Center" Tag="Remove WinPE capture ISO after FFU capture."/>
<CheckBox x:Name="chkCleanupDeployISO" Content="Cleanup Deploy ISO" Margin="0,0,0,8" VerticalAlignment="Center" Tag="Remove WinPE deployment ISO after FFU capture."/>
<CheckBox x:Name="chkCleanupDrivers" Content="Cleanup Drivers" Margin="0,0,0,8" VerticalAlignment="Center" Tag="Remove drivers folder after FFU capture."/>
<CheckBox x:Name="chkRemoveFFU" Content="Remove FFU" Margin="0,0,0,8" VerticalAlignment="Center" Tag="Remove FFU after copying to USB drive."/>
+18 -57
View File
@@ -3,11 +3,8 @@ param (
[string]$adkPath = 'C:\Program Files (x86)\Windows Kits\10\',
[string]$WindowsArch = 'x64',
[bool]$CopyPEDrivers = $false,
[string]$CaptureISO = "$PSScriptRoot\WinPE_FFU_Capture_x64.iso",
[string]$DeployISO = "$PSScriptRoot\WinPE_FFU_Deploy_x64.iso",
[string]$LogFile = "$PSScriptRoot\Create-PEMedia.log",
[bool]$Capture,
[bool]$Deploy = $true
[string]$LogFile = "$PSScriptRoot\Create-PEMedia.log"
)
function WriteLog($LogText) {
@@ -77,12 +74,7 @@ function Invoke-Process {
}
function New-PEMedia {
param (
[Parameter()]
[bool]$Capture,
[Parameter()]
[bool]$Deploy
)
param ()
#Need to use the Demployment and Imaging tools environment to create winPE media
$DandIEnv = "$adkPath`Assessment and Deployment Kit\Deployment Tools\DandISetEnv.bat"
$WinPEFFUPath = "$FFUDevelopmentPath\WinPE"
@@ -135,36 +127,21 @@ function New-PEMedia {
Add-WindowsPackage -Path "$WinPEFFUPath\mount" -PackagePath $PackagePath | Out-Null
WriteLog "Adding package complete"
}
If ($Capture) {
WriteLog "Copying $FFUDevelopmentPath\WinPECaptureFFUFiles\* to WinPE capture media"
Copy-Item -Path "$FFUDevelopmentPath\WinPECaptureFFUFiles\*" -Destination "$WinPEFFUPath\mount" -Recurse -Force | out-null
WriteLog "Copy complete"
#Remove Bootfix.bin - for BIOS systems, shouldn't be needed, but doesn't hurt to remove for our purposes
#Remove-Item -Path "$WinPEFFUPath\media\boot\bootfix.bin" -Force | Out-null
# $WinPEISOName = 'WinPE_FFU_Capture.iso'
$WinPEISOFile = $CaptureISO
# $Capture = $false
}
If ($Deploy) {
WriteLog "Copying $FFUDevelopmentPath\WinPEDeployFFUFiles\* to WinPE deploy media"
Copy-Item -Path "$FFUDevelopmentPath\WinPEDeployFFUFiles\*" -Destination "$WinPEFFUPath\mount" -Recurse -Force | Out-Null
WriteLog 'Copy complete'
#If $CopyPEDrivers = $true, add drivers to WinPE media using dism
if ($CopyPEDrivers) {
WriteLog "Adding drivers to WinPE media"
try {
Add-WindowsDriver -Path "$WinPEFFUPath\Mount" -Driver "$FFUDevelopmentPath\PEDrivers" -Recurse -ErrorAction SilentlyContinue | Out-null
}
catch {
WriteLog 'Some drivers failed to be added to the FFU. This can be expected. Continuing.'
}
WriteLog "Adding drivers complete"
WriteLog "Copying $FFUDevelopmentPath\WinPEDeployFFUFiles\* to WinPE deploy media"
Copy-Item -Path "$FFUDevelopmentPath\WinPEDeployFFUFiles\*" -Destination "$WinPEFFUPath\mount" -Recurse -Force | Out-Null
WriteLog 'Copy complete'
#If $CopyPEDrivers = $true, add drivers to WinPE media using dism
if ($CopyPEDrivers) {
WriteLog "Adding drivers to WinPE media"
try {
Add-WindowsDriver -Path "$WinPEFFUPath\Mount" -Driver "$FFUDevelopmentPath\PEDrivers" -Recurse -ErrorAction SilentlyContinue | Out-null
}
# $WinPEISOName = 'WinPE_FFU_Deploy.iso'
$WinPEISOFile = $DeployISO
# $Deploy = $false
catch {
WriteLog 'Some drivers failed to be added to the FFU. This can be expected. Continuing.'
}
WriteLog "Adding drivers complete"
}
$WinPEISOFile = $DeployISO
WriteLog 'Dismounting WinPE media'
Dismount-WindowsImage -Path "$WinPEFFUPath\mount" -Save | Out-Null
WriteLog 'Dismount complete'
@@ -179,21 +156,10 @@ function New-PEMedia {
WriteLog "Creating WinPE ISO at $WinPEISOFile"
# & "$OSCDIMG" -m -o -u2 -udfver102 -bootdata:2`#p0,e,b$OSCDIMGPath\etfsboot.com`#pEF,e,b$OSCDIMGPath\Efisys_noprompt.bin $WinPEFFUPath\media $FFUDevelopmentPath\$WinPEISOName | Out-null
if($WindowsArch -eq 'x64'){
if($Capture){
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:2`#p0,e,b`"$OSCDIMGPath\etfsboot.com`"`#pEF,e,b`"$OSCDIMGPath\Efisys_noprompt.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
}
if($Deploy){
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:2`#p0,e,b`"$OSCDIMGPath\etfsboot.com`"`#pEF,e,b`"$OSCDIMGPath\Efisys.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
}
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:2`#p0,e,b`"$OSCDIMGPath\etfsboot.com`"`#pEF,e,b`"$OSCDIMGPath\Efisys.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
}
elseif($WindowsArch -eq 'arm64'){
if($Capture){
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:1`#pEF,e,b`"$OSCDIMGPath\Efisys_noprompt.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
}
if($Deploy){
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:1`#pEF,e,b`"$OSCDIMGPath\Efisys.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
}
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:1`#pEF,e,b`"$OSCDIMGPath\Efisys.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
}
Invoke-Process $OSCDIMG $OSCDIMGArgs
WriteLog "ISO created successfully"
@@ -201,9 +167,4 @@ function New-PEMedia {
Remove-Item -Path "$WinPEFFUPath" -Recurse -Force
WriteLog 'Cleanup complete'
}
if($Capture){
New-PEMedia -Capture $Capture
}
if($Deploy){
New-PEMedia -Deploy $Deploy
}
New-PEMedia
+3 -5
View File
@@ -85,12 +85,10 @@ graph TD
subgraph "VM-Based Capture (-InstallApps)"
direction LR
BB[Create Hyper-V VM from VHDX];
BB --> BC["Create WinPE Capture Media iso"];
BC --> BD[Configure network share for capture];
BD --> BE["Start VM: Boots to Audit Mode"];
BB --> BE["Start VM: Boots to Audit Mode"];
BE --> BF[Orchestrator runs: Installs apps, syspreps, shuts down];
BF --> BG[VM reboots from Capture Media];
BG --> BH["CaptureFFU.ps1 runs, saves FFU to share, shuts down"];
BF --> BG[Host optimizes and remounts VHDX];
BG --> BH["DISM captures FFU directly from host-mounted VHDX"];
end
subgraph "Direct VHDX Capture"
@@ -6,11 +6,9 @@ function Invoke-FFUPostBuildCleanup {
[string]$AppsPath,
[string]$DriversPath,
[string]$FFUCapturePath,
[string]$CaptureISOPath,
[string]$DeployISOPath,
[string]$AppsISOPath,
[string]$KBPath,
[bool]$RemoveCaptureISO = $false,
[bool]$RemoveDeployISO = $false,
[bool]$RemoveAppsISO = $false,
[bool]$RemoveDrivers = $false,
@@ -22,13 +20,9 @@ function Invoke-FFUPostBuildCleanup {
$originalProgressPreference = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
try {
WriteLog "CommonCleanup: Starting cleanup (CaptureISO=$RemoveCaptureISO DeployISO=$RemoveDeployISO AppsISO=$RemoveAppsISO Drivers=$RemoveDrivers FFU=$RemoveFFU Apps=$RemoveApps Updates=$RemoveUpdates RemoveDownloadedESD=$RemoveDownloadedESD KBPath=$KBPath)."
WriteLog "CommonCleanup: Starting cleanup (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)) {
WriteLog "CommonCleanup: Removing $CaptureISOPath"
try { Remove-Item -LiteralPath $CaptureISOPath -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $CaptureISOPath : $($_.Exception.Message)" }
}
if ($RemoveDeployISO -and -not [string]::IsNullOrWhiteSpace($DeployISOPath) -and (Test-Path -LiteralPath $DeployISOPath)) {
WriteLog "CommonCleanup: Removing $DeployISOPath"
try { Remove-Item -LiteralPath $DeployISOPath -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $DeployISOPath : $($_.Exception.Message)" }
@@ -39,11 +33,6 @@ function Invoke-FFUPostBuildCleanup {
}
# Legacy / root-level WinPE ISOs (pattern-based)
if ($RemoveCaptureISO) {
Get-ChildItem -LiteralPath $RootPath -Filter 'WinPE_FFU_Capture*.iso' -ErrorAction SilentlyContinue | ForEach-Object {
try { WriteLog "CommonCleanup: Removing legacy capture ISO $($_.FullName)"; Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing legacy capture ISO $($_.FullName) : $($_.Exception.Message)" }
}
}
if ($RemoveDeployISO) {
Get-ChildItem -LiteralPath $RootPath -Filter 'WinPE_FFU_Deploy*.iso' -ErrorAction SilentlyContinue | ForEach-Object {
try { WriteLog "CommonCleanup: Removing legacy deploy ISO $($_.FullName)"; Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing legacy deploy ISO $($_.FullName) : $($_.Exception.Message)" }
@@ -25,7 +25,6 @@ function Get-UIConfig {
else { $null }
BuildUSBDrive = $State.Controls.chkBuildUSBDriveEnable.IsChecked
CleanupAppsISO = $State.Controls.chkCleanupAppsISO.IsChecked
CleanupCaptureISO = $State.Controls.chkCleanupCaptureISO.IsChecked
CleanupDeployISO = $State.Controls.chkCleanupDeployISO.IsChecked
CleanupDrivers = $State.Controls.chkCleanupDrivers.IsChecked
CompactOS = $State.Controls.chkCompactOS.IsChecked
@@ -38,7 +37,6 @@ function Get-UIConfig {
CopyPPKG = $State.Controls.chkCopyPPKG.IsChecked
CopyUnattend = $State.Controls.chkCopyUnattend.IsChecked
CopyAdditionalFFUFiles = $State.Controls.chkCopyAdditionalFFUFiles.IsChecked
CreateCaptureMedia = $State.Controls.chkCreateCaptureMedia.IsChecked
CreateDeploymentMedia = $State.Controls.chkCreateDeploymentMedia.IsChecked
InjectUnattend = $State.Controls.chkInjectUnattend.IsChecked
CustomFFUNameTemplate = $State.Controls.txtCustomFFUNameTemplate.Text
@@ -84,7 +82,6 @@ function Get-UIConfig {
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
UpdateLatestCU = $State.Controls.chkUpdateLatestCU.IsChecked
@@ -96,13 +93,11 @@ function Get-UIConfig {
UpdatePreviewCU = $State.Controls.chkUpdatePreviewCU.IsChecked
UserAppListPath = $State.Controls.txtUserAppListPath.Text
USBDriveList = @{}
Username = $State.Controls.txtUsername.Text
Threads = [int]$State.Controls.txtThreads.Text
BitsPriority = $State.Controls.cmbBitsPriority.SelectedItem
MaxUSBDrives = [int]$State.Controls.txtMaxUSBDrives.Text
ThemeMode = if ($null -ne $State.Controls.cmbThemeMode -and $null -ne $State.Controls.cmbThemeMode.SelectedItem) { $State.Controls.cmbThemeMode.SelectedItem } else { "System" }
Verbose = $State.Controls.chkVerbose.IsChecked
VMHostIPAddress = $State.Controls.txtVMHostIPAddress.Text
VMLocation = $State.Controls.txtVMLocation.Text
VMSwitchName = if ($State.Controls.cmbVMSwitchName.SelectedItem -eq 'Other') {
$State.Controls.txtCustomVMSwitchName.Text
@@ -414,7 +409,6 @@ function Select-VMSwitchFromConfig {
$State.Controls.txtCustomVMSwitchName.Visibility = 'Visible'
$State.Controls.txtCustomVMSwitchName.Text = $configSwitch
$State.Data.customVMSwitchName = $configSwitch
$State.Data.customVMHostIP = $ConfigContent.VMHostIPAddress
WriteLog "LoadConfig: VMSwitchName '$configSwitch' not found. Selected 'Other' and populated custom VM Switch Name textbox."
}
}
@@ -442,8 +436,6 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'txtFFUDevPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'FFUDevelopmentPath' -State $State
Set-UIValue -ControlName 'txtCustomFFUNameTemplate' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'CustomFFUNameTemplate' -State $State
Set-UIValue -ControlName 'txtFFUCaptureLocation' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'FFUCaptureLocation' -State $State
Set-UIValue -ControlName 'txtShareName' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'ShareName' -State $State
Set-UIValue -ControlName 'txtUsername' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Username' -State $State
Set-UIValue -ControlName 'txtThreads' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Threads' -State $State
Set-UIValue -ControlName 'cmbBitsPriority' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'BitsPriority' -State $State
Set-UIValue -ControlName 'txtMaxUSBDrives' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'MaxUSBDrives' -State $State
@@ -455,7 +447,6 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'chkAllowExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'AllowExternalHardDiskMedia' -State $State
Set-UIValue -ControlName 'chkPromptExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'PromptExternalHardDiskMedia' -State $State
Set-UIValue -ControlName 'chkCopyAdditionalFFUFiles' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyAdditionalFFUFiles' -State $State
Set-UIValue -ControlName 'chkCreateCaptureMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CreateCaptureMedia' -State $State
Set-UIValue -ControlName 'chkCreateDeploymentMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CreateDeploymentMedia' -State $State
Set-UIValue -ControlName 'chkInjectUnattend' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'InjectUnattend' -State $State
Set-UIValue -ControlName 'chkVerbose' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'Verbose' -State $State
@@ -467,7 +458,6 @@ function Update-UIFromConfig {
# Post Build Cleanup group (Build Tab)
Set-UIValue -ControlName 'chkCleanupAppsISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupAppsISO' -State $State
Set-UIValue -ControlName 'chkCleanupCaptureISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupCaptureISO' -State $State
Set-UIValue -ControlName 'chkCleanupDeployISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupDeployISO' -State $State
Set-UIValue -ControlName 'chkCleanupDrivers' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupDrivers' -State $State
Set-UIValue -ControlName 'chkRemoveFFU' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'RemoveFFU' -State $State
@@ -477,7 +467,6 @@ function Update-UIFromConfig {
# Hyper-V Settings
Select-VMSwitchFromConfig -State $State -ConfigContent $ConfigContent
Set-UIValue -ControlName 'txtVMHostIPAddress' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'VMHostIPAddress' -State $State
Set-UIValue -ControlName 'txtDiskSize' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Disksize' -TransformValue { param($val) $val / 1GB } -State $State
Set-UIValue -ControlName 'txtMemory' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Memory' -TransformValue { param($val) $val / 1GB } -State $State
Set-UIValue -ControlName 'txtProcessors' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Processors' -State $State
@@ -884,11 +873,10 @@ function Invoke-RestoreDefaults {
$ffuCaptureRaw = Get-PathScalar -value $State.Controls.txtFFUCaptureLocation.Text
$ffuCapturePath = if ([string]::IsNullOrWhiteSpace($ffuCaptureRaw)) { Join-Path $rootPath 'FFU' } else { $ffuCaptureRaw }
$captureISOPath = Join-Path $rootPath 'WinPECaptureFFUFiles\WinPE-Capture.iso'
$deployISOPath = Join-Path $rootPath 'WinPEDeployFFUFiles\WinPE-Deploy.iso'
$appsISOPath = Join-Path $rootPath 'Apps.iso'
$msg = "Restore Defaults will:`n`n- Delete generated config and app/driver list JSON files`n- Remove ISO files (Capture, Deploy, Apps) if present`n- Remove Apps/Update/downloaded artifacts`n- Remove driver folder contents (not the folder)`n- Remove FFU files in the capture folder`n`nSample/template files and VM/VHDX cache are NOT removed.`n`nProceed?"
$msg = "Restore Defaults will:`n`n- Delete generated config and app/driver list JSON files`n- Remove ISO files (Deploy, Apps) if present`n- Remove Apps/Update/downloaded artifacts`n- Remove driver folder contents (not the folder)`n- Remove FFU files in the capture folder`n`nSample/template files and VM/VHDX cache are NOT removed.`n`nProceed?"
$result = [System.Windows.MessageBox]::Show($msg, "Confirm Restore Defaults", "YesNo", "Warning")
if ($result -ne [System.Windows.MessageBoxResult]::Yes) {
WriteLog "RestoreDefaults: User cancelled."
@@ -924,11 +912,9 @@ function Invoke-RestoreDefaults {
-AppsPath $appsPath `
-DriversPath $driversPath `
-FFUCapturePath $ffuCapturePath `
-CaptureISOPath $captureISOPath `
-DeployISOPath $deployISOPath `
-AppsISOPath $appsISOPath `
-KBPath (Join-Path $rootPath 'KB') `
-RemoveCaptureISO:$true `
-RemoveDeployISO:$true `
-RemoveAppsISO:$true `
-RemoveDrivers:$true `
@@ -391,30 +391,13 @@ function Register-EventHandlers {
if ([string]::IsNullOrWhiteSpace($localState.Controls.txtCustomVMSwitchName.Text) -and $null -ne $localState.Data.customVMSwitchName) {
$localState.Controls.txtCustomVMSwitchName.Text = $localState.Data.customVMSwitchName
}
if ($null -ne $localState.Data.customVMHostIP -and -not [string]::IsNullOrWhiteSpace($localState.Data.customVMHostIP)) {
$localState.Controls.txtVMHostIPAddress.Text = $localState.Data.customVMHostIP
}
}
else {
$localState.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed'
if ($null -ne $selectedItem -and $localState.Data.vmSwitchMap.ContainsKey($selectedItem)) {
$localState.Controls.txtVMHostIPAddress.Text = $localState.Data.vmSwitchMap[$selectedItem]
}
else {
$localState.Controls.txtVMHostIPAddress.Text = '' # Clear IP if not found or key null
}
}
})
# Persist custom VM switch name/IP when user edits them while 'Other' is selected
$State.Controls.txtVMHostIPAddress.Add_LostFocus({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
if ($localState.Controls.cmbVMSwitchName.SelectedItem -eq 'Other') {
$localState.Data.customVMHostIP = $localState.Controls.txtVMHostIPAddress.Text
}
})
# Persist custom VM switch name when user edits it while 'Other' is selected
$State.Controls.txtCustomVMSwitchName.Add_LostFocus({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
@@ -206,20 +206,16 @@ function Initialize-UIControls {
$State.Controls.pbOverallProgress = $window.FindName('progressBar')
$State.Controls.txtOverallStatus = $window.FindName('txtStatus')
$State.Controls.cmbVMSwitchName = $window.FindName('cmbVMSwitchName')
$State.Controls.txtVMHostIPAddress = $window.FindName('txtVMHostIPAddress')
$State.Controls.txtCustomVMSwitchName = $window.FindName('txtCustomVMSwitchName')
$State.Controls.txtFFUDevPath = $window.FindName('txtFFUDevPath')
$State.Controls.txtCustomFFUNameTemplate = $window.FindName('txtCustomFFUNameTemplate')
$State.Controls.txtFFUCaptureLocation = $window.FindName('txtFFUCaptureLocation')
$State.Controls.txtShareName = $window.FindName('txtShareName')
$State.Controls.txtUsername = $window.FindName('txtUsername')
$State.Controls.txtThreads = $window.FindName('txtThreads')
$State.Controls.cmbBitsPriority = $window.FindName('cmbBitsPriority')
$State.Controls.txtMaxUSBDrives = $window.FindName('txtMaxUSBDrives')
$State.Controls.chkCompactOS = $window.FindName('chkCompactOS')
$State.Controls.chkOptimize = $window.FindName('chkOptimize')
$State.Controls.chkAllowVHDXCaching = $window.FindName('chkAllowVHDXCaching')
$State.Controls.chkCreateCaptureMedia = $window.FindName('chkCreateCaptureMedia')
$State.Controls.chkCreateDeploymentMedia = $window.FindName('chkCreateDeploymentMedia')
$State.Controls.chkInjectUnattend = $window.FindName('chkInjectUnattend')
$State.Controls.chkVerbose = $window.FindName('chkVerbose')
@@ -227,7 +223,6 @@ function Initialize-UIControls {
$State.Controls.chkCopyUnattend = $window.FindName('chkCopyUnattend')
$State.Controls.chkCopyPPKG = $window.FindName('chkCopyPPKG')
$State.Controls.chkCleanupAppsISO = $window.FindName('chkCleanupAppsISO')
$State.Controls.chkCleanupCaptureISO = $window.FindName('chkCleanupCaptureISO')
$State.Controls.chkCleanupDeployISO = $window.FindName('chkCleanupDeployISO')
$State.Controls.chkCleanupDrivers = $window.FindName('chkCleanupDrivers')
$State.Controls.chkRemoveFFU = $window.FindName('chkRemoveFFU')
@@ -351,18 +346,11 @@ function Initialize-VMSwitchData {
if ($State.Controls.cmbVMSwitchName.Items.Count -gt 1) {
$State.Controls.cmbVMSwitchName.SelectedIndex = 0
$firstSwitch = $State.Controls.cmbVMSwitchName.SelectedItem
if ($null -ne $firstSwitch -and $State.Data.vmSwitchMap.ContainsKey($firstSwitch)) {
$State.Controls.txtVMHostIPAddress.Text = $State.Data.vmSwitchMap[$firstSwitch]
}
else {
$State.Controls.txtVMHostIPAddress.Text = $State.Defaults.generalDefaults.VMHostIPAddress # Use default if IP not found or key null
}
$State.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed'
}
else {
$State.Controls.cmbVMSwitchName.SelectedItem = 'Other'
$State.Controls.txtCustomVMSwitchName.Visibility = 'Visible'
$State.Controls.txtVMHostIPAddress.Text = $State.Defaults.generalDefaults.VMHostIPAddress # Use default
}
}
@@ -378,8 +366,6 @@ function Initialize-UIDefaults {
$State.Controls.txtFFUDevPath.Text = $State.FFUDevelopmentPath
$State.Controls.txtCustomFFUNameTemplate.Text = $State.Defaults.generalDefaults.CustomFFUNameTemplate
$State.Controls.txtFFUCaptureLocation.Text = $State.Defaults.generalDefaults.FFUCaptureLocation
$State.Controls.txtShareName.Text = $State.Defaults.generalDefaults.ShareName
$State.Controls.txtUsername.Text = $State.Defaults.generalDefaults.Username
$State.Controls.txtThreads.Text = $State.Defaults.generalDefaults.Threads
$State.Controls.cmbBitsPriority.SelectedItem = $State.Defaults.generalDefaults.BitsPriority
$State.Controls.txtMaxUSBDrives.Text = $State.Defaults.generalDefaults.MaxUSBDrives
@@ -389,7 +375,6 @@ function Initialize-UIDefaults {
$State.Controls.chkOptimize.IsChecked = $State.Defaults.generalDefaults.Optimize
$State.Controls.chkAllowVHDXCaching.IsChecked = $State.Defaults.generalDefaults.AllowVHDXCaching
$State.Controls.chkInjectUnattend.IsChecked = $State.Defaults.generalDefaults.InjectUnattend
$State.Controls.chkCreateCaptureMedia.IsChecked = $State.Defaults.generalDefaults.CreateCaptureMedia
$State.Controls.chkCreateDeploymentMedia.IsChecked = $State.Defaults.generalDefaults.CreateDeploymentMedia
$State.Controls.chkAllowExternalHardDiskMedia.IsChecked = $State.Defaults.generalDefaults.AllowExternalHardDiskMedia
$State.Controls.chkPromptExternalHardDiskMedia.IsChecked = $State.Defaults.generalDefaults.PromptExternalHardDiskMedia
@@ -398,7 +383,6 @@ function Initialize-UIDefaults {
$State.Controls.chkCopyUnattend.IsChecked = $State.Defaults.generalDefaults.CopyUnattend
$State.Controls.chkCopyPPKG.IsChecked = $State.Defaults.generalDefaults.CopyPPKG
$State.Controls.chkCleanupAppsISO.IsChecked = $State.Defaults.generalDefaults.CleanupAppsISO
$State.Controls.chkCleanupCaptureISO.IsChecked = $State.Defaults.generalDefaults.CleanupCaptureISO
$State.Controls.chkCleanupDeployISO.IsChecked = $State.Defaults.generalDefaults.CleanupDeployISO
$State.Controls.chkCleanupDrivers.IsChecked = $State.Defaults.generalDefaults.CleanupDrivers
$State.Controls.chkRemoveFFU.IsChecked = $State.Defaults.generalDefaults.RemoveFFU
@@ -115,8 +115,6 @@ function Get-GeneralDefaults {
# Build Tab Defaults
CustomFFUNameTemplate = "{WindowsRelease}_{WindowsVersion}_{SKU}_{yyyy}-{MM}-{dd}_{HH}{mm}"
FFUCaptureLocation = $ffuCapturePath
ShareName = "FFUCaptureShare"
Username = "ffu_user"
Threads = 5
BitsPriority = 'Normal'
MaxUSBDrives = 5
@@ -124,7 +122,6 @@ function Get-GeneralDefaults {
CompactOS = $true
Optimize = $true
AllowVHDXCaching = $false
CreateCaptureMedia = $true
CreateDeploymentMedia = $true
Verbose = $false
AllowExternalHardDiskMedia = $false
@@ -136,7 +133,6 @@ function Get-GeneralDefaults {
CopyPPKG = $false
InjectUnattend = $false
CleanupAppsISO = $true
CleanupCaptureISO = $true
CleanupDeployISO = $true
CleanupDrivers = $false
RemoveFFU = $false
@@ -144,7 +140,6 @@ function Get-GeneralDefaults {
RemoveUpdates = $false
RemoveDownloadedESD = $true
# Hyper-V Settings Defaults
VMHostIPAddress = ""
DiskSizeGB = 50
MemoryGB = 4
Processors = 4
@@ -1,4 +0,0 @@
select disk 0
select partition 3
Assign letter="M"
exit
@@ -1,237 +0,0 @@
$VMHostIPAddress = '192.168.1.158'
$ShareName = 'FFUCaptureShare'
$UserName = 'ffu_user'
$Password = '23202eb4-10c3-47e9-b389-f0c462663a23'
$CustomFFUNameTemplate = '{WindowsRelease}_{WindowsVersion}_{SKU}_{yyyy}-{MM}-{dd}_{HH}{mm}'
$netuseCommand = "net use W: \\$VMHostIPAddress\$ShareName /user:$UserName $Password 2>&1"
# Connect to network share
try {
Write-Host "Connecting to network share via $netuseCommand"
$netUseResult = net use W: "\\$VMHostIPAddress\$ShareName" "/user:$UserName" "$Password" 2>&1
# Check if the result contains an error
if ($LASTEXITCODE -ne 0) {
# Extract the error code from the Exception Message
# Example message format: "System error 53 has occurred."
$message = $netUseResult.Exception.Message
$regex = [regex]'System error (\d+)'
$match = $regex.Match($message)
if ($match.Success) {
$errorCode = [int]$match.Groups[1].Value
$errorMessage = switch ($errorCode) {
53 { "Network path not found. Verify the IP address is correct and the server is accessible." }
67 { "Network name cannot be found. Verify the share name exists on the server." }
86 { "Password is incorrect for the specified username." }
1219 { "Multiple connections to the share exist."}
1326 { "Logon failure: unknown username or bad password." }
1385 { "Logon failure: the user has not been granted the requested logon type at this computer.
This is likely due to changes to the User Rights Assignment: Access this computer from the network local security policy
See: https://github.com/rbalsleyMSFT/FFU/issues/122 for more info" }
1792 { "Unable to connect. Verify the server is running and accepting connections." }
2250 { "Network connection attempt timed out." }
default { "Network connection failed with error code: $errorCode. Details: $message" }
}
# Write-Error $errorMessage
throw $errorMessage
}
}
} catch {
Write-Error "Failed to connect to network share: Error code: $errorcode $_"
Write-Host "Some things to try:"
Write-Host '1. If not using an external switch, change to using an external switch'
Write-Host '2. Make sure the VMHostIPAddress is correct for the VMSwitch that is being used'
Write-Host '3. Try disabling the Windows Firewall on the host machine as a test only. If that helps, there is a Windows firewall rule that is blocking SMB 445 into the VM host'
Write-Host '4. If this is a machine that is managed by your organization, try using another machine that is not managed. There could be security policies in place that are blocking the connection to the share.'
Write-Host '5. You can also try disabling Hyper-V and re-enabling it. This has helped some users in the past.'
Write-Host '6. If all else fails, open an issue on the github repo and attach screenshots of this message, your FFUDevelopment.log, your command line that you used to build the FFU, and/or the config file you used (if you used one).'
pause
throw
}
$AssignDriveLetter = 'x:\AssignDriveLetter.txt'
try {
Write-Host 'Assigning M: as Windows drive letter'
Start-Process -FilePath diskpart.exe -ArgumentList "/S $AssignDriveLetter" -Wait -ErrorAction Stop | Out-Null
}
catch {
Write-Error "Failed to assign drive letter using diskpart: $_"
}
#Load Registry Hive
$Software = 'M:\Windows\System32\config\software'
try {
Write-Host "Loading software registry hive to $Software"
if (-not (Test-Path -Path $Software)) {
throw "Software registry hive not found at $Software"
}
$regResult = reg load "HKLM\FFU" $Software 2>&1
if ($LASTEXITCODE -ne 0) {
throw "Registry load failed with exit code $($LASTEXITCODE): $regResult"
}
Write-Host "Successfully loaded software registry hive."
}
catch {
Write-Error "Failed to load registry hive: $_"
}
try {
#Find Windows version values
Write-Host "Retrieving Windows information from the registry..."
$SKU = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'EditionID'
Write-Host "SKU: $SKU"
[int]$CurrentBuild = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'CurrentBuild'
Write-Host "CurrentBuild: $CurrentBuild"
if ($CurrentBuild -notin 14393, 17763) {
Write-Host "CurrentBuild is not 14393 or 17763, retrieving WindowsVersion..."
$WindowsVersion = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'DisplayVersion'
Write-Host "WindowsVersion: $WindowsVersion"
}
$InstallationType = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'InstallationType'
Write-Host "InstallationType: $InstallationType"
$BuildDate = Get-Date -uformat %b%Y
Write-Host "BuildDate: $BuildDate"
$SKU = switch ($SKU) {
Core { 'Home' }
CoreN { 'Home_N' }
CoreSingleLanguage { 'Home_SL' }
Professional { 'Pro' }
ProfessionalN { 'Pro_N' }
ProfessionalEducation { 'Pro_Edu' }
ProfessionalEducationN { 'Pro_Edu_N' }
Enterprise { 'Ent' }
EnterpriseN { 'Ent_N' }
EnterpriseS { 'Ent_LTSC' }
EnterpriseSN { 'Ent_N_LTSC' }
IoTEnterpriseS { 'IoT_Ent_LTSC' }
Education { 'Edu' }
EducationN { 'Edu_N' }
ProfessionalWorkstation { 'Pro_Wks' }
ProfessionalWorkstationN { 'Pro_Wks_N' }
ServerStandard { 'Srv_Std' }
ServerDatacenter { 'Srv_Dtc' }
}
if ($InstallationType -eq "Client") {
if ($CurrentBuild -ge 22000) {
$WindowsRelease = 'Win11'
Write-Host "WindowsRelease: $WindowsRelease"
}
else {
$WindowsRelease = 'Win10'
Write-Host "WindowsRelease: $WindowsRelease"
}
}
else {
$WindowsRelease = switch ($CurrentBuild) {
26100 { '2025' }
20348 { '2022' }
17763 { '2019' }
14393 { '2016' }
Default { $WindowsVersion }
}
Write-Host "WindowsRelease: $WindowsRelease"
if ($InstallationType -eq "Server Core") {
$SKU += "_Core"
Write-Host "InstallType is Server Core, changing SKU to: $SKU"
}
}
if ($CustomFFUNameTemplate) {
Write-Host 'Using custom FFU name template...'
$FFUFileName = $CustomFFUNameTemplate
$FFUFileName = $FFUFileName -replace '{WindowsRelease}', $WindowsRelease
$FFUFileName = $FFUFileName -replace '{WindowsVersion}', $WindowsVersion
$FFUFileName = $FFUFileName -replace '{SKU}', $SKU
$FFUFileName = $FFUFileName -replace '{BuildDate}', $BuildDate
$FFUFileName = $FFUFileName -replace '{yyyy}', (Get-Date -UFormat '%Y')
$FFUFileName = $FFUFileName -creplace '{MM}', (Get-Date -UFormat '%m')
$FFUFileName = $FFUFileName -replace '{dd}', (Get-Date -UFormat '%d')
$FFUFileName = $FFUFileName -creplace '{HH}', (Get-Date -UFormat '%H')
$FFUFileName = $FFUFileName -creplace '{hh}', (Get-Date -UFormat '%I')
$FFUFileName = $FFUFileName -creplace '{mm}', (Get-Date -UFormat '%M')
$FFUFileName = $FFUFileName -replace '{tt}', (Get-Date -UFormat '%p')
Write-Host "FFU File Name: $FFUFileName"
#If the custom FFU name template does not end with .ffu, append it
if ($FFUFileName -notlike '*.ffu') {
$FFUFileName += '.ffu'
Write-Host "Appended .ffu to FFU file name: $FFUFileName"
}
$dismArgs = "/capture-ffu /imagefile=W:\$FFUFileName /capturedrive=\\.\PhysicalDrive0 /name:$WindowsRelease$WindowsVersion$SKU /Compress:Default"
Write-Host "DISM arguments for capture: $dismArgs"
}
else {
#If Office is installed, modify the file name of the FFU
$Office = Get-ChildItem -Path 'M:\Program Files\Microsoft Office' -ErrorAction SilentlyContinue
if ($Office) {
$ffuFilePath = "W:\$WindowsRelease`_$WindowsVersion`_$SKU`_Office`_$BuildDate.ffu"
Write-Host "Office is installed, using modified FFU file name: $ffuFilePath"
}
else {
$ffuFilePath = "W:\$WindowsRelease`_$WindowsVersion`_$SKU`_Apps`_$BuildDate.ffu"
Write-Host "Office is not installed, using modified FFU file name: $ffuFilePath"
}
$dismArgs = "/capture-ffu /imagefile=$ffuFilePath /capturedrive=\\.\PhysicalDrive0 /name:$WindowsRelease$WindowsVersion$SKU /Compress:Default"
Write-Host "DISM arguments for capture: $dismArgs"
}
#Unload Registry
Set-Location X:\
Remove-Variable SKU
Remove-Variable CurrentBuild
if ($CurrentBuild -notin 14393, 17763) {
Remove-Variable WindowsVersion
}
if ($Office) {
Remove-Variable Office
}
try {
Write-Host "Unloading registry hive HKLM\FFU..."
$regUnloadResult = reg unload "HKLM\FFU" 2>&1
if ($LASTEXITCODE -ne 0) {
throw "Registry unload failed with exit code $($LASTEXITCODE): $regUnloadResult"
}
Write-Host "Successfully unloaded registry hive."
}
catch {
Write-Error "Failed to unload registry hive: $_"
}
Write-Host "Sleeping for 60 seconds to allow registry to unload prior to capture"
Start-sleep 60
try {
Write-Host "Starting DISM FFU capture..."
$dismProcess = Start-Process -FilePath dism.exe -ArgumentList $dismArgs -Wait -PassThru -ErrorAction Stop
if ($dismProcess.ExitCode -ne 0) {
throw "DISM capture failed with exit code $($dismProcess.ExitCode)"
}
Write-Host "DISM FFU capture completed successfully."
}
catch {
Write-Error "FFU capture failed: $_"
}
try {
Write-Host "Copying DISM log to network share..."
xcopy X:\Windows\logs\dism\dism.log W:\ /Y | Out-Null
}
catch {
Write-Warning "Failed to copy DISM log: $_"
}
Write-Host "DISM log copied to network share, shutting down..."
wpeutil Shutdown
}
catch {
Write-Error "An unexpected error occurred: $_"
}
@@ -1,5 +0,0 @@
wpeinit
powercfg /s 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
powershell -Noprofile -ExecutionPolicy Bypass -File x:\CaptureFFU.ps1
exit
Binary file not shown.