Files
FFU/FFUDevelopment/FFU.Common/FFU.Common.Drivers.Dell.psm1
rbalsleyMSFT 895728ebe8 Prefer DeviceGroup arch for Dell client drivers
Use DeviceGroup OperatingSystem osArch when selecting Dell client driver packages, falling back to SupportedOperatingSystems when DeviceGroup metadata is absent. This keeps existing server catalog behavior unchanged.
2026-06-05 10:41:00 -07:00

317 lines
14 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
.SYNOPSIS
Common Dell driver helpers (catalog index, model listing, latest package selection).
#>
function Convert-DellVendorVersion {
param([Parameter(Mandatory=$true)][string]$VendorVersion)
$segments = $VendorVersion.Split('.') | ForEach-Object {
if ($_ -match '^\d+$') { [int]$_ } else { 0 }
}
return ,$segments
}
function Compare-DellVendorVersion {
param(
[int[]]$Left,
[int[]]$Right
)
$len = [Math]::Max($Left.Length,$Right.Length)
for ($i=0; $i -lt $len; $i++) {
$l = if ($i -lt $Left.Length) { $Left[$i] } else { 0 }
$r = if ($i -lt $Right.Length) { $Right[$i] } else { 0 }
if ($l -gt $r) { return 1 }
if ($l -lt $r) { return -1 }
}
return 0
}
function Test-DellDriverComponentOsArch {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][System.Xml.XmlElement]$Component,
[Parameter(Mandatory=$true)][ValidateSet('x64', 'x86', 'ARM64')][string]$WindowsArch
)
$deviceGroupOsNodes = @($Component.SelectNodes("*[local-name()='DeviceGroup']/*[local-name()='OperatingSystem']"))
if ($deviceGroupOsNodes.Count -gt 0) {
$validDeviceGroupOs = $deviceGroupOsNodes | Where-Object { $_.GetAttribute('osArch') -eq $WindowsArch } | Select-Object -First 1
return $null -ne $validDeviceGroupOs
}
$supportedOsNodes = @($Component.SelectNodes("*[local-name()='SupportedOperatingSystems']/*[local-name()='OperatingSystem']"))
if ($supportedOsNodes.Count -eq 0) { return $false }
$validSupportedOs = $supportedOsNodes | Where-Object { $_.GetAttribute('osArch') -eq $WindowsArch } | Select-Object -First 1
return $null -ne $validSupportedOs
}
function Get-DellCatalogIndex {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$DriversFolder
)
$dellFolder = Join-Path $DriversFolder 'Dell'
if (-not (Test-Path $dellFolder)) { New-Item -Path $dellFolder -ItemType Directory -Force | Out-Null }
$cabPath = Join-Path $dellFolder 'CatalogIndexPC.cab'
$xmlPath = Join-Path $dellFolder 'CatalogIndexPC.xml'
$url = 'https://downloads.dell.com/catalog/CatalogIndexPC.cab'
$need = $true
if (Test-Path $xmlPath) {
$ageDays = ((Get-Date) - (Get-Item $xmlPath).CreationTime).TotalDays
if ($ageDays -lt 7) { $need = $false }
}
if ($need) {
if (Test-Path $cabPath) { Remove-Item $cabPath -Force -ErrorAction SilentlyContinue }
if (Test-Path $xmlPath) { Remove-Item $xmlPath -Force -ErrorAction SilentlyContinue }
Start-BitsTransferWithRetry -Source $url -Destination $cabPath
Invoke-Process -FilePath Expand.exe -ArgumentList """$cabPath"" ""$xmlPath""" | Out-Null
Remove-Item $cabPath -Force -ErrorAction SilentlyContinue
}
if (-not (Test-Path $xmlPath)) { throw "Dell CatalogIndexPC XML missing: $xmlPath" }
return $xmlPath
}
function Get-DellClientModels {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$CatalogIndexXmlPath
)
$settings = New-Object System.Xml.XmlReaderSettings
$settings.IgnoreWhitespace = $true
$settings.IgnoreComments = $true
$reader = [System.Xml.XmlReader]::Create($CatalogIndexXmlPath,$settings)
$models = [System.Collections.Generic.List[pscustomobject]]::new()
try {
while ($reader.Read()) {
if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq 'GroupManifest') {
# Read subtree to pick out brand/model/systemID + path
$sub = $reader.ReadSubtree()
$doc = New-Object System.Xml.XmlDocument
$doc.Load($sub)
$sub.Dispose()
# Use local-name() to ignore namespaces
$brandNode = $doc.SelectSingleNode("//*[local-name()='SupportedSystems']/*[local-name()='Brand']")
if (-not $brandNode) { continue }
$brandDisplay = ($brandNode.SelectSingleNode("*[local-name()='Display']")?.InnerText).Trim()
$modelNode = $brandNode.SelectSingleNode("*[local-name()='Model']")
if (-not $modelNode) { continue }
$modelNumber = ($modelNode.SelectSingleNode("*[local-name()='Display']")?.InnerText).Trim()
$systemId = $modelNode.GetAttribute('systemID')
$manifestInfo = $doc.SelectSingleNode("//*[local-name()='ManifestInformation']")
if (-not $manifestInfo) { continue }
$pathAttr = $manifestInfo.GetAttribute('path')
if (-not $pathAttr) { continue }
$cabUrl = 'https://downloads.dell.com/' + $pathAttr
# Normalize model display using GroupManifest Display CDATA if available (strip 'PDK Catalog for')
$gmDisplayNode = $doc.SelectSingleNode("/*[local-name()='GroupManifest']/*[local-name()='Display']")
$modelFull = $null
if ($gmDisplayNode -and $gmDisplayNode.InnerText) {
$rawDisplay = $gmDisplayNode.InnerText.Trim()
$modelFull = ($rawDisplay -replace '^\s*PDK Catalog for\s+','').Trim()
}
if ([string]::IsNullOrWhiteSpace($modelFull)) {
# Fallback: assemble from brand/model nodes (legacy heuristic)
$prefixedModelNumber = $modelNumber
if ($modelNumber -and $brandDisplay) {
if ($modelNumber.StartsWith($brandDisplay,[System.StringComparison]::OrdinalIgnoreCase)) {
$prefixedModelNumber = $modelNumber
}
else {
$prefixedModelNumber = "$brandDisplay $modelNumber"
}
}
elseif ($brandDisplay -and -not $modelNumber) {
$prefixedModelNumber = $brandDisplay
}
$modelFull = $prefixedModelNumber
}
$modelDisplay = "$modelFull ($systemId)"
$models.Add([pscustomobject]@{
Brand = $brandDisplay
ModelNumber = $modelNumber
SystemId = $systemId
CabRelativePath = $pathAttr
CabUrl = $cabUrl
ModelDisplay = $modelDisplay
})
}
}
}
finally {
$reader.Dispose()
}
return $models
}
function Get-DellLatestDriverPackages {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$ModelXmlPath,
[Parameter(Mandatory=$true)][string]$WindowsArch,
[Parameter(Mandatory=$true)][int]$WindowsRelease
)
if (-not (Test-Path $ModelXmlPath)) { throw "Model XML not found: $ModelXmlPath" }
$xml = [xml](Get-Content -Path $ModelXmlPath -Raw)
# Collect all SoftwareComponent nodes
$components = $xml.SelectNodes("//*[local-name()='SoftwareComponent']")
if (-not $components) { return @() }
$rawPackages = [System.Collections.Generic.List[pscustomobject]]::new()
foreach ($comp in $components) {
$ctype = $comp.SelectSingleNode("*[local-name()='ComponentType']")
if (-not $ctype) { continue }
if ($ctype.GetAttribute('value') -ne 'DRVR') { continue }
# OS filtering (arch only release filtering intentionally minimal for now)
if (-not (Test-DellDriverComponentOsArch -Component $comp -WindowsArch $WindowsArch)) { continue }
$path = $comp.GetAttribute('path')
if (-not $path) { continue }
$downloadUrl = "https://downloads.dell.com/$path"
$fileName = [IO.Path]::GetFileName($path)
$vendorVersion = $comp.GetAttribute('vendorVersion')
$versionArr = if ($vendorVersion) { Convert-DellVendorVersion $vendorVersion } else { @(0) }
$dateTimeAttr = $comp.GetAttribute('dateTime')
$dt = Get-Date
if ($dateTimeAttr) {
try { $dt = [DateTime]::Parse($dateTimeAttr) } catch { }
}
$categoryNode = $comp.SelectSingleNode("*[local-name()='Category']/*[local-name()='Display']")
$category = if ($categoryNode) { $categoryNode.InnerText.Trim() } else { 'Uncategorized' }
# Collect componentIDs (SupportedDevices + SupportedDCHDevices)
$compIds = [System.Collections.Generic.List[string]]::new()
$devNodes = @($comp.SelectNodes(".//*[local-name()='Device']"))
foreach ($dn in $devNodes) {
$id = $dn.GetAttribute('componentID')
if ($id) { [void]$compIds.Add($id) }
}
if ($compIds.Count -eq 0) { continue }
# Build a deterministic sortable key: zero-pad each numeric segment to 6 digits
$versionSortable = ($versionArr | ForEach-Object { $_.ToString('D6') }) -join '-'
# Capture a humanreadable driver name (preserve spaces like HP/Lenovo; remove only illegal path chars and extra whitespace)
$displayNode = $comp.SelectSingleNode("*[local-name()='Name']/*[local-name()='Display']")
$nameRaw = if ($displayNode) { $displayNode.InnerText.Trim() } else { $fileName }
# Remove characters not suitable for display (and disallowed in file names) but keep spaces
$nameDisplay = $nameRaw -replace '[\\\/:\*\?\"\<\>\|]', ' ' -replace '[,]', '-'
# Collapse multiple spaces to single
$nameDisplay = ($nameDisplay -replace '\s+', ' ').Trim()
$rawPackages.Add([pscustomobject]@{
Path = $path
DownloadUrl = $downloadUrl
FileName = $fileName
Name = $nameDisplay
Category = $category
VendorVersion = $vendorVersion
VersionArray = $versionArr
VersionSortable = $versionSortable
DateTime = $dt
ComponentIds = $compIds
})
}
if ($rawPackages.Count -eq 0) { return @() }
# Sort newest first by VersionSortable (lexicographic works due to zero padding) then DateTime
$sorted = $rawPackages | Sort-Object -Property @{ Expression = { $_.VersionSortable }; Descending = $true }, @{ Expression = { $_.DateTime }; Descending = $true }
$chosen = [System.Collections.Generic.List[pscustomobject]]::new()
$assignedIds = [System.Collections.Generic.HashSet[string]]::new()
foreach ($pkg in $sorted) {
$hasOverlap = $false
foreach ($cid in $pkg.ComponentIds) {
if ($assignedIds.Contains($cid)) { $hasOverlap = $true; break }
}
if ($hasOverlap) {
WriteLog "Get-DellLatestDriverPackages: Skipping superseded package $($pkg.FileName) (shared componentID with newer package)."
continue
}
foreach ($cid in $pkg.ComponentIds) { [void]$assignedIds.Add($cid) }
$chosen.Add([pscustomobject]@{
Path = $pkg.Path
DownloadUrl = $pkg.DownloadUrl
DriverFileName = $pkg.FileName
Name = $pkg.Name
Category = $pkg.Category
VendorVersion = $pkg.VendorVersion
DateTime = $pkg.DateTime
ComponentIds = $pkg.ComponentIds
})
}
if ($chosen.Count -eq 0) {
WriteLog "Get-DellLatestDriverPackages: No qualifying driver packages after supersedence."
return @()
}
WriteLog ("Get-DellLatestDriverPackages: Selected {0} package(s) after supersedence." -f $chosen.Count)
return $chosen
}
# Resolve a Dell permodel CabUrl when missing by inspecting CatalogIndexPC
function Resolve-DellCabUrlFromModel {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$DriversFolder,
[Parameter()][string]$ModelDisplay,
[Parameter()][string]$SystemId
)
if ([string]::IsNullOrWhiteSpace($SystemId) -and -not [string]::IsNullOrWhiteSpace($ModelDisplay)) {
# Try to parse the trailing (XXXX) token (SystemId)
if ($ModelDisplay -match '\(([0-9A-Fa-f]{4})\)\s*$') {
$SystemId = $matches[1].ToUpperInvariant()
}
}
if ([string]::IsNullOrWhiteSpace($SystemId)) {
WriteLog "Resolve-DellCabUrlFromModel: No SystemId could be determined from '$ModelDisplay'."
return $null
}
try {
$indexXml = Get-DellCatalogIndex -DriversFolder $DriversFolder
# Reuse existing model parsing to avoid duplicating streaming logic
$allModels = Get-DellClientModels -CatalogIndexXmlPath $indexXml
$match = $allModels | Where-Object { $_.SystemId -eq $SystemId } | Select-Object -First 1
if ($null -eq $match) {
WriteLog "Resolve-DellCabUrlFromModel: SystemId '$SystemId' not found in CatalogIndexPC.xml."
return $null
}
WriteLog "Resolve-DellCabUrlFromModel: Resolved CabUrl for '$($match.ModelDisplay)' -> $($match.CabUrl)"
return [pscustomobject]@{
Brand = $match.Brand
ModelNumber = $match.ModelNumber
SystemId = $match.SystemId
CabRelativePath = $match.CabRelativePath
CabUrl = $match.CabUrl
ModelDisplay = $match.ModelDisplay
}
}
catch {
WriteLog "Resolve-DellCabUrlFromModel: Failure resolving CabUrl for '$ModelDisplay' / SystemId '$SystemId' : $($_.Exception.Message)"
return $null
}
}
Export-ModuleMember -Function Convert-DellVendorVersion,Compare-DellVendorVersion,Get-DellCatalogIndex,Get-DellClientModels,Get-DellLatestDriverPackages,Resolve-DellCabUrlFromModel