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.
This commit is contained in:
rbalsleyMSFT
2026-04-07 10:48:34 -07:00
parent 78212f06d7
commit 4a2d8e63ea
13 changed files with 764 additions and 61 deletions
+213 -11
View File
@@ -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 {