Add support for SerialComputerNames CSV mapping

Introduces a new `SerialComputerNames` device naming mode that allows automated device naming during deployment based on the BIOS serial number. The mapping is provided via a CSV file with `SerialNumber` and `ComputerName` columns.

This feature requires `CopyUnattend` and writes a `SerialComputerNames.csv` file to the USB deployment media, replacing the need for manual prompts or prefix selection when device serial numbers are known in advance. The UI has been updated to support creating, loading, and saving the CSV mapping content.
This commit is contained in:
rbalsleyMSFT
2026-04-15 14:39:14 -07:00
parent 24f10b89b0
commit 38323e6be1
12 changed files with 335 additions and 15 deletions
+69 -3
View File
@@ -73,7 +73,7 @@ When set to $true, will copy the provisioning package from the $FFUDevelopmentPa
When set to $true, stages the selected architecture-specific unattend XML file as Unattend.xml on the deployment partition of the USB drive. Default is $false. When set to $true, stages the selected architecture-specific unattend XML file as Unattend.xml on the deployment partition of the USB drive. Default is $false.
.PARAMETER DeviceNamingMode .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, Prompt, Template, and Prefixes. Controls how device naming is handled when unattend content is copied to USB media or injected into the FFU. Supported values are Legacy, None, Prompt, Template, Prefixes, and SerialComputerNames.
.PARAMETER DeviceNameTemplate .PARAMETER DeviceNameTemplate
Sets the device name used when DeviceNamingMode is Template. Supports a static name or the %serial% token when CopyUnattend is used. Sets the device name used when DeviceNamingMode is Template. Supports a static name or the %serial% token when CopyUnattend is used.
@@ -84,6 +84,12 @@ Sets the prefixes used when DeviceNamingMode is Prefixes. Each entry becomes a l
.PARAMETER DeviceNamePrefixesPath .PARAMETER DeviceNamePrefixesPath
Path to the source prefixes file used for legacy copy or when DeviceNamePrefixes is not supplied. Default is $FFUDevelopmentPath\Unattend\prefixes.txt. Path to the source prefixes file used for legacy copy or when DeviceNamePrefixes is not supplied. Default is $FFUDevelopmentPath\Unattend\prefixes.txt.
.PARAMETER DeviceNameSerialComputerNames
Sets the CSV content used when DeviceNamingMode is SerialComputerNames. The CSV must include SerialNumber and ComputerName headers.
.PARAMETER DeviceNameSerialComputerNamesPath
Path to the source CSV file used when DeviceNamingMode is SerialComputerNames and DeviceNameSerialComputerNames is not supplied. Default is $FFUDevelopmentPath\Unattend\SerialComputerNames.csv.
.PARAMETER UnattendX64FilePath .PARAMETER UnattendX64FilePath
Path to the x64 unattend XML source file. Default is $FFUDevelopmentPath\Unattend\unattend_x64.xml. Path to the x64 unattend XML source file. Default is $FFUDevelopmentPath\Unattend\unattend_x64.xml.
@@ -425,11 +431,13 @@ param(
[bool]$AllowVHDXCaching, [bool]$AllowVHDXCaching,
[bool]$CopyPPKG, [bool]$CopyPPKG,
[bool]$CopyUnattend, [bool]$CopyUnattend,
[ValidateSet('Legacy', 'None', 'Prompt', 'Template', 'Prefixes')] [ValidateSet('Legacy', 'None', 'Prompt', 'Template', 'Prefixes', 'SerialComputerNames')]
[string]$DeviceNamingMode = 'Legacy', [string]$DeviceNamingMode = 'Legacy',
[string]$DeviceNameTemplate, [string]$DeviceNameTemplate,
[string[]]$DeviceNamePrefixes, [string[]]$DeviceNamePrefixes,
[string]$DeviceNamePrefixesPath, [string]$DeviceNamePrefixesPath,
[string[]]$DeviceNameSerialComputerNames,
[string]$DeviceNameSerialComputerNamesPath,
[string]$UnattendX64FilePath, [string]$UnattendX64FilePath,
[string]$UnattendArm64FilePath, [string]$UnattendArm64FilePath,
[bool]$CopyAutopilot, [bool]$CopyAutopilot,
@@ -644,7 +652,7 @@ function Save-StagedUnattendFile {
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$DestinationPath, [string]$DestinationPath,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[ValidateSet('Legacy', 'None', 'Prompt', 'Template', 'Prefixes')] [ValidateSet('Legacy', 'None', 'Prompt', 'Template', 'Prefixes', 'SerialComputerNames')]
[string]$DeviceNamingMode, [string]$DeviceNamingMode,
[string]$DeviceNameTemplate, [string]$DeviceNameTemplate,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
@@ -685,6 +693,11 @@ function Save-StagedUnattendFile {
$computerNamePath.ComputerNameElement.InnerText = '*' $computerNamePath.ComputerNameElement.InnerText = '*'
} }
} }
elseif ($DeviceNamingMode -eq 'SerialComputerNames') {
if ($computerNamePath.CreatedComputerNameElement) {
$computerNamePath.ComputerNameElement.InnerText = '*'
}
}
elseif (($DeviceNamingMode -eq 'Legacy') -and $computerNamePath.CreatedComputerNameElement) { elseif (($DeviceNamingMode -eq 'Legacy') -and $computerNamePath.CreatedComputerNameElement) {
$computerNamePath.ComputerNameElement.InnerText = if ($LegacyPrefixesWillBeStaged) { '*' } else { 'MyComputer' } $computerNamePath.ComputerNameElement.InnerText = if ($LegacyPrefixesWillBeStaged) { '*' } else { 'MyComputer' }
} }
@@ -707,12 +720,24 @@ $resolvedDeviceNamePrefixesPath = if ([string]::IsNullOrWhiteSpace($DeviceNamePr
else { else {
$DeviceNamePrefixesPath $DeviceNamePrefixesPath
} }
$effectiveDeviceNameSerialComputerNames = @($DeviceNameSerialComputerNames | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })
$resolvedDeviceNameSerialComputerNamesPath = if ([string]::IsNullOrWhiteSpace($DeviceNameSerialComputerNamesPath)) {
Join-Path (Join-Path $FFUDevelopmentPath 'Unattend') 'SerialComputerNames.csv'
}
else {
$DeviceNameSerialComputerNamesPath
}
if (($DeviceNamingMode -eq 'Prefixes') -and ($effectiveDeviceNamePrefixes.Count -eq 0) -and (Test-Path -Path $resolvedDeviceNamePrefixesPath -PathType Leaf)) { 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() }) $effectiveDeviceNamePrefixes = @(Get-Content -Path $resolvedDeviceNamePrefixesPath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })
WriteLog "Loaded device name prefixes from $resolvedDeviceNamePrefixesPath" WriteLog "Loaded device name prefixes from $resolvedDeviceNamePrefixesPath"
} }
if (($DeviceNamingMode -eq 'SerialComputerNames') -and ($effectiveDeviceNameSerialComputerNames.Count -eq 0) -and (Test-Path -Path $resolvedDeviceNameSerialComputerNamesPath -PathType Leaf)) {
$effectiveDeviceNameSerialComputerNames = @(Get-Content -Path $resolvedDeviceNameSerialComputerNamesPath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })
WriteLog "Loaded serial computer-name mappings from $resolvedDeviceNameSerialComputerNamesPath"
}
if ($CopyUnattend -and $InjectUnattend) { if ($CopyUnattend -and $InjectUnattend) {
throw 'CopyUnattend and InjectUnattend cannot both be set to `$true. Select only one unattend delivery method.' throw 'CopyUnattend and InjectUnattend cannot both be set to `$true. Select only one unattend delivery method.'
} }
@@ -749,6 +774,38 @@ elseif ($DeviceNamingMode -eq 'Prefixes') {
throw 'DeviceNamingMode Prefixes requires at least one DeviceNamePrefixes entry or a valid DeviceNamePrefixesPath.' throw 'DeviceNamingMode Prefixes requires at least one DeviceNamePrefixes entry or a valid DeviceNamePrefixesPath.'
} }
} }
elseif ($DeviceNamingMode -eq 'SerialComputerNames') {
if (-not $CopyUnattend) {
throw 'DeviceNamingMode SerialComputerNames requires CopyUnattend. Serial-to-computer-name mapping is not supported with InjectUnattend.'
}
if ($effectiveDeviceNameSerialComputerNames.Count -eq 0) {
throw 'DeviceNamingMode SerialComputerNames requires DeviceNameSerialComputerNames content or a valid DeviceNameSerialComputerNamesPath.'
}
try {
$serialComputerNameMappings = @($effectiveDeviceNameSerialComputerNames | ConvertFrom-Csv -ErrorAction Stop)
}
catch {
throw "DeviceNamingMode SerialComputerNames requires valid CSV content with SerialNumber and ComputerName headers. $($_.Exception.Message)"
}
if ($serialComputerNameMappings.Count -eq 0) {
throw 'DeviceNamingMode SerialComputerNames requires at least one CSV data row.'
}
$serialComputerNameHeaders = @($serialComputerNameMappings[0].PSObject.Properties.Name)
if ((-not ($serialComputerNameHeaders -contains 'SerialNumber')) -or (-not ($serialComputerNameHeaders -contains 'ComputerName'))) {
throw 'DeviceNamingMode SerialComputerNames requires SerialNumber and ComputerName headers.'
}
$validSerialComputerNameMappings = @($serialComputerNameMappings | Where-Object {
-not [string]::IsNullOrWhiteSpace([string]$_.SerialNumber) -and -not [string]::IsNullOrWhiteSpace([string]$_.ComputerName)
})
if ($validSerialComputerNameMappings.Count -eq 0) {
throw 'DeviceNamingMode SerialComputerNames requires at least one row with both SerialNumber and ComputerName values.'
}
}
# Validate that the selected Windows SKU is compatible with the chosen Windows release and ensure an ISO is provided for unsupported releases # Validate that the selected Windows SKU is compatible with the chosen Windows release and ensure an ISO is provided for unsupported releases
$clientSKUs = @( $clientSKUs = @(
@@ -4570,6 +4627,11 @@ Function New-DeploymentUSB {
$computerNamePath.ComputerNameElement.InnerText = '*' $computerNamePath.ComputerNameElement.InnerText = '*'
} }
} }
elseif ($DeviceNamingMode -eq 'SerialComputerNames') {
if ($computerNamePath.CreatedComputerNameElement) {
$computerNamePath.ComputerNameElement.InnerText = '*'
}
}
elseif (($DeviceNamingMode -eq 'Legacy') -and $computerNamePath.CreatedComputerNameElement) { elseif (($DeviceNamingMode -eq 'Legacy') -and $computerNamePath.CreatedComputerNameElement) {
$computerNamePath.ComputerNameElement.InnerText = if ($LegacyPrefixesWillBeStaged) { '*' } else { 'MyComputer' } $computerNamePath.ComputerNameElement.InnerText = if ($LegacyPrefixesWillBeStaged) { '*' } else { 'MyComputer' }
} }
@@ -4644,6 +4706,10 @@ Function New-DeploymentUSB {
WriteLog "Writing prefixes.txt file to $UnattendPathOnUSB" WriteLog "Writing prefixes.txt file to $UnattendPathOnUSB"
$using:effectiveDeviceNamePrefixes | Set-Content -Path (Join-Path $UnattendPathOnUSB 'prefixes.txt') -Encoding UTF8 $using:effectiveDeviceNamePrefixes | Set-Content -Path (Join-Path $UnattendPathOnUSB 'prefixes.txt') -Encoding UTF8
} }
elseif ($using:DeviceNamingMode -eq 'SerialComputerNames') {
WriteLog "Writing SerialComputerNames.csv file to $UnattendPathOnUSB"
$using:effectiveDeviceNameSerialComputerNames | Set-Content -Path (Join-Path $UnattendPathOnUSB 'SerialComputerNames.csv') -Encoding UTF8
}
elseif ($legacyPrefixesWillBeStaged) { elseif ($legacyPrefixesWillBeStaged) {
WriteLog "Copying prefixes.txt file to $UnattendPathOnUSB" WriteLog "Copying prefixes.txt file to $UnattendPathOnUSB"
Copy-Item -Path $using:resolvedDeviceNamePrefixesPath -Destination (Join-Path $UnattendPathOnUSB 'prefixes.txt') -Force | Out-Null Copy-Item -Path $using:resolvedDeviceNamePrefixesPath -Destination (Join-Path $UnattendPathOnUSB 'prefixes.txt') -Force | Out-Null
+16
View File
@@ -528,6 +528,22 @@ $script:uiState.Controls.btnRun.Add_Click({
return return
} }
} }
elseif ($config.DeviceNamingMode -eq 'SerialComputerNames') {
if (-not $config.CopyUnattend) {
[System.Windows.MessageBox]::Show("Select Copy Unattend.xml before using 'Specify Serial to Device Name Mapping'.", "Copy Unattend Required", "OK", "Warning") | Out-Null
$btnRun.IsEnabled = $true
$script:uiState.Controls.txtStatus.Text = "Build canceled: serial computer-name mapping requires Copy Unattend.xml."
return
}
$hasSavedSerialComputerNamesPath = -not [string]::IsNullOrWhiteSpace([string]$config.DeviceNameSerialComputerNamesPath) -and (Test-Path -Path $config.DeviceNameSerialComputerNamesPath -PathType Leaf)
if ((($null -eq $config.DeviceNameSerialComputerNames) -or ($config.DeviceNameSerialComputerNames.Count -eq 0)) -and -not $hasSavedSerialComputerNamesPath) {
[System.Windows.MessageBox]::Show("Enter CSV content or choose a valid SerialComputerNames.csv file before using 'Specify Serial to Device Name Mapping'.", "Serial Mapping Required", "OK", "Warning") | Out-Null
$btnRun.IsEnabled = $true
$script:uiState.Controls.txtStatus.Text = "Build canceled: serial computer-name mapping required."
return
}
}
$configFilePath = Join-Path $config.FFUDevelopmentPath "\config\FFUConfig.json" $configFilePath = Join-Path $config.FFUDevelopmentPath "\config\FFUConfig.json"
# Sort top-level keys alphabetically for consistent output # Sort top-level keys alphabetically for consistent output
+18
View File
@@ -963,6 +963,24 @@
<Button x:Name="btnSaveDeviceNamePrefixes" Content="Save Prefixes" Padding="12,4" ToolTip="Save the current prefixes list to the Prefixes File Path."/> <Button x:Name="btnSaveDeviceNamePrefixes" Content="Save Prefixes" Padding="12,4" ToolTip="Save the current prefixes list to the Prefixes File Path."/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<RadioButton x:Name="rbDeviceNamingSerialComputerNames" Content="Specify Serial to Device Name Mapping" GroupName="DeviceNamingMode" Margin="0,8,0,8" ToolTip="Create or import a SerialComputerNames.csv file. This option requires Copy Unattend.xml."/>
<StackPanel x:Name="deviceNameSerialComputerNamesPanel" Margin="32,0,0,0" Visibility="Collapsed">
<TextBlock Text="SerialComputerNames.csv File Path" Margin="0,0,0,8" TextWrapping="Wrap"/>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="txtDeviceNameSerialComputerNamesPath" Grid.Column="0" VerticalAlignment="Center" ToolTip="Path to the serial-to-device-name mapping CSV file. You can use any file name."/>
<Button x:Name="btnBrowseDeviceNameSerialComputerNamesPath" Grid.Column="1" Content="Browse..." Padding="12,4" Margin="8,0,0,0" VerticalAlignment="Center" ToolTip="Browse to a SerialComputerNames.csv source file path."/>
</Grid>
<TextBlock Text="Enter CSV content with SerialNumber and ComputerName headers. Each row maps one BIOS serial number to one computer name during deployment." Margin="0,0,0,4" TextWrapping="Wrap"/>
<TextBlock Text="Example: SerialNumber,ComputerName" Margin="0,0,0,8" TextWrapping="Wrap" Opacity="0.75"/>
<TextBox x:Name="txtDeviceNameSerialComputerNames" MinHeight="120" AcceptsReturn="True" TextWrapping="NoWrap" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" ToolTip="Each CSV row maps a serial number to a computer name."/>
<StackPanel Orientation="Horizontal" Margin="0,8,0,0" HorizontalAlignment="Left">
<Button x:Name="btnSaveDeviceNameSerialComputerNames" Content="Save Serial Mapping" Padding="12,4" ToolTip="Save the current serial-to-device-name mapping to the CSV file path."/>
</StackPanel>
</StackPanel>
</StackPanel> </StackPanel>
</Expander> </Expander>
@@ -40,6 +40,8 @@ function Get-UIConfig {
DeviceNameTemplate = $State.Controls.txtDeviceNameTemplate.Text DeviceNameTemplate = $State.Controls.txtDeviceNameTemplate.Text
DeviceNamePrefixesPath = $State.Controls.txtDeviceNamePrefixesPath.Text DeviceNamePrefixesPath = $State.Controls.txtDeviceNamePrefixesPath.Text
DeviceNamePrefixes = @(Get-DeviceNamePrefixes -State $State) DeviceNamePrefixes = @(Get-DeviceNamePrefixes -State $State)
DeviceNameSerialComputerNamesPath = $State.Controls.txtDeviceNameSerialComputerNamesPath.Text
DeviceNameSerialComputerNames = @(Get-SerialComputerNamesLines -State $State)
CopyAdditionalFFUFiles = $State.Controls.chkCopyAdditionalFFUFiles.IsChecked CopyAdditionalFFUFiles = $State.Controls.chkCopyAdditionalFFUFiles.IsChecked
CreateDeploymentMedia = $State.Controls.chkCreateDeploymentMedia.IsChecked CreateDeploymentMedia = $State.Controls.chkCreateDeploymentMedia.IsChecked
InjectUnattend = $State.Controls.chkInjectUnattend.IsChecked InjectUnattend = $State.Controls.chkInjectUnattend.IsChecked
@@ -473,21 +475,27 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'chkCopyUnattend' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyUnattend' -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 'chkCopyPPKG' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyPPKG' -State $State
Set-UIValue -ControlName 'txtDeviceNamePrefixesPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNamePrefixesPath' -State $State Set-UIValue -ControlName 'txtDeviceNamePrefixesPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNamePrefixesPath' -State $State
Set-UIValue -ControlName 'txtDeviceNameSerialComputerNamesPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNameSerialComputerNamesPath' -State $State
Set-UIValue -ControlName 'txtDeviceNameTemplate' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNameTemplate' -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 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
Set-UIValue -ControlName 'txtDeviceNameSerialComputerNames' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNameSerialComputerNames' -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)) { if ([string]::IsNullOrWhiteSpace($State.Controls.txtDeviceNamePrefixesPath.Text)) {
$State.Controls.txtDeviceNamePrefixesPath.Text = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $State.Controls.txtFFUDevPath.Text $State.Controls.txtDeviceNamePrefixesPath.Text = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $State.Controls.txtFFUDevPath.Text
} }
if ([string]::IsNullOrWhiteSpace($State.Controls.txtDeviceNameSerialComputerNamesPath.Text)) {
$State.Controls.txtDeviceNameSerialComputerNamesPath.Text = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $State.Controls.txtFFUDevPath.Text
}
$loadedDeviceNamingMode = $null $loadedDeviceNamingMode = $null
if ($ConfigContent.PSObject.Properties.Name -contains 'DeviceNamingMode') { if ($ConfigContent.PSObject.Properties.Name -contains 'DeviceNamingMode') {
$candidateDeviceNamingMode = [string]$ConfigContent.DeviceNamingMode $candidateDeviceNamingMode = [string]$ConfigContent.DeviceNamingMode
if ($candidateDeviceNamingMode -in @('Legacy', 'None', 'Prompt', 'Template', 'Prefixes')) { if ($candidateDeviceNamingMode -in @('Legacy', 'None', 'Prompt', 'Template', 'Prefixes', 'SerialComputerNames')) {
$loadedDeviceNamingMode = $candidateDeviceNamingMode $loadedDeviceNamingMode = $candidateDeviceNamingMode
} }
} }
$displayDeviceNamingMode = if ($loadedDeviceNamingMode -in @('Prompt', 'Template', 'Prefixes')) { $displayDeviceNamingMode = if ($loadedDeviceNamingMode -in @('Prompt', 'Template', 'Prefixes', 'SerialComputerNames')) {
$loadedDeviceNamingMode $loadedDeviceNamingMode
} }
else { else {
@@ -495,6 +503,7 @@ function Update-UIFromConfig {
} }
Set-DeviceNamingModeState -State $State -DisplayMode $displayDeviceNamingMode -LoadedMode $loadedDeviceNamingMode Set-DeviceNamingModeState -State $State -DisplayMode $displayDeviceNamingMode -LoadedMode $loadedDeviceNamingMode
Import-DeviceNamePrefixesFromConfiguredPath -State $State Import-DeviceNamePrefixesFromConfiguredPath -State $State
Import-SerialComputerNamesFromConfiguredPath -State $State
Update-DeviceNamingControls -State $State Update-DeviceNamingControls -State $State
# Post Build Cleanup group (Build Tab) # Post Build Cleanup group (Build Tab)
@@ -42,13 +42,17 @@ function Get-SelectedDeviceNamingMode {
return 'Prefixes' return 'Prefixes'
} }
if ($true -eq $State.Controls.rbDeviceNamingSerialComputerNames.IsChecked) {
return 'SerialComputerNames'
}
return 'None' return 'None'
} }
function Set-DeviceNamingMode { function Set-DeviceNamingMode {
param( param(
[PSCustomObject]$State, [PSCustomObject]$State,
[ValidateSet('None', 'Prompt', 'Template', 'Prefixes')] [ValidateSet('None', 'Prompt', 'Template', 'Prefixes', 'SerialComputerNames')]
[string]$Mode [string]$Mode
) )
@@ -56,12 +60,13 @@ function Set-DeviceNamingMode {
$State.Controls.rbDeviceNamingPrompt.IsChecked = $Mode -eq 'Prompt' $State.Controls.rbDeviceNamingPrompt.IsChecked = $Mode -eq 'Prompt'
$State.Controls.rbDeviceNamingTemplate.IsChecked = $Mode -eq 'Template' $State.Controls.rbDeviceNamingTemplate.IsChecked = $Mode -eq 'Template'
$State.Controls.rbDeviceNamingPrefixes.IsChecked = $Mode -eq 'Prefixes' $State.Controls.rbDeviceNamingPrefixes.IsChecked = $Mode -eq 'Prefixes'
$State.Controls.rbDeviceNamingSerialComputerNames.IsChecked = $Mode -eq 'SerialComputerNames'
} }
function Set-DeviceNamingModeState { function Set-DeviceNamingModeState {
param( param(
[PSCustomObject]$State, [PSCustomObject]$State,
[ValidateSet('None', 'Prompt', 'Template', 'Prefixes')] [ValidateSet('None', 'Prompt', 'Template', 'Prefixes', 'SerialComputerNames')]
[string]$DisplayMode, [string]$DisplayMode,
[AllowNull()] [AllowNull()]
[string]$LoadedMode [string]$LoadedMode
@@ -121,6 +126,20 @@ function Get-DeviceNamePrefixes {
) )
} }
function Get-SerialComputerNamesLines {
param([PSCustomObject]$State)
if ($null -eq $State.Controls.txtDeviceNameSerialComputerNames) {
return @()
}
return @(
$State.Controls.txtDeviceNameSerialComputerNames.Text -split "\r?\n" |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
ForEach-Object { $_.Trim() }
)
}
function Import-DeviceNamePrefixesFile { function Import-DeviceNamePrefixesFile {
param( param(
[PSCustomObject]$State, [PSCustomObject]$State,
@@ -140,6 +159,25 @@ function Import-DeviceNamePrefixesFile {
return $true return $true
} }
function Import-SerialComputerNamesFile {
param(
[PSCustomObject]$State,
[string]$FilePath
)
if ([string]::IsNullOrWhiteSpace($FilePath) -or -not (Test-Path -Path $FilePath -PathType Leaf)) {
return $false
}
$serialMappingLines = @(Get-Content -Path $FilePath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })
if ($null -ne $State.Controls.txtDeviceNameSerialComputerNamesPath) {
$State.Controls.txtDeviceNameSerialComputerNamesPath.Text = $FilePath
}
$State.Controls.txtDeviceNameSerialComputerNames.Text = $serialMappingLines -join [System.Environment]::NewLine
WriteLog "Imported serial computer-name mappings from $FilePath"
return $true
}
function Get-DefaultDeviceNamePrefixesPath { function Get-DefaultDeviceNamePrefixesPath {
param([string]$FFUDevelopmentPath) param([string]$FFUDevelopmentPath)
@@ -150,6 +188,16 @@ function Get-DefaultDeviceNamePrefixesPath {
return Join-Path (Join-Path $FFUDevelopmentPath 'unattend') 'prefixes.txt' return Join-Path (Join-Path $FFUDevelopmentPath 'unattend') 'prefixes.txt'
} }
function Get-DefaultSerialComputerNamesPath {
param([string]$FFUDevelopmentPath)
if ([string]::IsNullOrWhiteSpace($FFUDevelopmentPath)) {
return $null
}
return Join-Path (Join-Path $FFUDevelopmentPath 'unattend') 'SerialComputerNames.csv'
}
function Get-DefaultUnattendFilePath { function Get-DefaultUnattendFilePath {
param( param(
[string]$FFUDevelopmentPath, [string]$FFUDevelopmentPath,
@@ -188,6 +236,29 @@ function Import-DeviceNamePrefixesFromConfiguredPath {
} }
} }
function Import-SerialComputerNamesFromConfiguredPath {
param(
[PSCustomObject]$State,
[switch]$SkipIfTextPresent
)
if ($SkipIfTextPresent -and -not [string]::IsNullOrWhiteSpace($State.Controls.txtDeviceNameSerialComputerNames.Text)) {
return
}
$serialComputerNamesPath = $State.Controls.txtDeviceNameSerialComputerNamesPath.Text
if ([string]::IsNullOrWhiteSpace($serialComputerNamesPath)) {
$serialComputerNamesPath = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $State.Controls.txtFFUDevPath.Text
if (-not [string]::IsNullOrWhiteSpace($serialComputerNamesPath) -and $null -ne $State.Controls.txtDeviceNameSerialComputerNamesPath) {
$State.Controls.txtDeviceNameSerialComputerNamesPath.Text = $serialComputerNamesPath
}
}
if (Test-Path -Path $serialComputerNamesPath -PathType Leaf) {
Import-SerialComputerNamesFile -State $State -FilePath $serialComputerNamesPath | Out-Null
}
}
function Test-DeviceNameTemplateUsesSerialToken { function Test-DeviceNameTemplateUsesSerialToken {
param([PSCustomObject]$State) param([PSCustomObject]$State)
@@ -201,7 +272,7 @@ function Update-UnattendSelectionControls {
$isCopyUnattendSelected = $true -eq $State.Controls.chkCopyUnattend.IsChecked $isCopyUnattendSelected = $true -eq $State.Controls.chkCopyUnattend.IsChecked
$isInjectUnattendSelected = $true -eq $State.Controls.chkInjectUnattend.IsChecked $isInjectUnattendSelected = $true -eq $State.Controls.chkInjectUnattend.IsChecked
$deviceNameTemplateUsesSerialToken = Test-DeviceNameTemplateUsesSerialToken -State $State $deviceNameTemplateUsesSerialToken = Test-DeviceNameTemplateUsesSerialToken -State $State
$requiresCopiedUnattend = ($selectedDeviceNamingMode -in @('Prompt', 'Prefixes')) -or $deviceNameTemplateUsesSerialToken $requiresCopiedUnattend = ($selectedDeviceNamingMode -in @('Prompt', 'Prefixes', 'SerialComputerNames')) -or $deviceNameTemplateUsesSerialToken
if ($isCopyUnattendSelected -and $isInjectUnattendSelected) { if ($isCopyUnattendSelected -and $isInjectUnattendSelected) {
if ($requiresCopiedUnattend) { if ($requiresCopiedUnattend) {
@@ -247,19 +318,24 @@ function Update-UnattendSelectionControls {
function Update-DeviceNamingControls { function Update-DeviceNamingControls {
param([PSCustomObject]$State) param([PSCustomObject]$State)
if (($true -eq $State.Controls.chkInjectUnattend.IsChecked) -and (($true -eq $State.Controls.rbDeviceNamingPrompt.IsChecked) -or ($true -eq $State.Controls.rbDeviceNamingPrefixes.IsChecked))) { if (($true -eq $State.Controls.chkInjectUnattend.IsChecked) -and (($true -eq $State.Controls.rbDeviceNamingPrompt.IsChecked) -or ($true -eq $State.Controls.rbDeviceNamingPrefixes.IsChecked) -or ($true -eq $State.Controls.rbDeviceNamingSerialComputerNames.IsChecked))) {
$State.Controls.rbDeviceNamingNone.IsChecked = $true $State.Controls.rbDeviceNamingNone.IsChecked = $true
} }
$selectedDeviceNamingMode = Get-SelectedDeviceNamingMode -State $State $selectedDeviceNamingMode = Get-SelectedDeviceNamingMode -State $State
$State.Controls.deviceNameTemplatePanel.Visibility = if ($selectedDeviceNamingMode -eq 'Template') { 'Visible' } else { 'Collapsed' } $State.Controls.deviceNameTemplatePanel.Visibility = if ($selectedDeviceNamingMode -eq 'Template') { 'Visible' } else { 'Collapsed' }
$State.Controls.deviceNamePrefixesPanel.Visibility = if ($selectedDeviceNamingMode -eq 'Prefixes') { 'Visible' } else { 'Collapsed' } $State.Controls.deviceNamePrefixesPanel.Visibility = if ($selectedDeviceNamingMode -eq 'Prefixes') { 'Visible' } else { 'Collapsed' }
$State.Controls.deviceNameSerialComputerNamesPanel.Visibility = if ($selectedDeviceNamingMode -eq 'SerialComputerNames') { 'Visible' } else { 'Collapsed' }
$State.Controls.rbDeviceNamingPrompt.IsEnabled = -not ($true -eq $State.Controls.chkInjectUnattend.IsChecked) $State.Controls.rbDeviceNamingPrompt.IsEnabled = -not ($true -eq $State.Controls.chkInjectUnattend.IsChecked)
$State.Controls.rbDeviceNamingPrefixes.IsEnabled = -not ($true -eq $State.Controls.chkInjectUnattend.IsChecked) $State.Controls.rbDeviceNamingPrefixes.IsEnabled = -not ($true -eq $State.Controls.chkInjectUnattend.IsChecked)
$State.Controls.rbDeviceNamingSerialComputerNames.IsEnabled = -not ($true -eq $State.Controls.chkInjectUnattend.IsChecked)
if ($selectedDeviceNamingMode -eq 'Prefixes') { if ($selectedDeviceNamingMode -eq 'Prefixes') {
Import-DeviceNamePrefixesFromConfiguredPath -State $State -SkipIfTextPresent Import-DeviceNamePrefixesFromConfiguredPath -State $State -SkipIfTextPresent
} }
elseif ($selectedDeviceNamingMode -eq 'SerialComputerNames') {
Import-SerialComputerNamesFromConfiguredPath -State $State -SkipIfTextPresent
}
Update-UnattendSelectionControls -State $State Update-UnattendSelectionControls -State $State
} }
@@ -480,18 +556,24 @@ function Register-EventHandlers {
$selectedPath = Invoke-BrowseAction -Type 'Folder' -Title "Select FFU Development Path" $selectedPath = Invoke-BrowseAction -Type 'Folder' -Title "Select FFU Development Path"
if ($selectedPath) { if ($selectedPath) {
$currentPrefixesPath = $localState.Controls.txtDeviceNamePrefixesPath.Text $currentPrefixesPath = $localState.Controls.txtDeviceNamePrefixesPath.Text
$currentSerialComputerNamesPath = $localState.Controls.txtDeviceNameSerialComputerNamesPath.Text
$currentUnattendX64FilePath = $localState.Controls.txtUnattendX64FilePath.Text $currentUnattendX64FilePath = $localState.Controls.txtUnattendX64FilePath.Text
$currentUnattendArm64FilePath = $localState.Controls.txtUnattendArm64FilePath.Text $currentUnattendArm64FilePath = $localState.Controls.txtUnattendArm64FilePath.Text
$previousDefaultPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text $previousDefaultPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
$previousDefaultSerialComputerNamesPath = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
$previousDefaultUnattendX64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text -WindowsArch 'x64' $previousDefaultUnattendX64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text -WindowsArch 'x64'
$previousDefaultUnattendArm64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text -WindowsArch 'arm64' $previousDefaultUnattendArm64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text -WindowsArch 'arm64'
$localState.Controls.txtFFUDevPath.Text = $selectedPath $localState.Controls.txtFFUDevPath.Text = $selectedPath
$newDefaultPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $selectedPath $newDefaultPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $selectedPath
$newDefaultSerialComputerNamesPath = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $selectedPath
$newDefaultUnattendX64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $selectedPath -WindowsArch 'x64' $newDefaultUnattendX64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $selectedPath -WindowsArch 'x64'
$newDefaultUnattendArm64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $selectedPath -WindowsArch 'arm64' $newDefaultUnattendArm64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $selectedPath -WindowsArch 'arm64'
if ([string]::IsNullOrWhiteSpace($currentPrefixesPath) -or $currentPrefixesPath -ieq $previousDefaultPrefixesPath) { if ([string]::IsNullOrWhiteSpace($currentPrefixesPath) -or $currentPrefixesPath -ieq $previousDefaultPrefixesPath) {
$localState.Controls.txtDeviceNamePrefixesPath.Text = $newDefaultPrefixesPath $localState.Controls.txtDeviceNamePrefixesPath.Text = $newDefaultPrefixesPath
} }
if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath) -or $currentSerialComputerNamesPath -ieq $previousDefaultSerialComputerNamesPath) {
$localState.Controls.txtDeviceNameSerialComputerNamesPath.Text = $newDefaultSerialComputerNamesPath
}
if ([string]::IsNullOrWhiteSpace($currentUnattendX64FilePath) -or $currentUnattendX64FilePath -ieq $previousDefaultUnattendX64FilePath) { if ([string]::IsNullOrWhiteSpace($currentUnattendX64FilePath) -or $currentUnattendX64FilePath -ieq $previousDefaultUnattendX64FilePath) {
$localState.Controls.txtUnattendX64FilePath.Text = $newDefaultUnattendX64FilePath $localState.Controls.txtUnattendX64FilePath.Text = $newDefaultUnattendX64FilePath
} }
@@ -499,6 +581,7 @@ function Register-EventHandlers {
$localState.Controls.txtUnattendArm64FilePath.Text = $newDefaultUnattendArm64FilePath $localState.Controls.txtUnattendArm64FilePath.Text = $newDefaultUnattendArm64FilePath
} }
Import-DeviceNamePrefixesFromConfiguredPath -State $localState Import-DeviceNamePrefixesFromConfiguredPath -State $localState
Import-SerialComputerNamesFromConfiguredPath -State $localState
Update-DeviceNamingControls -State $localState Update-DeviceNamingControls -State $localState
} }
}) })
@@ -560,6 +643,16 @@ function Register-EventHandlers {
} }
Update-DeviceNamingControls -State $localState Update-DeviceNamingControls -State $localState
}) })
$State.Controls.rbDeviceNamingSerialComputerNames.Add_Checked({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
if (-not ($true -eq $localState.Flags.suppressDeviceNamingChangeTracking)) {
$localState.Flags.deviceNamingModeWasExplicitlyChanged = $true
$localState.Data.loadedDeviceNamingMode = $null
}
Update-DeviceNamingControls -State $localState
})
$State.Controls.btnBrowseDeviceNamePrefixesPath.Add_Click({ $State.Controls.btnBrowseDeviceNamePrefixesPath.Add_Click({
param($eventSource, $routedEventArgs) param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource) $window = [System.Windows.Window]::GetWindow($eventSource)
@@ -580,6 +673,26 @@ function Register-EventHandlers {
Update-DeviceNamingControls -State $localState Update-DeviceNamingControls -State $localState
} }
}) })
$State.Controls.btnBrowseDeviceNameSerialComputerNamesPath.Add_Click({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
$currentSerialComputerNamesPath = $localState.Controls.txtDeviceNameSerialComputerNamesPath.Text
if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) {
$currentSerialComputerNamesPath = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
}
$initialDirectory = if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) {
$null
}
else {
Split-Path $currentSerialComputerNamesPath -Parent
}
$fileName = if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) { 'SerialComputerNames.csv' } else { Split-Path $currentSerialComputerNamesPath -Leaf }
$selectedPath = Invoke-BrowseAction -Type 'OpenFile' -Title 'Select SerialComputerNames.csv file path' -Filter 'CSV files (*.csv)|*.csv|All files (*.*)|*.*' -InitialDirectory $initialDirectory -FileName $fileName
if (Import-SerialComputerNamesFile -State $localState -FilePath $selectedPath) {
Update-DeviceNamingControls -State $localState
}
})
$State.Controls.btnBrowseUnattendX64FilePath.Add_Click({ $State.Controls.btnBrowseUnattendX64FilePath.Add_Click({
param($eventSource, $routedEventArgs) param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource) $window = [System.Windows.Window]::GetWindow($eventSource)
@@ -653,6 +766,39 @@ function Register-EventHandlers {
[System.Windows.MessageBox]::Show("Saving prefixes failed for '$currentPrefixesPath'. $($_.Exception.Message)", "Save Prefixes Failed", "OK", "Error") | Out-Null [System.Windows.MessageBox]::Show("Saving prefixes failed for '$currentPrefixesPath'. $($_.Exception.Message)", "Save Prefixes Failed", "OK", "Error") | Out-Null
} }
}) })
$State.Controls.btnSaveDeviceNameSerialComputerNames.Add_Click({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
$serialComputerNameLines = @(Get-SerialComputerNamesLines -State $localState)
if ($serialComputerNameLines.Count -eq 0) {
[System.Windows.MessageBox]::Show("Enter CSV content before saving the serial mapping file.", "Serial Mapping Required", "OK", "Warning") | Out-Null
return
}
$currentSerialComputerNamesPath = $localState.Controls.txtDeviceNameSerialComputerNamesPath.Text
if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) {
$currentSerialComputerNamesPath = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
if (-not [string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) {
$localState.Controls.txtDeviceNameSerialComputerNamesPath.Text = $currentSerialComputerNamesPath
}
}
if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) {
[System.Windows.MessageBox]::Show("Select a valid SerialComputerNames.csv file path before saving the serial mapping.", "Serial Mapping File Path Required", "OK", "Warning") | Out-Null
return
}
try {
$serialComputerNameLines | Set-Content -Path $currentSerialComputerNamesPath -Encoding UTF8
$localState.Controls.txtDeviceNameSerialComputerNamesPath.Text = $currentSerialComputerNamesPath
WriteLog "Saved serial computer-name mappings to $currentSerialComputerNamesPath"
}
catch {
[System.Windows.MessageBox]::Show("Saving serial mapping failed for '$currentSerialComputerNamesPath'. $($_.Exception.Message)", "Save Serial Mapping Failed", "OK", "Error") | Out-Null
}
})
$State.Controls.chkCopyUnattend.Add_Checked({ $State.Controls.chkCopyUnattend.Add_Checked({
param($eventSource, $routedEventArgs) param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource) $window = [System.Windows.Window]::GetWindow($eventSource)
@@ -228,13 +228,19 @@ function Initialize-UIControls {
$State.Controls.rbDeviceNamingPrompt = $window.FindName('rbDeviceNamingPrompt') $State.Controls.rbDeviceNamingPrompt = $window.FindName('rbDeviceNamingPrompt')
$State.Controls.rbDeviceNamingTemplate = $window.FindName('rbDeviceNamingTemplate') $State.Controls.rbDeviceNamingTemplate = $window.FindName('rbDeviceNamingTemplate')
$State.Controls.rbDeviceNamingPrefixes = $window.FindName('rbDeviceNamingPrefixes') $State.Controls.rbDeviceNamingPrefixes = $window.FindName('rbDeviceNamingPrefixes')
$State.Controls.rbDeviceNamingSerialComputerNames = $window.FindName('rbDeviceNamingSerialComputerNames')
$State.Controls.deviceNameTemplatePanel = $window.FindName('deviceNameTemplatePanel') $State.Controls.deviceNameTemplatePanel = $window.FindName('deviceNameTemplatePanel')
$State.Controls.deviceNamePrefixesPanel = $window.FindName('deviceNamePrefixesPanel') $State.Controls.deviceNamePrefixesPanel = $window.FindName('deviceNamePrefixesPanel')
$State.Controls.deviceNameSerialComputerNamesPanel = $window.FindName('deviceNameSerialComputerNamesPanel')
$State.Controls.txtDeviceNameTemplate = $window.FindName('txtDeviceNameTemplate') $State.Controls.txtDeviceNameTemplate = $window.FindName('txtDeviceNameTemplate')
$State.Controls.txtDeviceNamePrefixesPath = $window.FindName('txtDeviceNamePrefixesPath') $State.Controls.txtDeviceNamePrefixesPath = $window.FindName('txtDeviceNamePrefixesPath')
$State.Controls.btnBrowseDeviceNamePrefixesPath = $window.FindName('btnBrowseDeviceNamePrefixesPath') $State.Controls.btnBrowseDeviceNamePrefixesPath = $window.FindName('btnBrowseDeviceNamePrefixesPath')
$State.Controls.txtDeviceNamePrefixes = $window.FindName('txtDeviceNamePrefixes') $State.Controls.txtDeviceNamePrefixes = $window.FindName('txtDeviceNamePrefixes')
$State.Controls.btnSaveDeviceNamePrefixes = $window.FindName('btnSaveDeviceNamePrefixes') $State.Controls.btnSaveDeviceNamePrefixes = $window.FindName('btnSaveDeviceNamePrefixes')
$State.Controls.txtDeviceNameSerialComputerNamesPath = $window.FindName('txtDeviceNameSerialComputerNamesPath')
$State.Controls.btnBrowseDeviceNameSerialComputerNamesPath = $window.FindName('btnBrowseDeviceNameSerialComputerNamesPath')
$State.Controls.txtDeviceNameSerialComputerNames = $window.FindName('txtDeviceNameSerialComputerNames')
$State.Controls.btnSaveDeviceNameSerialComputerNames = $window.FindName('btnSaveDeviceNameSerialComputerNames')
$State.Controls.chkVerbose = $window.FindName('chkVerbose') $State.Controls.chkVerbose = $window.FindName('chkVerbose')
$State.Controls.chkCopyAutopilot = $window.FindName('chkCopyAutopilot') $State.Controls.chkCopyAutopilot = $window.FindName('chkCopyAutopilot')
$State.Controls.chkCopyUnattend = $window.FindName('chkCopyUnattend') $State.Controls.chkCopyUnattend = $window.FindName('chkCopyUnattend')
@@ -400,7 +406,7 @@ function Initialize-UIDefaults {
$State.Controls.chkCopyAutopilot.IsChecked = $State.Defaults.generalDefaults.CopyAutopilot $State.Controls.chkCopyAutopilot.IsChecked = $State.Defaults.generalDefaults.CopyAutopilot
$State.Controls.chkCopyUnattend.IsChecked = $State.Defaults.generalDefaults.CopyUnattend $State.Controls.chkCopyUnattend.IsChecked = $State.Defaults.generalDefaults.CopyUnattend
$State.Controls.chkCopyPPKG.IsChecked = $State.Defaults.generalDefaults.CopyPPKG $State.Controls.chkCopyPPKG.IsChecked = $State.Defaults.generalDefaults.CopyPPKG
$defaultDeviceNamingMode = if ($State.Defaults.generalDefaults.DeviceNamingMode -in @('None', 'Prompt', 'Template', 'Prefixes')) { $defaultDeviceNamingMode = if ($State.Defaults.generalDefaults.DeviceNamingMode -in @('None', 'Prompt', 'Template', 'Prefixes', 'SerialComputerNames')) {
$State.Defaults.generalDefaults.DeviceNamingMode $State.Defaults.generalDefaults.DeviceNamingMode
} }
else { else {
@@ -410,7 +416,10 @@ function Initialize-UIDefaults {
$State.Controls.txtDeviceNameTemplate.Text = $State.Defaults.generalDefaults.DeviceNameTemplate $State.Controls.txtDeviceNameTemplate.Text = $State.Defaults.generalDefaults.DeviceNameTemplate
$State.Controls.txtDeviceNamePrefixesPath.Text = $State.Defaults.generalDefaults.DeviceNamePrefixesPath $State.Controls.txtDeviceNamePrefixesPath.Text = $State.Defaults.generalDefaults.DeviceNamePrefixesPath
$State.Controls.txtDeviceNamePrefixes.Text = ($State.Defaults.generalDefaults.DeviceNamePrefixes -join [System.Environment]::NewLine) $State.Controls.txtDeviceNamePrefixes.Text = ($State.Defaults.generalDefaults.DeviceNamePrefixes -join [System.Environment]::NewLine)
$State.Controls.txtDeviceNameSerialComputerNamesPath.Text = $State.Defaults.generalDefaults.DeviceNameSerialComputerNamesPath
$State.Controls.txtDeviceNameSerialComputerNames.Text = ($State.Defaults.generalDefaults.DeviceNameSerialComputerNames -join [System.Environment]::NewLine)
Import-DeviceNamePrefixesFromConfiguredPath -State $State Import-DeviceNamePrefixesFromConfiguredPath -State $State
Import-SerialComputerNamesFromConfiguredPath -State $State
Update-DeviceNamingControls -State $State Update-DeviceNamingControls -State $State
$State.Controls.chkCleanupAppsISO.IsChecked = $State.Defaults.generalDefaults.CleanupAppsISO $State.Controls.chkCleanupAppsISO.IsChecked = $State.Defaults.generalDefaults.CleanupAppsISO
$State.Controls.chkCleanupDeployISO.IsChecked = $State.Defaults.generalDefaults.CleanupDeployISO $State.Controls.chkCleanupDeployISO.IsChecked = $State.Defaults.generalDefaults.CleanupDeployISO
@@ -112,6 +112,7 @@ function Get-GeneralDefaults {
$userAppListPath = Join-Path -Path $appsPath -ChildPath "UserAppList.json" $userAppListPath = Join-Path -Path $appsPath -ChildPath "UserAppList.json"
$driversJsonPath = Join-Path -Path $driversPath -ChildPath "Drivers.json" $driversJsonPath = Join-Path -Path $driversPath -ChildPath "Drivers.json"
$deviceNamePrefixesPath = Join-Path -Path $unattendPath -ChildPath "prefixes.txt" $deviceNamePrefixesPath = Join-Path -Path $unattendPath -ChildPath "prefixes.txt"
$deviceNameSerialComputerNamesPath = Join-Path -Path $unattendPath -ChildPath "SerialComputerNames.csv"
$unattendX64FilePath = Join-Path -Path $unattendPath -ChildPath "unattend_x64.xml" $unattendX64FilePath = Join-Path -Path $unattendPath -ChildPath "unattend_x64.xml"
$unattendArm64FilePath = Join-Path -Path $unattendPath -ChildPath "unattend_arm64.xml" $unattendArm64FilePath = Join-Path -Path $unattendPath -ChildPath "unattend_arm64.xml"
@@ -142,6 +143,8 @@ function Get-GeneralDefaults {
DeviceNameTemplate = '' DeviceNameTemplate = ''
DeviceNamePrefixesPath = $deviceNamePrefixesPath DeviceNamePrefixesPath = $deviceNamePrefixesPath
DeviceNamePrefixes = @() DeviceNamePrefixes = @()
DeviceNameSerialComputerNamesPath = $deviceNameSerialComputerNamesPath
DeviceNameSerialComputerNames = @()
CleanupAppsISO = $true CleanupAppsISO = $true
CleanupDeployISO = $true CleanupDeployISO = $true
CleanupDrivers = $false CleanupDrivers = $false
Binary file not shown.
@@ -0,0 +1,4 @@
SerialNumber,ComputerName
ABC12345,CORP-001
DEF67890,KIOSK-010
XYZ24680,STORE-015
1 SerialNumber ComputerName
2 ABC12345 CORP-001
3 DEF67890 KIOSK-010
4 XYZ24680 STORE-015
+37 -4
View File
@@ -340,7 +340,7 @@ This option is primarily intended for scenarios where:
| Limitation | Description | | Limitation | Description |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **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 | | **No prefixes.txt, SerialComputerNames.csv, or %serial% support** | Unlike **Copy Unattend.xml**, this method does not support `prefixes.txt`, `SerialComputerNames.csv`, 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 | | **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 | | **Requires VM to be built** | This option only works when**Install Apps** is `$true` because the unattend file is included in the Apps ISO |
@@ -348,7 +348,7 @@ This option is primarily intended for scenarios where:
> Note > Note
> >
> Most users should continue using the **Copy Unattend** option via the USB drive, which provides more flexibility including support for `prefixes.txt` device naming. Use **Inject Unattend.xml** only when you won't be using the USB drive for deployment. > Most users should continue using the **Copy Unattend** option via the USB drive, which provides more flexibility including support for `prefixes.txt`, `SerialComputerNames.csv`, and `%serial%` device naming. Use **Inject Unattend.xml** only when you won't be using the USB drive for deployment.
{: .tip-title} {: .tip-title}
@@ -423,6 +423,37 @@ Use **Prefixes File Path** to point the UI at the source text file for the prefi
Use **Save Prefixes** to write the current multiline prefixes list back to the file specified in **Prefixes File Path**. Use **Save Prefixes** to write the current multiline prefixes list back to the file specified in **Prefixes File Path**.
### Specify Serial to Device Name Mapping
This option writes `SerialComputerNames.csv` from the CSV content in the UI. Use `SerialNumber,ComputerName` as the header row, then add one row per device. During deployment, `ApplyFFU.ps1` compares the current BIOS serial number to the `SerialNumber` column and uses the matching `ComputerName` value.
Sample `SerialComputerNames.csv` content:
```plaintext
SerialNumber,ComputerName
ABC12345,CORP-001
DEF67890,KIOSK-010
XYZ24680,STORE-015
```
- This option requires **Copy Unattend.xml**.
- **Inject Unattend.xml** is not supported with this option.
- If no matching serial number is found during deployment, `ApplyFFU.ps1` falls back to a random `FFU-*` computer name so setup can finish.
{: .note-title}
> Note
>
> If `prefixes.txt` and `SerialComputerNames.csv` are both staged manually on the same deployment media, `ApplyFFU.ps1` checks `prefixes.txt` first. FFU Builder avoids this conflict by only staging the naming file for the selected device-naming mode.
### SerialComputerNames.csv File Path
Use **SerialComputerNames.csv File Path** to point the UI at the source CSV file for the serial-to-device-name mapping. The file can use any name. When you browse to a mapping file in the UI, or when a saved configuration references a valid CSV path, the UI loads that file and populates the multiline CSV box from its contents.
### Save Serial Mapping
Use **Save Serial Mapping** to write the current CSV content back to the file specified in **SerialComputerNames.csv File Path**.
### Deployment Prompt Compatibility ### Deployment Prompt Compatibility
Older deployment media that already 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. Older deployment media that already 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.
@@ -443,8 +474,9 @@ The `.\FFUDevelopment\Unattend` folder includes sample files you can customize:
| **unattend_x64.xml** | Active unattend file used for x64 builds | | **unattend_x64.xml** | Active unattend file used for x64 builds |
| **unattend_arm64.xml** | Active unattend file used for arm64 builds | | **unattend_arm64.xml** | Active unattend file used for arm64 builds |
| **SamplePrefixes.txt** | Example prefixes file for device naming | | **SamplePrefixes.txt** | Example prefixes file for device naming |
| **SampleSerialComputerNames.csv** | Example serial-to-device-name CSV file |
Copy and customize the sample files to create your own `unattend_x64.xml`, `unattend_arm64.xml`, and `prefixes.txt` files. Copy and customize the sample files to create your own `unattend_x64.xml`, `unattend_arm64.xml`, `prefixes.txt`, and `SerialComputerNames.csv` files.
{: .note-title} {: .note-title}
@@ -537,12 +569,13 @@ When enabled, the build process copies:
- The selected x64 or arm64 unattend XML file → renamed to **Unattend.xml** on the USB drive - The selected x64 or arm64 unattend XML file → renamed to **Unattend.xml** on the USB drive
- **prefixes.txt** → created from the **Device Naming** prefixes list when that mode is selected - **prefixes.txt** → created from the **Device Naming** prefixes list when that mode is selected
- **SerialComputerNames.csv** → created from the **Device Naming** serial mapping list when that mode is selected
If you keep the default file paths in place, FFU Builder uses `unattend_x64.xml` for x64 builds and `unattend_arm64.xml` for arm64 builds. If you keep the default file paths in place, FFU Builder uses `unattend_x64.xml` for x64 builds and `unattend_arm64.xml` for arm64 builds.
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. 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.
See **Device Naming Expander** above for the available computer-name modes and prefixes-file behavior. See **Device Naming Expander** above for the available computer-name modes and naming-file behavior.
### Copy Provisioning Package ### Copy Provisioning Package
+3 -1
View File
@@ -41,12 +41,14 @@ This table lists all top-level parameters in BuildFFUVM.ps1.
| -CopyUnattend | bool | Copy Unattend.xml | When set to $true, stages the selected architecture-specific unattend XML file as Unattend.xml on the Deployment partition of the USB drive. Cannot be used together with -InjectUnattend. Default is $false. | | -CopyUnattend | bool | Copy Unattend.xml | When set to $true, stages the selected architecture-specific unattend XML file as Unattend.xml on 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. | | -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}. | | -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, Prompt, Template, and Prefixes. The UI shows None, Prompt, Template, and Prefixes. When device naming is left untouched in the UI, the generated config does not write DeviceNamingMode, which preserves the script default of Legacy. Prompt rewrites the staged deployment unattend to the existing manual prompt placeholder and requires -CopyUnattend. | | -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, Prompt, Template, Prefixes, and SerialComputerNames. The UI shows None, Prompt, Template, Prefixes, and SerialComputerNames. When device naming is left untouched in the UI, the generated config does not write DeviceNamingMode, which preserves the script default of Legacy. Prompt rewrites the staged deployment unattend to the existing manual prompt placeholder and requires -CopyUnattend. Prefixes writes prefixes.txt and requires -CopyUnattend. SerialComputerNames writes SerialComputerNames.csv and requires -CopyUnattend. |
| -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. | | -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. | | -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. |
| -DeviceNameSerialComputerNamesPath | string | SerialComputerNames.csv File Path | Path to the source CSV file used when DeviceNamingMode is SerialComputerNames and -DeviceNameSerialComputerNames is not supplied. Default is $FFUDevelopmentPath\Unattend\SerialComputerNames.csv. |
| -UnattendX64FilePath | string | x64 Unattend File Path | Path to the x64 unattend XML source file used by Copy Unattend.xml and Inject Unattend.xml. Default is $FFUDevelopmentPath\Unattend\unattend_x64.xml. | | -UnattendX64FilePath | string | x64 Unattend File Path | Path to the x64 unattend XML source file used by Copy Unattend.xml and Inject Unattend.xml. Default is $FFUDevelopmentPath\Unattend\unattend_x64.xml. |
| -UnattendArm64FilePath | string | arm64 Unattend File Path | Path to the arm64 unattend XML source file used by Copy Unattend.xml and Inject Unattend.xml. Default is $FFUDevelopmentPath\Unattend\unattend_arm64.xml. | | -UnattendArm64FilePath | string | arm64 Unattend File Path | Path to the arm64 unattend XML source file used by Copy Unattend.xml and Inject Unattend.xml. Default is $FFUDevelopmentPath\Unattend\unattend_arm64.xml. |
| -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. | | -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. |
| -DeviceNameSerialComputerNames | string[] | Specify Serial to Device Name Mapping | Sets the CSV content used when DeviceNamingMode is SerialComputerNames. The content must include SerialNumber and ComputerName headers, and the staged file is written as SerialComputerNames.csv 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. | | -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. | | -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. | | -DriversJsonPath | string | Drivers.json Path | Path to a JSON file that specifies which drivers to download. |
+14
View File
@@ -213,6 +213,20 @@ This option writes `prefixes.txt` from the list in the UI. Enter one prefix per
> >
> If the technician skips prefix selection when multiple prefixes are available, `ApplyFFU.ps1` leaves the existing unattend `ComputerName` value unchanged. With the current unattend samples set to `<ComputerName>*</ComputerName>`, Windows falls back to its default random computer-name behavior, typically resulting in a name such as `WIN-*`. > If the technician skips prefix selection when multiple prefixes are available, `ApplyFFU.ps1` leaves the existing unattend `ComputerName` value unchanged. With the current unattend samples set to `<ComputerName>*</ComputerName>`, Windows falls back to its default random computer-name behavior, typically resulting in a name such as `WIN-*`.
**Specify Serial to Device Name Mapping**
This option writes `SerialComputerNames.csv` from the CSV content in the UI. Use `SerialNumber,ComputerName` as the header row, then add one row per device. During deployment, `ApplyFFU.ps1` compares the current BIOS serial number to the CSV and applies the matching computer name.
- This option requires **Copy Unattend.xml**.
- **Inject Unattend.xml** is not supported with this option.
- If no matching serial number is found during deployment, `ApplyFFU.ps1` falls back to a random `FFU-*` computer name.
{: .note-title}
> Note
>
> If `prefixes.txt` and `SerialComputerNames.csv` are both present on the same deployment media, `ApplyFFU.ps1` checks `prefixes.txt` first. FFU Builder stages only the naming file for the selected device-naming mode.
{: .warning-title} {: .warning-title}
> Warning > Warning