From 8ab6603999e9d5f974482e2f1e574f76b7efcc97 Mon Sep 17 00:00:00 2001
From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com>
Date: Thu, 14 Aug 2025 16:20:55 -0700
Subject: [PATCH] feat: Add new checkbox to Inject Unattend.xml to VM.
- Creates a new parameter [bool]InjectUnattend
- This will take the FFUDevelopment\Unattend\unattend_[arch].xml file and copy it to the FFUDevelopment\Apps\Unattend and rename the file to unattend.xml.
- This is useful for situations where you don't use the USB drive for deploying the FFU but still have Unattend-related customizations that you want to apply.
---
.../Apps/Orchestration/Run-Sysprep.ps1 | 18 ++++++++-----
FFUDevelopment/BuildFFUVM.ps1 | 26 +++++++++++++++++++
FFUDevelopment/BuildFFUVM_UI.xaml | 4 ++-
.../FFUUI.Core/FFUUI.Core.Config.psm1 | 2 ++
.../FFUUI.Core/FFUUI.Core.Initialize.psm1 | 2 ++
FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 | 1 +
6 files changed, 45 insertions(+), 8 deletions(-)
diff --git a/FFUDevelopment/Apps/Orchestration/Run-Sysprep.ps1 b/FFUDevelopment/Apps/Orchestration/Run-Sysprep.ps1
index a383765..8a36290 100644
--- a/FFUDevelopment/Apps/Orchestration/Run-Sysprep.ps1
+++ b/FFUDevelopment/Apps/Orchestration/Run-Sysprep.ps1
@@ -1,14 +1,18 @@
#The below lines will remove the unattend.xml that gets the machine into audit mode. If not removed, the OS will get stuck booting to audit mode each time.
#Also kills the sysprep process in order to automate sysprep generalize
-# Convert these commands to native powershell
-# del c:\windows\panther\unattend\unattend.xml /F /Q
-# del c:\windows\panther\unattend.xml /F /Q
-# taskkill /IM sysprep.exe
-# timeout /t 10
-# & c:\windows\system32\sysprep\sysprep.exe /quiet /generalize /oobe
+Write-Host "Removing existing unattend.xml files and stopping sysprep process if running..."
Remove-Item -Path "C:\windows\panther\unattend\unattend.xml" -Force -ErrorAction SilentlyContinue
Remove-Item -Path "C:\windows\panther\unattend.xml" -Force -ErrorAction SilentlyContinue
Stop-Process -Name "sysprep" -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 10
-& "C:\windows\system32\sysprep\sysprep.exe" /quiet /generalize /oobe
+# If an Unattend.xml has been provided on the mounted Apps ISO (D:\Unattend\Unattend.xml),
+# pass it to sysprep; otherwise, run without /unattend.
+$unattendOnAppsIso = "D:\Unattend\Unattend.xml"
+if (Test-Path -Path $unattendOnAppsIso) {
+ Write-Host "Using $unattendOnAppsIso from Apps ISO..."
+ & "C:\windows\system32\sysprep\sysprep.exe" /quiet /generalize /oobe /unattend:$unattendOnAppsIso
+}
+else {
+ & "C:\windows\system32\sysprep\sysprep.exe" /quiet /generalize /oobe
+}
diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1
index f16671d..a3bff05 100644
--- a/FFUDevelopment/BuildFFUVM.ps1
+++ b/FFUDevelopment/BuildFFUVM.ps1
@@ -93,6 +93,9 @@ Prefix for the generated FFU file. Default is _FFU.
.PARAMETER Headers
Headers to use when downloading files. Not recommended to modify.
+.PARAMETER InjectUnattend
+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.
+
.PARAMETER InstallApps
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.
@@ -420,6 +423,7 @@ param(
[string]$ConfigFile,
[Parameter(Mandatory = $false)]
[string]$ExportConfigFile,
+ [bool]$InjectUnattend = $false,
[string]$orchestrationPath,
[bool]$UpdateADK = $true,
[bool]$CleanupCurrentRunDownloads = $false,
@@ -5125,6 +5129,28 @@ if ($InstallApps) {
}
#Create Apps ISO
+ # Inject Unattend.xml into Apps if requested and applicable
+ if ($InstallApps -and $InjectUnattend) {
+ # Determine source unattend.xml based on architecture
+ $archSuffix = if ($WindowsArch -ieq 'arm64') { 'arm64' } else { 'x64' }
+ $unattendSource = Join-Path $UnattendFolder "unattend_$archSuffix.xml"
+
+ # Ensure target folder exists under Apps
+ $targetFolder = Join-Path $AppsPath 'Unattend'
+ if (-not (Test-Path -Path $targetFolder -PathType Container)) {
+ New-Item -Path $targetFolder -ItemType Directory -Force | Out-Null
+ }
+
+ # Copy if source exists; otherwise log and skip
+ if (Test-Path -Path $unattendSource -PathType Leaf) {
+ $destination = Join-Path $targetFolder 'Unattend.xml'
+ Copy-Item -Path $unattendSource -Destination $destination -Force | Out-Null
+ WriteLog "Injected unattend file into Apps: $unattendSource -> $destination"
+ }
+ else {
+ WriteLog "InjectUnattend is true but source file missing: $unattendSource. Skipping unattend injection."
+ }
+ }
Set-Progress -Percentage 10 -Message "Creating Apps ISO..."
WriteLog "Creating $AppsISO file"
New-AppsISO
diff --git a/FFUDevelopment/BuildFFUVM_UI.xaml b/FFUDevelopment/BuildFFUVM_UI.xaml
index 5fb9c1e..af9b7d3 100644
--- a/FFUDevelopment/BuildFFUVM_UI.xaml
+++ b/FFUDevelopment/BuildFFUVM_UI.xaml
@@ -411,7 +411,8 @@
-
+
+
@@ -740,6 +741,7 @@
+
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
index 030d78b..1ca8734 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
@@ -38,6 +38,7 @@ function Get-UIConfig {
CopyUnattend = $State.Controls.chkCopyUnattend.IsChecked
CreateCaptureMedia = $State.Controls.chkCreateCaptureMedia.IsChecked
CreateDeploymentMedia = $State.Controls.chkCreateDeploymentMedia.IsChecked
+ InjectUnattend = $State.Controls.chkInjectUnattend.IsChecked
CustomFFUNameTemplate = $State.Controls.txtCustomFFUNameTemplate.Text
Disksize = [int64]$State.Controls.txtDiskSize.Text * 1GB
DownloadDrivers = $State.Controls.chkDownloadDrivers.IsChecked
@@ -340,6 +341,7 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'chkPromptExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'PromptExternalHardDiskMedia' -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
# USB Drive Modification group (Build Tab)
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
index 6352897..5ff335d 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
@@ -121,6 +121,7 @@ function Initialize-UIControls {
$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')
$State.Controls.chkCopyAutopilot = $window.FindName('chkCopyAutopilot')
$State.Controls.chkCopyUnattend = $window.FindName('chkCopyUnattend')
@@ -234,6 +235,7 @@ function Initialize-UIDefaults {
$State.Controls.chkUpdateADK.IsChecked = $State.Defaults.generalDefaults.UpdateADK
$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
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
index f1efbb3..d8edfa2 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
@@ -131,6 +131,7 @@ function Get-GeneralDefaults {
CopyAutopilot = $false
CopyUnattend = $false
CopyPPKG = $false
+ InjectUnattend = $false
CleanupAppsISO = $true
CleanupCaptureISO = $true
CleanupDeployISO = $true