From 4a2d8e63ea7cdc0901539f23459634c75d5c3c12 Mon Sep 17 00:00:00 2001
From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com>
Date: Tue, 7 Apr 2026 10:48:34 -0700
Subject: [PATCH] Add flexible device naming options to Unattend delivery
Introduces new parameters and UI controls to give users more choice over device naming when applying an Unattend.xml file.
Users can now specify a device name, use a static or template-based name with the `%serial%` variable, or continue using a list of prefixes.
The UI is updated with a new Device Naming expander to guide the user through the options and clearly indicate the requirements for each mode, ensuring that mutually exclusive options like Copy Unattend and Inject Unattend are not selected together.
Documentation is updated to reflect the new functionality.
---
FFUDevelopment/BuildFFUVM.ps1 | 224 +++++++++++++-
FFUDevelopment/BuildFFUVM_UI.ps1 | 54 ++++
FFUDevelopment/BuildFFUVM_UI.xaml | 46 ++-
.../FFUUI.Core/FFUUI.Core.Config.psm1 | 22 ++
.../FFUUI.Core/FFUUI.Core.Handlers.psm1 | 274 ++++++++++++++++++
.../FFUUI.Core/FFUUI.Core.Initialize.psm1 | 16 +
.../FFUUI.Core/FFUUI.Core.Shared.psm1 | 3 +
FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 | 6 +
.../WinPEDeployFFUFiles/ApplyFFU.ps1 | 122 ++++++--
FFUDevelopment/config/Sample_default.json | Bin 6162 -> 6506 bytes
docs/build.md | 32 +-
docs/parameters_reference.md | 8 +-
docs/quickstart.md | 18 +-
13 files changed, 764 insertions(+), 61 deletions(-)
diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1
index 5055dbb..bb61540 100644
--- a/FFUDevelopment/BuildFFUVM.ps1
+++ b/FFUDevelopment/BuildFFUVM.ps1
@@ -72,6 +72,18 @@ 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 DeviceNamingMode
+Controls how device naming is handled when unattend content is copied to USB media or injected into the FFU. Supported values are Legacy, None, Template, and Prefixes.
+
+.PARAMETER DeviceNameTemplate
+Sets the device name used when DeviceNamingMode is Template. Supports a static name or the %serial% token when CopyUnattend is used.
+
+.PARAMETER DeviceNamePrefixes
+Sets the prefixes used when DeviceNamingMode is Prefixes. Each entry becomes a line in prefixes.txt on the deployment media.
+
+.PARAMETER DeviceNamePrefixesPath
+Path to the source prefixes file used for legacy copy or when DeviceNamePrefixes is not supplied. Default is $FFUDevelopmentPath\Unattend\prefixes.txt.
+
.PARAMETER CreateDeploymentMedia
When set to $true, this will create WinPE deployment media for use when deploying to a physical device.
@@ -407,6 +419,11 @@ param(
[bool]$AllowVHDXCaching,
[bool]$CopyPPKG,
[bool]$CopyUnattend,
+ [ValidateSet('Legacy', 'None', 'Template', 'Prefixes')]
+ [string]$DeviceNamingMode = 'Legacy',
+ [string]$DeviceNameTemplate,
+ [string[]]$DeviceNamePrefixes,
+ [string]$DeviceNamePrefixesPath,
[bool]$CopyAutopilot,
[bool]$CompactOS = $true,
[bool]$CleanupDeployISO = $true,
@@ -505,6 +522,79 @@ if ($ConfigFile -and (Test-Path -Path $ConfigFile)) {
}
}
+function Get-UnattendSourcePath {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$UnattendFolder,
+ [Parameter(Mandatory = $true)]
+ [string]$WindowsArch
+ )
+
+ $archSuffix = if ($WindowsArch -ieq 'arm64') { 'arm64' } else { 'x64' }
+ return Join-Path $UnattendFolder "unattend_$archSuffix.xml"
+}
+
+function Test-UnattendHasComputerNameElement {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Path
+ )
+
+ [xml]$unattendXml = Get-Content -Path $Path
+ foreach ($component in $unattendXml.unattend.settings.component) {
+ if ($component.ComputerName) {
+ return $true
+ }
+ }
+
+ return $false
+}
+
+function Save-StagedUnattendFile {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$SourcePath,
+ [Parameter(Mandatory = $true)]
+ [string]$DestinationPath,
+ [Parameter(Mandatory = $true)]
+ [ValidateSet('Legacy', 'None', 'Template', 'Prefixes')]
+ [string]$DeviceNamingMode,
+ [string]$DeviceNameTemplate
+ )
+
+ if ($DeviceNamingMode -in @('Legacy', 'Prefixes')) {
+ Copy-Item -Path $SourcePath -Destination $DestinationPath -Force | Out-Null
+ return
+ }
+
+ [xml]$unattendXml = Get-Content -Path $SourcePath
+ $computerNameComponent = $null
+ foreach ($component in $unattendXml.unattend.settings.component) {
+ if ($component.ComputerName) {
+ $computerNameComponent = $component
+ break
+ }
+ }
+
+ if ($null -eq $computerNameComponent) {
+ if ($DeviceNamingMode -eq 'None') {
+ Copy-Item -Path $SourcePath -Destination $DestinationPath -Force | Out-Null
+ return
+ }
+
+ throw "ComputerName element not found in unattend source file: $SourcePath"
+ }
+
+ if ($DeviceNamingMode -eq 'None') {
+ $computerNameComponent.ComputerName = '*'
+ }
+ elseif ($DeviceNamingMode -eq 'Template') {
+ $computerNameComponent.ComputerName = $DeviceNameTemplate
+ }
+
+ $unattendXml.Save($DestinationPath)
+}
+
$vmSwitchWasExplicitlyBound = $PSBoundParameters.ContainsKey('VMSwitchName')
$enableVmNetworkingWasExplicitlyBound = $PSBoundParameters.ContainsKey('EnableVMNetworking')
if (-not $EnableVMNetworking -and $vmSwitchWasExplicitlyBound -and -not $enableVmNetworkingWasExplicitlyBound) {
@@ -512,6 +602,52 @@ if (-not $EnableVMNetworking -and $vmSwitchWasExplicitlyBound -and -not $enableV
WriteLog 'EnableVMNetworking not explicitly set. Enabling VM networking because -VMSwitchName was supplied on the command line.'
}
+$normalizedDeviceNameTemplate = if ($null -ne $DeviceNameTemplate) { $DeviceNameTemplate.Trim() } else { $null }
+$effectiveDeviceNamePrefixes = @($DeviceNamePrefixes | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })
+$resolvedDeviceNamePrefixesPath = if ([string]::IsNullOrWhiteSpace($DeviceNamePrefixesPath)) {
+ Join-Path (Join-Path $FFUDevelopmentPath 'Unattend') 'prefixes.txt'
+}
+else {
+ $DeviceNamePrefixesPath
+}
+
+if (($DeviceNamingMode -eq 'Prefixes') -and ($effectiveDeviceNamePrefixes.Count -eq 0) -and (Test-Path -Path $resolvedDeviceNamePrefixesPath -PathType Leaf)) {
+ $effectiveDeviceNamePrefixes = @(Get-Content -Path $resolvedDeviceNamePrefixesPath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })
+ WriteLog "Loaded device name prefixes from $resolvedDeviceNamePrefixesPath"
+}
+
+if ($CopyUnattend -and $InjectUnattend) {
+ throw 'CopyUnattend and InjectUnattend cannot both be set to `$true. Select only one unattend delivery method.'
+}
+
+if ($DeviceNamingMode -eq 'Template') {
+ if ([string]::IsNullOrWhiteSpace($normalizedDeviceNameTemplate)) {
+ throw 'DeviceNamingMode Template requires DeviceNameTemplate.'
+ }
+
+ $templateWithoutSupportedVariables = $normalizedDeviceNameTemplate -replace '(?i)%serial%', ''
+ if ($templateWithoutSupportedVariables -match '%') {
+ throw 'Only the %serial% device name variable is supported.'
+ }
+
+ if (-not ($CopyUnattend -or $InjectUnattend)) {
+ throw 'DeviceNamingMode Template requires either CopyUnattend or InjectUnattend.'
+ }
+
+ if ($InjectUnattend -and (-not $CopyUnattend) -and $normalizedDeviceNameTemplate -match '(?i)%serial%') {
+ throw 'The %serial% device name variable is only supported when CopyUnattend is used.'
+ }
+}
+elseif ($DeviceNamingMode -eq 'Prefixes') {
+ if (-not $CopyUnattend) {
+ throw 'DeviceNamingMode Prefixes requires CopyUnattend. Prefix-based naming is not supported with InjectUnattend.'
+ }
+
+ if ($effectiveDeviceNamePrefixes.Count -eq 0) {
+ throw 'DeviceNamingMode Prefixes requires at least one DeviceNamePrefixes entry or a valid DeviceNamePrefixesPath.'
+ }
+}
+
# Validate that the selected Windows SKU is compatible with the chosen Windows release and ensure an ISO is provided for unsupported releases
$clientSKUs = @(
'Home',
@@ -4184,6 +4320,57 @@ Function New-DeploymentUSB {
Import-Module "$($using:PSScriptRoot)\FFU.Common" -Force
Set-CommonCoreLogPath -Path $using:LogFile
+ function Get-LocalUnattendSourcePath {
+ param(
+ [string]$UnattendFolder,
+ [string]$WindowsArch
+ )
+
+ $archSuffix = if ($WindowsArch -ieq 'arm64') { 'arm64' } else { 'x64' }
+ return Join-Path $UnattendFolder "unattend_$archSuffix.xml"
+ }
+
+ function Save-LocalStagedUnattendFile {
+ param(
+ [string]$SourcePath,
+ [string]$DestinationPath,
+ [string]$DeviceNamingMode,
+ [string]$DeviceNameTemplate
+ )
+
+ if ($DeviceNamingMode -in @('Legacy', 'Prefixes')) {
+ Copy-Item -Path $SourcePath -Destination $DestinationPath -Force | Out-Null
+ return
+ }
+
+ [xml]$unattendXml = Get-Content -Path $SourcePath
+ $computerNameComponent = $null
+ foreach ($component in $unattendXml.unattend.settings.component) {
+ if ($component.ComputerName) {
+ $computerNameComponent = $component
+ break
+ }
+ }
+
+ if ($null -eq $computerNameComponent) {
+ if ($DeviceNamingMode -eq 'None') {
+ Copy-Item -Path $SourcePath -Destination $DestinationPath -Force | Out-Null
+ return
+ }
+
+ throw "ComputerName element not found in unattend source file: $SourcePath"
+ }
+
+ if ($DeviceNamingMode -eq 'None') {
+ $computerNameComponent.ComputerName = '*'
+ }
+ elseif ($DeviceNamingMode -eq 'Template') {
+ $computerNameComponent.ComputerName = $DeviceNameTemplate
+ }
+
+ $unattendXml.Save($DestinationPath)
+ }
+
$DiskNumber = $USBDrive.DeviceID.Replace("\\.\PHYSICALDRIVE", "")
WriteLog "Thread $([System.Threading.Thread]::CurrentThread.ManagedThreadId) processing DiskNumber $DiskNumber ($($USBDrive.Model))"
@@ -4244,15 +4431,15 @@ Function New-DeploymentUSB {
$UnattendPathOnUSB = Join-Path $DeployPartitionDriveLetter "Unattend"
WriteLog "Copying unattend file to $UnattendPathOnUSB"
New-Item -Path $UnattendPathOnUSB -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
- if ($using:WindowsArch -eq 'x64') {
- Copy-Item -Path (Join-Path $using:UnattendFolder 'unattend_x64.xml') -Destination (Join-Path $UnattendPathOnUSB 'Unattend.xml') -Force | Out-Null
+ $unattendSource = Get-LocalUnattendSourcePath -UnattendFolder $using:UnattendFolder -WindowsArch $using:WindowsArch
+ Save-LocalStagedUnattendFile -SourcePath $unattendSource -DestinationPath (Join-Path $UnattendPathOnUSB 'Unattend.xml') -DeviceNamingMode $using:DeviceNamingMode -DeviceNameTemplate $using:normalizedDeviceNameTemplate
+ if ($using:DeviceNamingMode -eq 'Prefixes') {
+ WriteLog "Writing prefixes.txt file to $UnattendPathOnUSB"
+ $using:effectiveDeviceNamePrefixes | Set-Content -Path (Join-Path $UnattendPathOnUSB 'prefixes.txt') -Encoding UTF8
}
- elseif ($using:WindowsArch -eq 'arm64') {
- Copy-Item -Path (Join-Path $using:UnattendFolder 'unattend_arm64.xml') -Destination (Join-Path $UnattendPathOnUSB 'Unattend.xml') -Force | Out-Null
- }
- if (Test-Path (Join-Path $using:UnattendFolder 'prefixes.txt')) {
+ elseif (($using:DeviceNamingMode -eq 'Legacy') -and (Test-Path -Path $using:resolvedDeviceNamePrefixesPath -PathType Leaf)) {
WriteLog "Copying prefixes.txt file to $UnattendPathOnUSB"
- Copy-Item -Path (Join-Path $using:UnattendFolder 'prefixes.txt') -Destination (Join-Path $UnattendPathOnUSB 'prefixes.txt') -Force | Out-Null
+ Copy-Item -Path $using:resolvedDeviceNamePrefixesPath -Destination (Join-Path $UnattendPathOnUSB 'prefixes.txt') -Force | Out-Null
}
WriteLog 'Copy completed'
}
@@ -5518,9 +5705,26 @@ if ($CopyUnattend) {
WriteLog "-CopyUnattend is set to `$true, but the $UnattendFolder folder is missing a .XML file"
throw "-CopyUnattend is set to `$true, but the $UnattendFolder folder is missing a .XML file"
}
+
+ if ($DeviceNamingMode -eq 'Prefixes') {
+ $unattendSourcePath = Get-UnattendSourcePath -UnattendFolder $UnattendFolder -WindowsArch $WindowsArch
+ if (-not (Test-UnattendHasComputerNameElement -Path $unattendSourcePath)) {
+ throw "DeviceNamingMode Prefixes requires a ComputerName element in $unattendSourcePath"
+ }
+ }
+
WriteLog 'Unattend validation complete'
}
+if ($InjectUnattend -and $DeviceNamingMode -eq 'Template') {
+ $injectUnattendSourcePath = Get-UnattendSourcePath -UnattendFolder $UnattendFolder -WindowsArch $WindowsArch
+ if (Test-Path -Path $injectUnattendSourcePath -PathType Leaf) {
+ if (-not (Test-UnattendHasComputerNameElement -Path $injectUnattendSourcePath)) {
+ throw "DeviceNamingMode Template requires a ComputerName element in $injectUnattendSourcePath"
+ }
+ }
+}
+
#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.
@@ -6418,9 +6622,7 @@ 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"
+ $unattendSource = Get-UnattendSourcePath -UnattendFolder $UnattendFolder -WindowsArch $WindowsArch
# Ensure target folder exists under Apps
$targetFolder = Join-Path $AppsPath 'Unattend'
@@ -6431,7 +6633,7 @@ if ($InstallApps) {
# 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
+ Save-StagedUnattendFile -SourcePath $unattendSource -DestinationPath $destination -DeviceNamingMode $DeviceNamingMode -DeviceNameTemplate $normalizedDeviceNameTemplate
WriteLog "Injected unattend file into Apps: $unattendSource -> $destination"
}
else {
diff --git a/FFUDevelopment/BuildFFUVM_UI.ps1 b/FFUDevelopment/BuildFFUVM_UI.ps1
index 3bf3926..90b8e32 100644
--- a/FFUDevelopment/BuildFFUVM_UI.ps1
+++ b/FFUDevelopment/BuildFFUVM_UI.ps1
@@ -432,6 +432,60 @@ $script:uiState.Controls.btnRun.Add_Click({
return
}
+ if ($config.CopyUnattend -and $config.InjectUnattend) {
+ [System.Windows.MessageBox]::Show("Copy Unattend.xml and Inject Unattend.xml cannot both be selected. Choose only one unattend delivery method.", "Unattend Selection Required", "OK", "Warning") | Out-Null
+ $btnRun.IsEnabled = $true
+ $script:uiState.Controls.txtStatus.Text = "Build canceled: choose only one unattend delivery method."
+ return
+ }
+
+ if ($config.DeviceNamingMode -eq 'Template') {
+ if ([string]::IsNullOrWhiteSpace([string]$config.DeviceNameTemplate)) {
+ [System.Windows.MessageBox]::Show("Specify a device name before using 'Specify Device Name'.", "Device Name Required", "OK", "Warning") | Out-Null
+ $btnRun.IsEnabled = $true
+ $script:uiState.Controls.txtStatus.Text = "Build canceled: device name required."
+ return
+ }
+
+ if (-not ($config.CopyUnattend -or $config.InjectUnattend)) {
+ [System.Windows.MessageBox]::Show("Select Copy Unattend.xml or Inject Unattend.xml before using 'Specify Device Name'.", "Unattend Selection Required", "OK", "Warning") | Out-Null
+ $btnRun.IsEnabled = $true
+ $script:uiState.Controls.txtStatus.Text = "Build canceled: unattend delivery method required for device naming."
+ return
+ }
+
+ $templateWithoutSupportedVariables = ([string]$config.DeviceNameTemplate) -replace '(?i)%serial%', ''
+ if ($templateWithoutSupportedVariables -match '%') {
+ [System.Windows.MessageBox]::Show("Only the %serial% device name variable is supported.", "Unsupported Device Name Variable", "OK", "Warning") | Out-Null
+ $btnRun.IsEnabled = $true
+ $script:uiState.Controls.txtStatus.Text = "Build canceled: unsupported device name variable."
+ return
+ }
+
+ if ($config.InjectUnattend -and (-not $config.CopyUnattend) -and ([string]$config.DeviceNameTemplate -match '(?i)%serial%')) {
+ [System.Windows.MessageBox]::Show("The %serial% device name variable is only supported when Copy Unattend.xml is selected.", "Unsupported Inject Unattend Setting", "OK", "Warning") | Out-Null
+ $btnRun.IsEnabled = $true
+ $script:uiState.Controls.txtStatus.Text = "Build canceled: %serial% requires Copy Unattend.xml."
+ return
+ }
+ }
+ elseif ($config.DeviceNamingMode -eq 'Prefixes') {
+ if (-not $config.CopyUnattend) {
+ [System.Windows.MessageBox]::Show("Select Copy Unattend.xml before using 'Specify a list of Prefixes'.", "Copy Unattend Required", "OK", "Warning") | Out-Null
+ $btnRun.IsEnabled = $true
+ $script:uiState.Controls.txtStatus.Text = "Build canceled: prefixes require Copy Unattend.xml."
+ return
+ }
+
+ $hasSavedPrefixesPath = -not [string]::IsNullOrWhiteSpace([string]$config.DeviceNamePrefixesPath) -and (Test-Path -Path $config.DeviceNamePrefixesPath -PathType Leaf)
+ if ((($null -eq $config.DeviceNamePrefixes) -or ($config.DeviceNamePrefixes.Count -eq 0)) -and -not $hasSavedPrefixesPath) {
+ [System.Windows.MessageBox]::Show("Enter at least one prefix or choose a valid prefixes file before using 'Specify a list of Prefixes'.", "Prefixes Required", "OK", "Warning") | Out-Null
+ $btnRun.IsEnabled = $true
+ $script:uiState.Controls.txtStatus.Text = "Build canceled: prefixes required."
+ return
+ }
+ }
+
$configFilePath = Join-Path $config.FFUDevelopmentPath "\config\FFUConfig.json"
# Sort top-level keys alphabetically for consistent output
$sortedConfig = [ordered]@{}
diff --git a/FFUDevelopment/BuildFFUVM_UI.xaml b/FFUDevelopment/BuildFFUVM_UI.xaml
index de8f077..4d5f511 100644
--- a/FFUDevelopment/BuildFFUVM_UI.xaml
+++ b/FFUDevelopment/BuildFFUVM_UI.xaml
@@ -836,9 +836,11 @@
-
+
-
+
+
+
@@ -903,8 +905,40 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -969,8 +1003,8 @@
-
-
+
+
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
index f8791b0..1d92654 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
@@ -36,6 +36,10 @@ function Get-UIConfig {
UseDriversAsPEDrivers = $State.Controls.chkUseDriversAsPEDrivers.IsChecked
CopyPPKG = $State.Controls.chkCopyPPKG.IsChecked
CopyUnattend = $State.Controls.chkCopyUnattend.IsChecked
+ DeviceNamingMode = Get-SelectedDeviceNamingMode -State $State
+ DeviceNameTemplate = $State.Controls.txtDeviceNameTemplate.Text
+ DeviceNamePrefixesPath = $State.Controls.txtDeviceNamePrefixesPath.Text
+ DeviceNamePrefixes = @(Get-DeviceNamePrefixes -State $State)
CopyAdditionalFFUFiles = $State.Controls.chkCopyAdditionalFFUFiles.IsChecked
CreateDeploymentMedia = $State.Controls.chkCreateDeploymentMedia.IsChecked
InjectUnattend = $State.Controls.chkInjectUnattend.IsChecked
@@ -456,6 +460,24 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'chkCopyAutopilot' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyAutopilot' -State $State
Set-UIValue -ControlName 'chkCopyUnattend' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyUnattend' -State $State
Set-UIValue -ControlName 'chkCopyPPKG' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyPPKG' -State $State
+ Set-UIValue -ControlName 'txtDeviceNamePrefixesPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNamePrefixesPath' -State $State
+ Set-UIValue -ControlName 'txtDeviceNameTemplate' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNameTemplate' -State $State
+ Set-UIValue -ControlName 'txtDeviceNamePrefixes' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNamePrefixes' -TransformValue { param($val) if ($val -is [System.Array]) { $val -join [System.Environment]::NewLine } else { [string]$val } } -State $State
+
+ if ([string]::IsNullOrWhiteSpace($State.Controls.txtDeviceNamePrefixesPath.Text)) {
+ $State.Controls.txtDeviceNamePrefixesPath.Text = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $State.Controls.txtFFUDevPath.Text
+ }
+
+ $deviceNamingMode = 'None'
+ if ($ConfigContent.PSObject.Properties.Name -contains 'DeviceNamingMode') {
+ $deviceNamingMode = [string]$ConfigContent.DeviceNamingMode
+ }
+ if ($deviceNamingMode -notin @('None', 'Template', 'Prefixes')) {
+ $deviceNamingMode = 'None'
+ }
+ Set-DeviceNamingMode -State $State -Mode $deviceNamingMode
+ Import-DeviceNamePrefixesFromConfiguredPath -State $State
+ Update-DeviceNamingControls -State $State
# Post Build Cleanup group (Build Tab)
Set-UIValue -ControlName 'chkCleanupAppsISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupAppsISO' -State $State
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1
index 32a13fb..5fa99e3 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1
@@ -27,6 +27,172 @@ function Update-VMNetworkingControls {
}
}
+function Get-SelectedDeviceNamingMode {
+ param([PSCustomObject]$State)
+
+ if ($true -eq $State.Controls.rbDeviceNamingTemplate.IsChecked) {
+ return 'Template'
+ }
+
+ if ($true -eq $State.Controls.rbDeviceNamingPrefixes.IsChecked) {
+ return 'Prefixes'
+ }
+
+ return 'None'
+}
+
+function Set-DeviceNamingMode {
+ param(
+ [PSCustomObject]$State,
+ [ValidateSet('None', 'Template', 'Prefixes')]
+ [string]$Mode
+ )
+
+ $State.Controls.rbDeviceNamingNone.IsChecked = $Mode -eq 'None'
+ $State.Controls.rbDeviceNamingTemplate.IsChecked = $Mode -eq 'Template'
+ $State.Controls.rbDeviceNamingPrefixes.IsChecked = $Mode -eq 'Prefixes'
+}
+
+function Get-DeviceNamePrefixes {
+ param([PSCustomObject]$State)
+
+ if ($null -eq $State.Controls.txtDeviceNamePrefixes) {
+ return @()
+ }
+
+ return @(
+ $State.Controls.txtDeviceNamePrefixes.Text -split "\r?\n" |
+ Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
+ ForEach-Object { $_.Trim() }
+ )
+}
+
+function Import-DeviceNamePrefixesFile {
+ param(
+ [PSCustomObject]$State,
+ [string]$FilePath
+ )
+
+ if ([string]::IsNullOrWhiteSpace($FilePath) -or -not (Test-Path -Path $FilePath -PathType Leaf)) {
+ return $false
+ }
+
+ $prefixLines = @(Get-Content -Path $FilePath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })
+ if ($null -ne $State.Controls.txtDeviceNamePrefixesPath) {
+ $State.Controls.txtDeviceNamePrefixesPath.Text = $FilePath
+ }
+ $State.Controls.txtDeviceNamePrefixes.Text = $prefixLines -join [System.Environment]::NewLine
+ WriteLog "Imported device name prefixes from $FilePath"
+ return $true
+}
+
+function Get-DefaultDeviceNamePrefixesPath {
+ param([string]$FFUDevelopmentPath)
+
+ if ([string]::IsNullOrWhiteSpace($FFUDevelopmentPath)) {
+ return $null
+ }
+
+ return Join-Path (Join-Path $FFUDevelopmentPath 'unattend') 'prefixes.txt'
+}
+
+function Import-DeviceNamePrefixesFromConfiguredPath {
+ param(
+ [PSCustomObject]$State,
+ [switch]$SkipIfTextPresent
+ )
+
+ if ($SkipIfTextPresent -and -not [string]::IsNullOrWhiteSpace($State.Controls.txtDeviceNamePrefixes.Text)) {
+ return
+ }
+
+ $prefixFilePath = $State.Controls.txtDeviceNamePrefixesPath.Text
+ if ([string]::IsNullOrWhiteSpace($prefixFilePath)) {
+ $prefixFilePath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $State.Controls.txtFFUDevPath.Text
+ if (-not [string]::IsNullOrWhiteSpace($prefixFilePath) -and $null -ne $State.Controls.txtDeviceNamePrefixesPath) {
+ $State.Controls.txtDeviceNamePrefixesPath.Text = $prefixFilePath
+ }
+ }
+
+ if (Test-Path -Path $prefixFilePath -PathType Leaf) {
+ Import-DeviceNamePrefixesFile -State $State -FilePath $prefixFilePath | Out-Null
+ }
+}
+
+function Test-DeviceNameTemplateUsesSerialToken {
+ param([PSCustomObject]$State)
+
+ return ((Get-SelectedDeviceNamingMode -State $State) -eq 'Template') -and ($State.Controls.txtDeviceNameTemplate.Text -match '(?i)%serial%')
+}
+
+function Update-UnattendSelectionControls {
+ param([PSCustomObject]$State)
+
+ $selectedDeviceNamingMode = Get-SelectedDeviceNamingMode -State $State
+ $isCopyUnattendSelected = $true -eq $State.Controls.chkCopyUnattend.IsChecked
+ $isInjectUnattendSelected = $true -eq $State.Controls.chkInjectUnattend.IsChecked
+ $deviceNameTemplateUsesSerialToken = Test-DeviceNameTemplateUsesSerialToken -State $State
+
+ if ($isCopyUnattendSelected -and $isInjectUnattendSelected) {
+ if (($selectedDeviceNamingMode -eq 'Prefixes') -or $deviceNameTemplateUsesSerialToken) {
+ $State.Controls.chkInjectUnattend.IsChecked = $false
+ $isInjectUnattendSelected = $false
+ }
+ else {
+ $State.Controls.chkCopyUnattend.IsChecked = $false
+ $isCopyUnattendSelected = $false
+ }
+ }
+
+ if (($selectedDeviceNamingMode -eq 'Prefixes') -or $deviceNameTemplateUsesSerialToken) {
+ if (-not $isCopyUnattendSelected) {
+ $State.Controls.chkCopyUnattend.IsChecked = $true
+ $isCopyUnattendSelected = $true
+ }
+
+ if ($isInjectUnattendSelected) {
+ $State.Controls.chkInjectUnattend.IsChecked = $false
+ $isInjectUnattendSelected = $false
+ }
+
+ $State.Controls.chkCopyUnattend.IsEnabled = $false
+ $State.Controls.chkInjectUnattend.IsEnabled = $false
+ return
+ }
+
+ if ($isCopyUnattendSelected) {
+ $State.Controls.chkCopyUnattend.IsEnabled = $true
+ $State.Controls.chkInjectUnattend.IsEnabled = $false
+ }
+ elseif ($isInjectUnattendSelected) {
+ $State.Controls.chkCopyUnattend.IsEnabled = $false
+ $State.Controls.chkInjectUnattend.IsEnabled = $true
+ }
+ else {
+ $State.Controls.chkCopyUnattend.IsEnabled = $true
+ $State.Controls.chkInjectUnattend.IsEnabled = $true
+ }
+}
+
+function Update-DeviceNamingControls {
+ param([PSCustomObject]$State)
+
+ if (($true -eq $State.Controls.chkInjectUnattend.IsChecked) -and ($true -eq $State.Controls.rbDeviceNamingPrefixes.IsChecked)) {
+ $State.Controls.rbDeviceNamingNone.IsChecked = $true
+ }
+
+ $selectedDeviceNamingMode = Get-SelectedDeviceNamingMode -State $State
+ $State.Controls.deviceNameTemplatePanel.Visibility = if ($selectedDeviceNamingMode -eq 'Template') { 'Visible' } else { 'Collapsed' }
+ $State.Controls.deviceNamePrefixesPanel.Visibility = if ($selectedDeviceNamingMode -eq 'Prefixes') { 'Visible' } else { 'Collapsed' }
+ $State.Controls.rbDeviceNamingPrefixes.IsEnabled = -not ($true -eq $State.Controls.chkInjectUnattend.IsChecked)
+
+ if ($selectedDeviceNamingMode -eq 'Prefixes') {
+ Import-DeviceNamePrefixesFromConfiguredPath -State $State -SkipIfTextPresent
+ }
+
+ Update-UnattendSelectionControls -State $State
+}
+
function Register-EventHandlers {
param([PSCustomObject]$State)
WriteLog "Registering UI event handlers..."
@@ -242,7 +408,15 @@ function Register-EventHandlers {
$localState = $window.Tag
$selectedPath = Invoke-BrowseAction -Type 'Folder' -Title "Select FFU Development Path"
if ($selectedPath) {
+ $currentPrefixesPath = $localState.Controls.txtDeviceNamePrefixesPath.Text
+ $previousDefaultPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
$localState.Controls.txtFFUDevPath.Text = $selectedPath
+ $newDefaultPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $selectedPath
+ if ([string]::IsNullOrWhiteSpace($currentPrefixesPath) -or $currentPrefixesPath -ieq $previousDefaultPrefixesPath) {
+ $localState.Controls.txtDeviceNamePrefixesPath.Text = $newDefaultPrefixesPath
+ }
+ Import-DeviceNamePrefixesFromConfiguredPath -State $localState
+ Update-DeviceNamingControls -State $localState
}
})
@@ -256,6 +430,106 @@ function Register-EventHandlers {
}
})
+ $State.Controls.rbDeviceNamingNone.Add_Checked({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ Update-DeviceNamingControls -State $window.Tag
+ })
+ $State.Controls.rbDeviceNamingTemplate.Add_Checked({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ Update-DeviceNamingControls -State $window.Tag
+ })
+ $State.Controls.txtDeviceNameTemplate.Add_TextChanged({
+ param($eventSource, $textChangedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ if ($null -ne $window -and $null -ne $window.Tag) {
+ Update-DeviceNamingControls -State $window.Tag
+ }
+ })
+ $State.Controls.rbDeviceNamingPrefixes.Add_Checked({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ Update-DeviceNamingControls -State $window.Tag
+ })
+ $State.Controls.btnBrowseDeviceNamePrefixesPath.Add_Click({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ $currentPrefixesPath = $localState.Controls.txtDeviceNamePrefixesPath.Text
+ if ([string]::IsNullOrWhiteSpace($currentPrefixesPath)) {
+ $currentPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
+ }
+ $initialDirectory = if ([string]::IsNullOrWhiteSpace($currentPrefixesPath)) {
+ $null
+ }
+ else {
+ Split-Path $currentPrefixesPath -Parent
+ }
+ $fileName = if ([string]::IsNullOrWhiteSpace($currentPrefixesPath)) { 'prefixes.txt' } else { Split-Path $currentPrefixesPath -Leaf }
+ $selectedPath = Invoke-BrowseAction -Type 'OpenFile' -Title 'Select prefixes file path' -Filter 'Text files (*.txt)|*.txt|All files (*.*)|*.*' -InitialDirectory $initialDirectory -FileName $fileName
+ if (Import-DeviceNamePrefixesFile -State $localState -FilePath $selectedPath) {
+ Update-DeviceNamingControls -State $localState
+ }
+ })
+ $State.Controls.btnSaveDeviceNamePrefixes.Add_Click({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ $prefixLines = @(Get-DeviceNamePrefixes -State $localState)
+
+ if ($prefixLines.Count -eq 0) {
+ [System.Windows.MessageBox]::Show("Enter at least one prefix before saving the prefixes file.", "Prefixes Required", "OK", "Warning") | Out-Null
+ return
+ }
+
+ $currentPrefixesPath = $localState.Controls.txtDeviceNamePrefixesPath.Text
+ if ([string]::IsNullOrWhiteSpace($currentPrefixesPath)) {
+ $currentPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
+ if (-not [string]::IsNullOrWhiteSpace($currentPrefixesPath)) {
+ $localState.Controls.txtDeviceNamePrefixesPath.Text = $currentPrefixesPath
+ }
+ }
+
+ if ([string]::IsNullOrWhiteSpace($currentPrefixesPath)) {
+ [System.Windows.MessageBox]::Show("Select a valid Prefixes File Path before saving prefixes.", "Prefixes File Path Required", "OK", "Warning") | Out-Null
+ return
+ }
+
+ try {
+ $prefixLines | Set-Content -Path $currentPrefixesPath -Encoding UTF8
+ $localState.Controls.txtDeviceNamePrefixesPath.Text = $currentPrefixesPath
+ WriteLog "Saved device name prefixes to $currentPrefixesPath"
+ }
+ catch {
+ [System.Windows.MessageBox]::Show("Saving prefixes failed for '$currentPrefixesPath'. $($_.Exception.Message)", "Save Prefixes Failed", "OK", "Error") | Out-Null
+ }
+ })
+ $State.Controls.chkCopyUnattend.Add_Checked({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ $localState.Controls.chkInjectUnattend.IsChecked = $false
+ Update-DeviceNamingControls -State $localState
+ })
+ $State.Controls.chkCopyUnattend.Add_Unchecked({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ Update-DeviceNamingControls -State $window.Tag
+ })
+ $State.Controls.chkInjectUnattend.Add_Checked({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ $localState.Controls.chkCopyUnattend.IsChecked = $false
+ Update-DeviceNamingControls -State $localState
+ })
+ $State.Controls.chkInjectUnattend.Add_Unchecked({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ Update-DeviceNamingControls -State $window.Tag
+ })
+
# Build USB Drive Settings Event Handlers
# The USB Expander is always visible; the checkbox controls child settings only
$State.Controls.chkBuildUSBDriveEnable.Add_Checked({
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
index 0d921af..e84e1f5 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
@@ -220,6 +220,16 @@ function Initialize-UIControls {
$State.Controls.chkAllowVHDXCaching = $window.FindName('chkAllowVHDXCaching')
$State.Controls.chkCreateDeploymentMedia = $window.FindName('chkCreateDeploymentMedia')
$State.Controls.chkInjectUnattend = $window.FindName('chkInjectUnattend')
+ $State.Controls.rbDeviceNamingNone = $window.FindName('rbDeviceNamingNone')
+ $State.Controls.rbDeviceNamingTemplate = $window.FindName('rbDeviceNamingTemplate')
+ $State.Controls.rbDeviceNamingPrefixes = $window.FindName('rbDeviceNamingPrefixes')
+ $State.Controls.deviceNameTemplatePanel = $window.FindName('deviceNameTemplatePanel')
+ $State.Controls.deviceNamePrefixesPanel = $window.FindName('deviceNamePrefixesPanel')
+ $State.Controls.txtDeviceNameTemplate = $window.FindName('txtDeviceNameTemplate')
+ $State.Controls.txtDeviceNamePrefixesPath = $window.FindName('txtDeviceNamePrefixesPath')
+ $State.Controls.btnBrowseDeviceNamePrefixesPath = $window.FindName('btnBrowseDeviceNamePrefixesPath')
+ $State.Controls.txtDeviceNamePrefixes = $window.FindName('txtDeviceNamePrefixes')
+ $State.Controls.btnSaveDeviceNamePrefixes = $window.FindName('btnSaveDeviceNamePrefixes')
$State.Controls.chkVerbose = $window.FindName('chkVerbose')
$State.Controls.chkCopyAutopilot = $window.FindName('chkCopyAutopilot')
$State.Controls.chkCopyUnattend = $window.FindName('chkCopyUnattend')
@@ -383,6 +393,12 @@ function Initialize-UIDefaults {
$State.Controls.chkCopyAutopilot.IsChecked = $State.Defaults.generalDefaults.CopyAutopilot
$State.Controls.chkCopyUnattend.IsChecked = $State.Defaults.generalDefaults.CopyUnattend
$State.Controls.chkCopyPPKG.IsChecked = $State.Defaults.generalDefaults.CopyPPKG
+ Set-DeviceNamingMode -State $State -Mode $State.Defaults.generalDefaults.DeviceNamingMode
+ $State.Controls.txtDeviceNameTemplate.Text = $State.Defaults.generalDefaults.DeviceNameTemplate
+ $State.Controls.txtDeviceNamePrefixesPath.Text = $State.Defaults.generalDefaults.DeviceNamePrefixesPath
+ $State.Controls.txtDeviceNamePrefixes.Text = ($State.Defaults.generalDefaults.DeviceNamePrefixes -join [System.Environment]::NewLine)
+ Import-DeviceNamePrefixesFromConfiguredPath -State $State
+ Update-DeviceNamingControls -State $State
$State.Controls.chkCleanupAppsISO.IsChecked = $State.Defaults.generalDefaults.CleanupAppsISO
$State.Controls.chkCleanupDeployISO.IsChecked = $State.Defaults.generalDefaults.CleanupDeployISO
$State.Controls.chkCleanupDrivers.IsChecked = $State.Defaults.generalDefaults.CleanupDrivers
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1
index 2ccabde..3aef928 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1
@@ -1173,6 +1173,9 @@ function Invoke-BrowseAction {
if (-not [string]::IsNullOrWhiteSpace($InitialDirectory)) {
$dialog.InitialDirectory = $InitialDirectory
}
+ if (-not [string]::IsNullOrWhiteSpace($FileName)) {
+ $dialog.FileName = $FileName
+ }
if ($dialog.ShowDialog()) {
return $dialog.FileName
}
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
index 6525348..a841359 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
@@ -106,10 +106,12 @@ function Get-GeneralDefaults {
$peDriversPath = Join-Path -Path $FFUDevelopmentPath -ChildPath "PEDrivers"
$vmLocationPath = Join-Path -Path $FFUDevelopmentPath -ChildPath "VM"
$ffuCapturePath = Join-Path -Path $FFUDevelopmentPath -ChildPath "FFU"
+ $unattendPath = Join-Path -Path $FFUDevelopmentPath -ChildPath "unattend"
$officePath = Join-Path -Path $appsPath -ChildPath "Office"
$appListJsonPath = Join-Path -Path $appsPath -ChildPath "AppList.json"
$userAppListPath = Join-Path -Path $appsPath -ChildPath "UserAppList.json"
$driversJsonPath = Join-Path -Path $driversPath -ChildPath "Drivers.json"
+ $deviceNamePrefixesPath = Join-Path -Path $unattendPath -ChildPath "prefixes.txt"
return [PSCustomObject]@{
# Build Tab Defaults
@@ -132,6 +134,10 @@ function Get-GeneralDefaults {
CopyUnattend = $false
CopyPPKG = $false
InjectUnattend = $false
+ DeviceNamingMode = 'None'
+ DeviceNameTemplate = ''
+ DeviceNamePrefixesPath = $deviceNamePrefixesPath
+ DeviceNamePrefixes = @()
CleanupAppsISO = $true
CleanupDeployISO = $true
CleanupDrivers = $false
diff --git a/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 b/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1
index 77b5878..ba94899 100644
--- a/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1
+++ b/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1
@@ -64,6 +64,68 @@ function Set-Computername($computername) {
return $computername
}
+function Get-UnattendComputerNameValue {
+ if ($null -eq $UnattendFile) {
+ return $null
+ }
+
+ [xml]$xml = Get-Content $UnattendFile
+ foreach ($component in $xml.unattend.settings.component) {
+ if ($component.ComputerName) {
+ return [string]$component.ComputerName
+ }
+ }
+
+ return $null
+}
+
+function Test-LegacyPromptComputerName($computername) {
+ if ([string]::IsNullOrWhiteSpace($computername)) {
+ return $false
+ }
+
+ $normalizedName = $computername.Trim().ToLowerInvariant()
+ return $normalizedName -in @('mycomputer', 'default')
+}
+
+function Get-NormalizedComputerName($computername) {
+ if ([string]::IsNullOrWhiteSpace($computername)) {
+ throw 'Computer name cannot be empty.'
+ }
+
+ $normalizedName = ($computername -replace "\s", '').Trim()
+ if ([string]::IsNullOrWhiteSpace($normalizedName)) {
+ throw 'Computer name cannot be empty after removing spaces.'
+ }
+
+ if ($normalizedName.Length -gt 15) {
+ $normalizedName = $normalizedName.Substring(0, 15)
+ }
+
+ return $normalizedName
+}
+
+function Resolve-ComputerNameTemplate($computerNameTemplate, $serialNumber) {
+ if ([string]::IsNullOrWhiteSpace($computerNameTemplate)) {
+ throw 'Computer name template cannot be empty.'
+ }
+
+ $resolvedName = $computerNameTemplate -replace '(?i)%serial%', $serialNumber
+ if ($resolvedName -match '%') {
+ throw 'Unsupported device name variable found. Only %serial% is supported.'
+ }
+
+ return Get-NormalizedComputerName($resolvedName)
+}
+
+function Set-ConfiguredComputerName($computername) {
+ $normalizedName = Get-NormalizedComputerName($computername)
+ $normalizedName = Set-Computername($normalizedName)
+ Writelog "Computer name will be set to $normalizedName"
+ Write-Host "Computer name will be set to $normalizedName"
+ return $normalizedName
+}
+
function Invoke-Process {
[CmdletBinding(SupportsShouldProcess)]
param
@@ -1023,8 +1085,19 @@ If (Test-Path -Path $UnattendComputerNamePath) {
}
}
-#Ask for device name if unattend exists
-If ($Unattend -or $UnattendPrefix -or $UnattendComputerName) {
+$UnattendConfiguredComputerName = $null
+$RequiresLegacyDeviceNamePrompt = $false
+$RequiresTemplateDeviceName = $false
+if ($Unattend) {
+ $UnattendConfiguredComputerName = Get-UnattendComputerNameValue
+ $RequiresLegacyDeviceNamePrompt = Test-LegacyPromptComputerName($UnattendConfiguredComputerName)
+ if (-not [string]::IsNullOrWhiteSpace($UnattendConfiguredComputerName) -and $UnattendConfiguredComputerName -match '(?i)%serial%') {
+ $RequiresTemplateDeviceName = $true
+ }
+}
+
+#Ask for device name if naming is explicitly required
+If ($UnattendPrefix -or $UnattendComputerName -or $RequiresTemplateDeviceName -or $RequiresLegacyDeviceNamePrompt) {
Write-SectionHeader 'Device Name Selection'
if ($Unattend -and $UnattendPrefix) {
Writelog 'Unattend file found with prefixes.txt. Getting prefixes.'
@@ -1060,17 +1133,8 @@ If ($Unattend -or $UnattendPrefix -or $UnattendComputerName) {
WriteLog "Will use $PrefixToUse as device name prefix"
Write-Host "Will use $PrefixToUse as device name prefix"
}
- #Get serial number to append. This can make names longer than 15 characters. Trim any leading or trailing whitespace
$serial = (Get-CimInstance -ClassName win32_bios).SerialNumber.Trim()
- #Combine prefix with serial
- $computername = ($PrefixToUse + $serial) -replace "\s", "" # Remove spaces because windows does not support spaces in the computer names
- #If computername is longer than 15 characters, reduce to 15. Sysprep/unattend doesn't like ComputerName being longer than 15 characters even though Windows accepts it
- If ($computername.Length -gt 15) {
- $computername = $computername.substring(0, 15)
- }
- $computername = Set-Computername($computername)
- Writelog "Computer name will be set to $computername"
- Write-Host "Computer name will be set to $computername"
+ $computername = Set-ConfiguredComputerName($PrefixToUse + $serial)
}
elseif ($Unattend -and $UnattendComputerName) {
Writelog 'Unattend file found with SerialComputerNames.csv. Getting name for current computer.'
@@ -1080,32 +1144,31 @@ If ($Unattend -or $UnattendPrefix -or $UnattendComputerName) {
$SCName = $SerialComputerNames | Where-Object { $_.SerialNumber -eq $SerialNumber }
If ($SCName) {
- [string]$computername = $SCName.ComputerName
- $computername = Set-Computername($computername)
- Writelog "Computer name will be set to $computername"
- Write-Host "Computer name will be set to $computername"
+ [string]$computername = Set-ConfiguredComputerName($SCName.ComputerName)
}
else {
Writelog 'No matching serial number found in SerialComputerNames.csv. Setting random computer name to complete setup.'
Write-Host 'No matching serial number found in SerialComputerNames.csv. Setting random computer name to complete setup.'
- [string]$computername = ("FFU-" + ( -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 11 | ForEach-Object { [char]$_ })))
- $computername = Set-Computername($computername)
- Writelog "Computer name will be set to $computername"
- Write-Host "Computer name will be set to $computername"
+ [string]$computername = Set-ConfiguredComputerName(("FFU-" + ( -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 11 | ForEach-Object { [char]$_ }))))
}
}
- elseif ($Unattend) {
+ elseif ($Unattend -and $RequiresTemplateDeviceName) {
+ Writelog 'Unattend file found with a %serial% computer name template. Resolving the template.'
+ $serialNumber = (Get-CimInstance -ClassName Win32_Bios).SerialNumber.Trim()
+ [string]$computername = Set-ConfiguredComputerName((Resolve-ComputerNameTemplate -computerNameTemplate $UnattendConfiguredComputerName -serialNumber $serialNumber))
+ }
+ elseif ($Unattend -and $RequiresLegacyDeviceNamePrompt) {
Writelog 'Unattend file found with no prefixes.txt, asking for name'
Write-Host 'Unattend file found but no prefixes.txt. Please enter a device name.'
- [string]$computername = Read-Host 'Enter device name'
- $computername = Set-Computername($computername)
- Writelog "Computer name will be set to $computername"
- Write-Host "Computer name will be set to $computername"
+ [string]$computername = Set-ConfiguredComputerName((Read-Host 'Enter device name'))
}
else {
WriteLog 'Device naming assets detected without unattend.xml. Skipping device naming prompts.'
}
}
+elseif ($Unattend) {
+ WriteLog 'Unattend file found. Device naming is not required, but unattend settings will still be applied.'
+}
else {
WriteLog 'No unattend folder found. Device name will be set via PPKG, AP JSON, or default OS name.'
}
@@ -1568,8 +1631,9 @@ If ($PPKGFileToInstall) {
}
}
#Set DeviceName
-If ($computername) {
- Write-SectionHeader -Title 'Applying Computer Name and Unattend Configuration'
+If ($Unattend) {
+ $unattendSectionTitle = if ($computername) { 'Applying Computer Name and Unattend Configuration' } else { 'Applying Unattend Configuration' }
+ Write-SectionHeader -Title $unattendSectionTitle
try {
$PantherDir = 'w:\windows\panther'
If (Test-Path -Path $PantherDir) {
@@ -1590,8 +1654,8 @@ If ($computername) {
}
}
catch {
- WriteLog "Copying Unattend.xml to name device failed"
- Stop-Script -Message "Copying Unattend.xml to name device failed with error: $_"
+ WriteLog 'Copying Unattend.xml to Panther failed'
+ Stop-Script -Message "Copying Unattend.xml to Panther failed with error: $_"
}
}
diff --git a/FFUDevelopment/config/Sample_default.json b/FFUDevelopment/config/Sample_default.json
index 689ef22c1cee97af442d0eee7acbba66f5e39a4d..af3cdb33d447449dc337a675b20442b2136b4d21 100644
GIT binary patch
delta 217
zcmbPa@XBbzG8TOohE#?!hD?TJAnnJH$dC(U=P{%+_%h@(qyWW~7_1l+fTSOgpU03o
zc_FJPCtSzm|164=d&Gp4F-=Kj2w*4z>Q4ijR{^ApC$6{DE(IEq$WQ`=sX$f=LktiW
mpekd~1Ikt~lz=SKhFb)68it`z=S4HbPG)2h-n@b(OBetJY%mxA
delta 12
TcmaE5G|6DYGM3H%SaXB{Cm{uH
diff --git a/docs/build.md b/docs/build.md
index b80c65f..b19fc83 100644
--- a/docs/build.md
+++ b/docs/build.md
@@ -199,24 +199,34 @@ This option is only available when **Build USB Drive** is checked.
When enabled, the build process copies:
- **unattend_x64.xml** (for x64 builds) or **unattend_arm64.xml** (for arm64 builds) → renamed to **Unattend.xml** on the USB drive
-- **prefixes.txt** (if present) → copied alongside the unattend file
+- **prefixes.txt** → created from the **Device Naming** prefixes list when that mode is selected
-During deployment, `ApplyFFU.ps1` detects the `Unattend` folder and uses these files to customize the device name and apply other Windows settings during OOBE.
+During deployment, `ApplyFFU.ps1` applies `Unattend.xml` whenever it is present. Device naming only happens when the **Device Naming** setting requires it, or when older media still uses the legacy prompt-based workflow.
#### Device Naming
-Device naming can be done from PE. The way this works is by leveraging an unattend.xml file to either take input from the user at imaging time or read a list of prefix values and append the serial number of the device. There are some major benefits to doing this:
+Use the **Device Naming** expander to decide whether `ComputerName` should be set at deployment time. There are some major benefits to doing this:
1. Total deployment time is reduced if naming is set at FFU deployment time since there is no additional reboot done during OOBE.
2. Reduces the need for multiple provisioning packages or autopilot profiles. This means you can use a single PPKG or autopilot profile.
-#### Prompt for Device Name
+#### No Device Name
-If you want to be prompted for the device name, simply check **Copy Unattend.xml.** This tells the build script to copy the appropriate architecture unattend_arch.xml file from the `C:\FFUDevelopment\Unattend` folder to the `.\unattend` folder on the deploy partition of the USB drive.
+This is the default option. The unattend file is still applied, but Windows generates a random computer name instead of forcing a prompt or a fixed name.
-#### Device Naming with prefixes.txt
+#### Specify Device Name
-If a `prefixes.txt` file exists in the `Unattend` folder and there are multiple prefixes in the file, the deployment script prompts the technician to select a prefix from the file. The prefix is combined with the device's serial number to create the computer name. If there is a single prefix, the technician is not prompted and the script will automatically select that prefix.
+Use this option when you want a static device name or a template such as `Comp-%serial%`.
+
+- With **Copy Unattend.xml**, `%serial%` is resolved during deployment in PE.
+- With **Inject Unattend.xml**, only static names are supported.
+- **Copy Unattend.xml** and **Inject Unattend.xml** are mutually exclusive. Select only one.
+
+#### Specify a list of Prefixes
+
+This option writes `prefixes.txt` from the list in the UI. Enter one prefix per line or import an existing prefixes file. The source file can use any name because the UI now tracks the path separately.
+
+If there is a single prefix, deployment uses it automatically. If there are multiple prefixes, the technician is prompted to select one. The selected prefix is combined with the device serial number to create the computer name.
For example, with a prefix of `CORP-` and a serial number of `ABC123`, the resulting computer name would be `CORP-ABC123` (truncated to 15 characters if necessary).
@@ -228,6 +238,10 @@ STORE-
KIOSK-
```
+#### Legacy Prompt Behavior
+
+Older deployment media that still has an unattend file with `ComputerName` set to the legacy placeholder value and no `prefixes.txt` file will still prompt for a device name during deployment.
+
{: .warning-title}
> Warning
@@ -483,6 +497,8 @@ Controls the `-InjectUnattend` parameter. When checked, copies the architecture-
This option is only available when **Install Apps** is checked.
+`Copy Unattend.xml` and `Inject Unattend.xml` are mutually exclusive. Select only one.
+
### How It Works
When enabled, the build process:
@@ -522,7 +538,7 @@ This option is primarily intended for scenarios where:
| Limitation | Description |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **No prefixes.txt support** | Unlike the**Copy Unattend** option for USB drives, this method does not support `prefixes.txt` for dynamic device naming based on serial numbers |
+| **No prefixes.txt or %serial% support** | Unlike **Copy Unattend.xml**, this method does not support `prefixes.txt` or the `%serial%` variable for deployment-time device naming |
| **Fixed configuration** | The unattend settings are baked into the FFU at build time and cannot be changed at deployment time |
| **Requires VM to be built** | This option only works when**Install Apps** is `$true` because the unattend file is included in the Apps ISO |
diff --git a/docs/parameters_reference.md b/docs/parameters_reference.md
index 6daff1e..67912f0 100644
--- a/docs/parameters_reference.md
+++ b/docs/parameters_reference.md
@@ -38,9 +38,13 @@ This table lists all top-level parameters in BuildFFUVM.ps1.
| -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. |
+| -CopyUnattend | bool | Copy Unattend.xml | When set to $true, will copy the $FFUDevelopmentPath\Unattend folder to the Deployment partition of the USB drive. Cannot be used together with -InjectUnattend. Default is $false. |
| -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}. |
+| -DeviceNamingMode | string | Device Naming expander | Controls how device naming is handled when unattend content is copied to USB media or injected into the FFU. Accepted values are Legacy, None, Template, and Prefixes. The UI uses None, Template, and Prefixes. |
+| -DeviceNameTemplate | string | Specify Device Name | Sets the device name used when DeviceNamingMode is Template. Supports a static name or the %serial% token when -CopyUnattend is used. |
+| -DeviceNamePrefixesPath | string | Prefixes File Path | Path to the source prefixes file used for legacy copy or when -DeviceNamePrefixes is not supplied. Default is $FFUDevelopmentPath\Unattend\prefixes.txt. |
+| -DeviceNamePrefixes | string[] | Specify a list of Prefixes | Sets the prefixes used when DeviceNamingMode is Prefixes. Each entry becomes a line in prefixes.txt on the deployment media. |
| -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. |
@@ -50,7 +54,7 @@ This table lists all top-level parameters in BuildFFUVM.ps1.
| -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. |
+| -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. Cannot be used together with -CopyUnattend. 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. |
diff --git a/docs/quickstart.md b/docs/quickstart.md
index add63bb..c1f5e29 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -186,18 +186,26 @@ Another safety measure is **Select Specific USB Drives**. When you check **Selec
**Device Naming**
-Device naming can be done from PE. The way this works is by leveraging an unattend.xml file to either take input from the user at imaging time or read a list of prefix values and append the serial number of the device. There are some major benefits to doing this:
+Use the **Device Naming** expander on the Build page to decide whether `ComputerName` should be set during deployment. There are some major benefits to doing this:
1. Total deployment time is reduced if naming is set at FFU deployment time since there is no additional reboot done during OOBE.
2. Reduces the need for multiple provisioning packages or autopilot profiles. This means you can use a single PPKG or autopilot profile.
-**Prompt for Device Name**
+**No Device Name**
-If you want to be prompted for the device name, simply check **Copy Unattend.xml.** This tells the build script to copy the appropriate architecture unattend_arch.xml file from the `C:\FFUDevelopment\Unattend` folder to the `.\unattend` folder on the deploy partition of the USB drive.
+This is the default option. The unattend file is still applied, but Windows generates a random computer name.
-**Specifying Multiple Name Prefixes**
+**Specify Device Name**
-If you have multiple device name prefixes for different locations or device use cases, or even a single prefix, you can specify a prefixes.txt file in the `C:\FFUDevelopment\unattend` folder. If the prefixes.txt file is detected and a single prefix is listed, the device will just use that prefix and append the serial number of the device. If there are multiple prefixes listed in the prefixes.txt file, you will be prompted to select which prefix you want to name the device and the serial number will be appended to that prefix. If you want a dash in the name, include the dash in the prefix (e.g. if ABCD- is in the prefixes.txt file, the device name will be ABCD-SerialNumber).
+Use this option when you want a static device name or a template such as `Comp-%serial%`.
+
+- With **Copy Unattend.xml**, `%serial%` is resolved during deployment in PE.
+- With **Inject Unattend.xml**, only static names are supported.
+- **Copy Unattend.xml** and **Inject Unattend.xml** are mutually exclusive. Select only one.
+
+**Specify a list of Prefixes**
+
+This option writes `prefixes.txt` from the list in the UI. Enter one prefix per line or import an existing prefixes file. The source file can use any name because the UI tracks the prefixes path separately. If there is one prefix, deployment uses it automatically. If there are multiple prefixes, the technician is prompted to select one and the serial number is appended to that prefix.
{: .warning-title}