mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
Merge pull request #394 from rbalsleyMSFT/SurfaceMapping
Surface mapping
This commit is contained in:
@@ -0,0 +1,591 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Common Microsoft/Surface driver helpers (cache index, SKU mapping).
|
||||||
|
.DESCRIPTION
|
||||||
|
This module contains Microsoft/Surface-specific functions used by the UI and scripts
|
||||||
|
to map Surface driver packs to System SKU values using:
|
||||||
|
- Source A: Surface System SKU reference (Learn)
|
||||||
|
- Source B: Support page model list
|
||||||
|
- Source C: Download Center details (window.__DLCDetails__)
|
||||||
|
#>
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# SECTION: Microsoft Surface Driver Index Cache (Sources A/B/C)
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function Get-SurfaceDriverIndexCachePath {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$DriversFolder
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store the cache under Drivers\Microsoft so it travels with the driver content
|
||||||
|
$microsoftDriversFolder = Join-Path -Path $DriversFolder -ChildPath 'Microsoft'
|
||||||
|
if (-not (Test-Path -Path $microsoftDriversFolder -PathType Container)) {
|
||||||
|
New-Item -Path $microsoftDriversFolder -ItemType Directory -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Join-Path -Path $microsoftDriversFolder -ChildPath 'SurfaceDriverIndex.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Import-SurfaceDriverIndexCache {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$DriversFolder
|
||||||
|
)
|
||||||
|
|
||||||
|
$cachePath = Get-SurfaceDriverIndexCachePath -DriversFolder $DriversFolder
|
||||||
|
|
||||||
|
# Surface cache TTL (7 days): treat stale caches as missing so we re-download Sources A/B/C as needed.
|
||||||
|
$cacheTtlDays = 7
|
||||||
|
if (-not (Test-Path -Path $cachePath -PathType Leaf)) {
|
||||||
|
return [pscustomobject]@{
|
||||||
|
ModelIndex = @()
|
||||||
|
SkuIndex = @()
|
||||||
|
DownloadCenterDetails = @()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$cacheAgeDays = ((Get-Date) - (Get-Item -Path $cachePath -ErrorAction Stop).LastWriteTime).TotalDays
|
||||||
|
if ($cacheAgeDays -ge $cacheTtlDays) {
|
||||||
|
WriteLog "Surface cache: Cache file '$cachePath' is older than $cacheTtlDays days ($([math]::Round($cacheAgeDays, 1)) days). Refreshing."
|
||||||
|
return [pscustomobject]@{
|
||||||
|
ModelIndex = @()
|
||||||
|
SkuIndex = @()
|
||||||
|
DownloadCenterDetails = @()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteLog "Surface cache: Loading cached SurfaceDriverIndex.json from '$cachePath' (age: $([math]::Round($cacheAgeDays, 1)) days)."
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Surface cache: Failed to read cache timestamp for '$cachePath'. Refreshing. Error: $($_.Exception.Message)"
|
||||||
|
return [pscustomobject]@{
|
||||||
|
ModelIndex = @()
|
||||||
|
SkuIndex = @()
|
||||||
|
DownloadCenterDetails = @()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$cache = Get-Content -Path $cachePath -Raw | ConvertFrom-Json -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Warning: Could not read Surface driver cache '$cachePath'. Creating a new cache. Error: $($_.Exception.Message)"
|
||||||
|
return [pscustomobject]@{
|
||||||
|
ModelIndex = @()
|
||||||
|
SkuIndex = @()
|
||||||
|
DownloadCenterDetails = @()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $cache) {
|
||||||
|
return [pscustomobject]@{
|
||||||
|
ModelIndex = @()
|
||||||
|
SkuIndex = @()
|
||||||
|
DownloadCenterDetails = @()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure expected properties exist (backward compatible with earlier cache shapes)
|
||||||
|
if (-not $cache.PSObject.Properties['ModelIndex']) {
|
||||||
|
$cache | Add-Member -NotePropertyName ModelIndex -NotePropertyValue @()
|
||||||
|
}
|
||||||
|
if (-not $cache.PSObject.Properties['SkuIndex']) {
|
||||||
|
$cache | Add-Member -NotePropertyName SkuIndex -NotePropertyValue @()
|
||||||
|
}
|
||||||
|
if (-not $cache.PSObject.Properties['DownloadCenterDetails']) {
|
||||||
|
$cache | Add-Member -NotePropertyName DownloadCenterDetails -NotePropertyValue @()
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cache
|
||||||
|
}
|
||||||
|
|
||||||
|
function Save-SurfaceDriverIndexCache {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$Cache,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$DriversFolder
|
||||||
|
)
|
||||||
|
|
||||||
|
$cachePath = Get-SurfaceDriverIndexCachePath -DriversFolder $DriversFolder
|
||||||
|
$Cache | ConvertTo-Json -Depth 10 | Set-Content -Path $cachePath -Encoding UTF8
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-SurfaceComparableName {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Text
|
||||||
|
)
|
||||||
|
|
||||||
|
# Normalize Surface marketing strings into a comparable family key.
|
||||||
|
# This intentionally strips consumer/commercial/processor qualifiers so we can join Sources A/B/C.
|
||||||
|
$value = [System.Net.WebUtility]::HtmlDecode($Text)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $value.Trim()
|
||||||
|
$value = $value -replace '\(', ' '
|
||||||
|
$value = $value -replace '\)', ' '
|
||||||
|
$value = $value -replace ',', ' '
|
||||||
|
|
||||||
|
# Normalize punctuation that frequently differs between Support/Learn pages
|
||||||
|
# (e.g. Wi‑Fi unicode hyphen, AT&T, Y!mobile)
|
||||||
|
$value = $value -replace '[-\u2010\u2011\u2012\u2013\u2014\u2212]', ' '
|
||||||
|
$value = $value -replace '&', ' '
|
||||||
|
$value = $value -replace '!', ' '
|
||||||
|
$value = $value -replace '™', ' '
|
||||||
|
|
||||||
|
$value = $value -replace '(?i)\bMicrosoft\b', ''
|
||||||
|
$value = $value -replace '(?i)\bfor\s+Business\b', ''
|
||||||
|
$value = $value -replace '(?i)\bConsumer\b', ''
|
||||||
|
$value = $value -replace '(?i)\bCommercial\b', ''
|
||||||
|
|
||||||
|
# Strip processor/connection qualifiers that cause mismatches between WMI, Learn, and Support naming.
|
||||||
|
$value = $value -replace '(?i)\bwith\s+Intel\b', ''
|
||||||
|
$value = $value -replace '(?i)\bIntel\s+processor\b', ''
|
||||||
|
$value = $value -replace '(?i)\bIntel\b', ''
|
||||||
|
$value = $value -replace '(?i)\bSnapdragon\s+processor\b', ''
|
||||||
|
$value = $value -replace '(?i)\bSnapdragon\b', ''
|
||||||
|
$value = $value -replace '(?i)\bwith\s+5G\b', ''
|
||||||
|
$value = $value -replace '(?i)\bLTE\b', ''
|
||||||
|
$value = $value -replace '(?i)\b4G\b', ''
|
||||||
|
$value = $value -replace '(?i)\bprocessor\b', ''
|
||||||
|
|
||||||
|
# Cleanup: remove orphaned "with" left behind by earlier removals (e.g., "Surface Pro 9 with Intel Processor")
|
||||||
|
$value = $value -replace '(?i)\bwith\b', ''
|
||||||
|
$value = $value -replace '\s+', ' '
|
||||||
|
|
||||||
|
return $value.Trim().ToUpperInvariant()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SurfaceSystemSkuReferenceIndex {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$DriversFolder
|
||||||
|
)
|
||||||
|
|
||||||
|
# Source A: Learn page with authoritative Device / System Model / System SKU table
|
||||||
|
$cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder
|
||||||
|
if ($cache.SkuIndex -and $cache.SkuIndex.Count -gt 0) {
|
||||||
|
return @($cache.SkuIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = 'https://learn.microsoft.com/en-us/surface/surface-system-sku-reference'
|
||||||
|
WriteLog "Surface cache: Downloading System SKU reference table from $url"
|
||||||
|
|
||||||
|
$headers = @{ 'User-Agent' = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' }
|
||||||
|
$webContent = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $headers
|
||||||
|
$html = $webContent.Content
|
||||||
|
|
||||||
|
$skuRows = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||||
|
|
||||||
|
$rowMatches = [regex]::Matches($html, '<tr[^>]*>(.*?)</tr>', [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
||||||
|
foreach ($rowMatch in $rowMatches) {
|
||||||
|
$rowContent = $rowMatch.Groups[1].Value
|
||||||
|
$cellMatches = [regex]::Matches($rowContent, '<td[^>]*>\s*(?:<p[^>]*>)?(.*?)(?:</p>)?\s*</td>', [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
||||||
|
if ($cellMatches.Count -lt 3) { continue }
|
||||||
|
|
||||||
|
$device = ([System.Net.WebUtility]::HtmlDecode(($cellMatches[0].Groups[1].Value).Trim()))
|
||||||
|
$systemModel = ([System.Net.WebUtility]::HtmlDecode(($cellMatches[1].Groups[1].Value).Trim()))
|
||||||
|
$systemSkuRaw = ([System.Net.WebUtility]::HtmlDecode(($cellMatches[2].Groups[1].Value).Trim()))
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($device) -or [string]::IsNullOrWhiteSpace($systemSkuRaw)) { continue }
|
||||||
|
|
||||||
|
$skuList = @($systemSkuRaw)
|
||||||
|
|
||||||
|
foreach ($sku in $skuList) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($sku)) { continue }
|
||||||
|
$skuRows.Add([pscustomobject]@{
|
||||||
|
Device = $device
|
||||||
|
SystemModel = $systemModel
|
||||||
|
SystemSku = $sku.Trim().ToUpperInvariant()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cache.SkuIndex = @($skuRows)
|
||||||
|
Save-SurfaceDriverIndexCache -Cache $cache -DriversFolder $DriversFolder
|
||||||
|
WriteLog "Surface cache: Stored $($skuRows.Count) SKU entries."
|
||||||
|
|
||||||
|
return @($skuRows)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SurfaceDownloadCenterDetails {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$DriversFolder,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ModelLink,
|
||||||
|
[Parameter()]
|
||||||
|
[string]$ModelName = $null
|
||||||
|
)
|
||||||
|
|
||||||
|
# Source C: Download Center details page (window.__DLCDetails__) containing file names + direct URLs
|
||||||
|
$cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder
|
||||||
|
$existing = @($cache.DownloadCenterDetails | Where-Object { $_.Link -eq $ModelLink } | Select-Object -First 1)
|
||||||
|
if ($existing.Count -gt 0 -and $existing[0].Files -and $existing[0].Files.Count -gt 0) {
|
||||||
|
# Backfill Model into cache when available
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($ModelName)) {
|
||||||
|
if (-not $existing[0].PSObject.Properties['Model'] -or [string]::IsNullOrWhiteSpace($existing[0].Model)) {
|
||||||
|
try {
|
||||||
|
$existing[0] | Add-Member -NotePropertyName Model -NotePropertyValue $ModelName -Force
|
||||||
|
|
||||||
|
$newDetails = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||||
|
foreach ($item in @($cache.DownloadCenterDetails)) {
|
||||||
|
if ($null -ne $item -and $item.PSObject.Properties['Link'] -and $item.Link -ne $ModelLink) {
|
||||||
|
$newDetails.Add($item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$newDetails.Add($existing[0])
|
||||||
|
$cache.DownloadCenterDetails = @($newDetails)
|
||||||
|
Save-SurfaceDriverIndexCache -Cache $cache -DriversFolder $DriversFolder
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Surface cache: Failed to backfill Model for DownloadCenterDetails entry '$ModelLink'. Error: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($existing[0].Files)
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteLog "Surface cache: Downloading Download Center details from $ModelLink"
|
||||||
|
$headers = @{ 'User-Agent' = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' }
|
||||||
|
$downloadPageContent = Invoke-WebRequest -Uri $ModelLink -UseBasicParsing -Headers $headers
|
||||||
|
|
||||||
|
$scriptPattern = '<script>window.__DLCDetails__={(.*?)}<\/script>'
|
||||||
|
$scriptMatch = [regex]::Match($downloadPageContent.Content, $scriptPattern)
|
||||||
|
if (-not $scriptMatch.Success) {
|
||||||
|
WriteLog "Surface cache: Could not find window.__DLCDetails__ on $ModelLink"
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
$scriptContent = $scriptMatch.Groups[1].Value
|
||||||
|
$downloadFilePattern = '"name":"([^"]+\.(?:msi|zip))",[^}]*?"url":"(.*?)"'
|
||||||
|
$downloadFileMatches = [regex]::Matches($scriptContent, $downloadFilePattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
||||||
|
|
||||||
|
$files = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||||
|
foreach ($downloadFile in $downloadFileMatches) {
|
||||||
|
$currentFileName = $downloadFile.Groups[1].Value
|
||||||
|
$fileUrl = $downloadFile.Groups[2].Value
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentFileName) -or [string]::IsNullOrWhiteSpace($fileUrl)) { continue }
|
||||||
|
|
||||||
|
$files.Add([pscustomobject]@{
|
||||||
|
Name = $currentFileName
|
||||||
|
Url = $fileUrl
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
# Persist into cache
|
||||||
|
if ($files.Count -gt 0) {
|
||||||
|
$detailsEntry = [pscustomobject][ordered]@{
|
||||||
|
Model = $ModelName
|
||||||
|
Link = $ModelLink
|
||||||
|
Files = @($files)
|
||||||
|
}
|
||||||
|
|
||||||
|
$newDetails = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||||
|
foreach ($item in @($cache.DownloadCenterDetails)) {
|
||||||
|
if ($null -ne $item -and $item.PSObject.Properties['Link'] -and $item.Link -ne $ModelLink) {
|
||||||
|
$newDetails.Add($item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$newDetails.Add($detailsEntry)
|
||||||
|
$cache.DownloadCenterDetails = @($newDetails)
|
||||||
|
Save-SurfaceDriverIndexCache -Cache $cache -DriversFolder $DriversFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($files)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SurfaceSystemSkuListForMicrosoftDriver {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$DriversFolder,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ModelName,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ModelLink
|
||||||
|
)
|
||||||
|
|
||||||
|
$skuIndex = Get-SurfaceSystemSkuReferenceIndex -DriversFolder $DriversFolder
|
||||||
|
if ($null -eq $skuIndex -or $skuIndex.Count -eq 0) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = Get-SurfaceDownloadCenterDetails -DriversFolder $DriversFolder -ModelLink $ModelLink -ModelName $ModelName
|
||||||
|
$fileNames = @($files | ForEach-Object { $_.Name })
|
||||||
|
|
||||||
|
# Infer architecture hints from the MSI naming convention (best-effort)
|
||||||
|
$archHint = $null
|
||||||
|
if ($fileNames -match '(?i)_ARM_') {
|
||||||
|
$archHint = 'ARM64'
|
||||||
|
}
|
||||||
|
elseif ($fileNames -match '(?i)withIntel|_Intel_|Intel') {
|
||||||
|
$archHint = 'x64'
|
||||||
|
}
|
||||||
|
elseif ($ModelName -match '(?i)\bSQ3\b|\bSnapdragon\b') {
|
||||||
|
$archHint = 'ARM64'
|
||||||
|
}
|
||||||
|
elseif ($ModelName -match '(?i)with Intel') {
|
||||||
|
$archHint = 'x64'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Surface Pro (generic) is ambiguous in the SKU table because Surface Pro (5th Gen) and
|
||||||
|
# Surface Pro with LTE Advanced (5th Gen) both reuse SystemModel="Surface Pro".
|
||||||
|
# The "Surface Pro" driver pack does not have a unique SystemSKU value on the Learn page.
|
||||||
|
if ($ModelName.Trim() -match '(?i)^Surface\s+Pro$') {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build multiple candidate keys for models that contain multiple variants in one string
|
||||||
|
# Example: "Surface Pro 7+ and Surface Pro 7+ LTE"
|
||||||
|
$familyKeyCandidates = [System.Collections.Generic.List[string]]::new()
|
||||||
|
$familyKeySet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
|
||||||
|
$primaryKey = ConvertTo-SurfaceComparableName -Text $ModelName
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($primaryKey) -and $familyKeySet.Add($primaryKey)) {
|
||||||
|
$familyKeyCandidates.Add($primaryKey) | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = [regex]::Split($ModelName, '(?i)\s+and\s+')
|
||||||
|
|
||||||
|
# Track when the model text contains both LTE and non-LTE variants (e.g. "Surface Go 2 and Surface Go 2 LTE")
|
||||||
|
$hasLtePart = (@($parts | Where-Object { $_ -match '(?i)\bLTE\b' }).Count -gt 0)
|
||||||
|
$hasNonLtePart = (@($parts | Where-Object { $_ -notmatch '(?i)\bLTE\b' }).Count -gt 0)
|
||||||
|
|
||||||
|
foreach ($part in @($parts)) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($part)) { continue }
|
||||||
|
$candidate = ConvertTo-SurfaceComparableName -Text $part
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($candidate) -and $familyKeySet.Add($candidate)) {
|
||||||
|
$familyKeyCandidates.Add($candidate) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($familyKeyCandidates.Count -eq 0) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Surface 3 has multiple carrier/region variants that share the same SystemModel ("Surface 3").
|
||||||
|
# Add a base key so we can match all Surface 3 SKU rows, then refine down to the correct variant.
|
||||||
|
if ($ModelName -match '(?i)^Surface\s+3\b') {
|
||||||
|
$surface3BaseKey = 'SURFACE 3'
|
||||||
|
if ($familyKeySet.Add($surface3BaseKey)) {
|
||||||
|
$familyKeyCandidates.Add($surface3BaseKey) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Surface Go variants share the same SystemModel ("Surface Go") in the SKU table.
|
||||||
|
# Use a generation-aware base key so we don't cross-match Go vs Go 2/3/4 SKU rows.
|
||||||
|
if ($ModelName -match '(?i)^Surface\s+Go\s+2\b') {
|
||||||
|
$surfaceGoBaseKey = 'SURFACE GO 2'
|
||||||
|
if ($familyKeySet.Add($surfaceGoBaseKey)) {
|
||||||
|
$familyKeyCandidates.Add($surfaceGoBaseKey) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($ModelName -match '(?i)^Surface\s+Go\s+3\b') {
|
||||||
|
$surfaceGoBaseKey = 'SURFACE GO 3'
|
||||||
|
if ($familyKeySet.Add($surfaceGoBaseKey)) {
|
||||||
|
$familyKeyCandidates.Add($surfaceGoBaseKey) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($ModelName -match '(?i)^Surface\s+Go\s+4\b') {
|
||||||
|
$surfaceGoBaseKey = 'SURFACE GO 4'
|
||||||
|
if ($familyKeySet.Add($surfaceGoBaseKey)) {
|
||||||
|
$familyKeyCandidates.Add($surfaceGoBaseKey) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($ModelName -match '(?i)^Surface\s+Go\b') {
|
||||||
|
$surfaceGoBaseKey = 'SURFACE GO'
|
||||||
|
if ($familyKeySet.Add($surfaceGoBaseKey)) {
|
||||||
|
$familyKeyCandidates.Add($surfaceGoBaseKey) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Surface Pro 9 with 5G: the SKU table rows use SystemModel "Surface Pro 9".
|
||||||
|
# Add a base key so we can match the Pro 9 SKU rows, then refine down to the 5G rows.
|
||||||
|
if (($ModelName -match '(?i)^Surface\s+Pro\s+9\b') -and ($ModelName -match '(?i)\b5G\b')) {
|
||||||
|
$surfacePro9BaseKey = 'SURFACE PRO 9'
|
||||||
|
if ($familyKeySet.Add($surfacePro9BaseKey)) {
|
||||||
|
$familyKeyCandidates.Add($surfacePro9BaseKey) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Surface Pro with LTE Advanced maps to the "Surface Pro with LTE Advanced (5th Gen)" SKU table row.
|
||||||
|
# Add a base key so we can match Surface Pro rows, then refine to the LTE Advanced SKU.
|
||||||
|
if ($ModelName -match '(?i)^Surface\s+Pro\s+with\s+LTE\s+Advanced\b') {
|
||||||
|
$surfaceProBaseKey = 'SURFACE PRO'
|
||||||
|
if ($familyKeySet.Add($surfaceProBaseKey)) {
|
||||||
|
$familyKeyCandidates.Add($surfaceProBaseKey) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Surface Laptop (1st Gen) maps to the base "Surface Laptop" SKU table row.
|
||||||
|
if (($ModelName -match '(?i)^Surface\s+Laptop\b') -and ($ModelName -match '(?i)\bGen\b')) {
|
||||||
|
$surfaceLaptopBaseKey = 'SURFACE LAPTOP'
|
||||||
|
if ($familyKeySet.Add($surfaceLaptopBaseKey)) {
|
||||||
|
$familyKeyCandidates.Add($surfaceLaptopBaseKey) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Surface Studio (1st Gen) maps to the base "Surface Studio" SKU table row.
|
||||||
|
if (($ModelName -match '(?i)^Surface\s+Studio\b') -and ($ModelName -match '(?i)\bGen\b')) {
|
||||||
|
$surfaceStudioBaseKey = 'SURFACE STUDIO'
|
||||||
|
if ($familyKeySet.Add($surfaceStudioBaseKey)) {
|
||||||
|
$familyKeyCandidates.Add($surfaceStudioBaseKey) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Surface Laptop 3/4 AMD/Intel packs map to the "Surface Laptop 3/4" SystemModel rows in the SKU table.
|
||||||
|
if ($ModelName -match '(?i)^Surface\s+Laptop\s+(3|4)\b' -and $ModelName -match '(?i)\b(AMD|Intel)\b') {
|
||||||
|
$generationMatch = [regex]::Match($ModelName, '(?i)^Surface\s+Laptop\s+(3|4)\b')
|
||||||
|
if ($generationMatch.Success) {
|
||||||
|
$surfaceLaptopGenBaseKey = "SURFACE LAPTOP $($generationMatch.Groups[1].Value)"
|
||||||
|
if ($familyKeySet.Add($surfaceLaptopGenBaseKey)) {
|
||||||
|
$familyKeyCandidates.Add($surfaceLaptopGenBaseKey) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Match by any candidate key against the SKU table
|
||||||
|
$skuMatches = @($skuIndex | Where-Object {
|
||||||
|
$deviceKey = ConvertTo-SurfaceComparableName -Text $_.Device
|
||||||
|
$modelKey = ConvertTo-SurfaceComparableName -Text $_.SystemModel
|
||||||
|
|
||||||
|
foreach ($candidateKey in $familyKeyCandidates) {
|
||||||
|
if ($deviceKey -eq $candidateKey -or $modelKey -eq $candidateKey) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $false
|
||||||
|
})
|
||||||
|
|
||||||
|
# Surface Hub 2 driver packs cover Surface Hub 2S + Surface Hub 3 devices.
|
||||||
|
# The System SKU table does not have a "Surface Hub 2" row, so map Hub 2 to all Hub SKUs.
|
||||||
|
if ($ModelName -match '(?i)^Surface\s+Hub\s+2\b') {
|
||||||
|
$hubSkuRows = @($skuIndex | Where-Object { $_.Device -match '(?i)^Surface\s+Hub' })
|
||||||
|
if ($hubSkuRows.Count -gt 0) {
|
||||||
|
$skuMatches = @($hubSkuRows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Surface 3: refine down to the correct SKU row based on the model variant text
|
||||||
|
# Use normalized text so punctuation/Unicode differences don't drop matches to zero.
|
||||||
|
if ($ModelName -match '(?i)^Surface\s+3\b') {
|
||||||
|
$modelNorm = ConvertTo-SurfaceComparableName -Text $ModelName
|
||||||
|
|
||||||
|
if ($modelNorm -match '(?i)\bWI\s+FI\b') {
|
||||||
|
$skuMatches = @($skuMatches | Where-Object { (ConvertTo-SurfaceComparableName -Text $_.Device) -match '(?i)\bWI\s+FI\b' })
|
||||||
|
}
|
||||||
|
elseif ($modelNorm -match '(?i)\bVERIZON\b') {
|
||||||
|
$skuMatches = @($skuMatches | Where-Object { (ConvertTo-SurfaceComparableName -Text $_.Device) -match '(?i)\bVERIZON\b' })
|
||||||
|
}
|
||||||
|
elseif ($modelNorm -match '(?i)\bOUTSIDE\s+OF\s+NORTH\s+AMERICA\b|\bY\s+MOBILE\b') {
|
||||||
|
$skuMatches = @($skuMatches | Where-Object { (ConvertTo-SurfaceComparableName -Text $_.Device) -match '(?i)\bOUTSIDE\s+OF\s+NORTH\s+AMERICA\b|\bY\s+MOBILE\b' })
|
||||||
|
}
|
||||||
|
elseif ($modelNorm -match '(?i)\bNORTH\s+AMERICA\b') {
|
||||||
|
# "North America (non-AT&T)" should map to the North America row (not AT&T/Verizon/outside-of-North-America)
|
||||||
|
$skuMatches = @($skuMatches | Where-Object {
|
||||||
|
$deviceNorm = ConvertTo-SurfaceComparableName -Text $_.Device
|
||||||
|
($deviceNorm -match '(?i)\bNORTH\s+AMERICA\b') -and
|
||||||
|
($deviceNorm -notmatch '(?i)\bOUTSIDE\b|\bY\s+MOBILE\b') -and
|
||||||
|
($deviceNorm -notmatch '(?i)\bAT\s+T\b|\bVERIZON\b')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
elseif (($modelNorm -match '(?i)\bAT\s+T\b') -and ($modelNorm -notmatch '(?i)\bNON\s+AT\s+T\b')) {
|
||||||
|
$skuMatches = @($skuMatches | Where-Object { (ConvertTo-SurfaceComparableName -Text $_.Device) -match '(?i)\bAT\s+T\b' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Surface Go: keep LTE SKU only for LTE-only models; exclude LTE SKU for non-LTE-only models.
|
||||||
|
# If the model name includes BOTH LTE and non-LTE variants (joined with "and"), do not filter.
|
||||||
|
# Surface Go 3 driver packs are treated as covering LTE + non-LTE unless explicitly labeled otherwise.
|
||||||
|
if ($ModelName -match '(?i)^Surface\s+Go\b') {
|
||||||
|
$isSurfaceGo3Base = ($ModelName -match '(?i)^Surface\s+Go\s+3\b') -and ($ModelName -notmatch '(?i)\bLTE\b')
|
||||||
|
|
||||||
|
if (-not $isSurfaceGo3Base) {
|
||||||
|
if (-not ($hasLtePart -and $hasNonLtePart)) {
|
||||||
|
if ($ModelName -match '(?i)\bLTE\b') {
|
||||||
|
$skuMatches = @($skuMatches | Where-Object { $_.Device -match '(?i)\bLTE\b' })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$skuMatches = @($skuMatches | Where-Object { $_.Device -notmatch '(?i)\bLTE\b' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Surface Pro 9 with 5G (SQ3): keep only the 5G SKU rows (U.S. + outside of U.S.).
|
||||||
|
if (($ModelName -match '(?i)^Surface\s+Pro\s+9\b') -and ($ModelName -match '(?i)\b5G\b')) {
|
||||||
|
$skuMatches = @($skuMatches | Where-Object { $_.Device -match '(?i)\b5G\b' })
|
||||||
|
}
|
||||||
|
|
||||||
|
# Surface Pro 10: split non-5G vs 5G SKU rows so the two driver packs don't share the same SystemSKUs.
|
||||||
|
if ($ModelName -match '(?i)^Surface\s+Pro\s+10\b') {
|
||||||
|
if ($ModelName -match '(?i)\b5G\b') {
|
||||||
|
$skuMatches = @($skuMatches | Where-Object {
|
||||||
|
($_.SystemSku -match '^SURFACE_PRO_10_WITH_5G_FOR_BUSINESS_') -or
|
||||||
|
($_.Device -match '(?i)\bwith\s+5G\b')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$skuMatches = @($skuMatches | Where-Object { $_.SystemSku -eq 'SURFACE_PRO_10_FOR_BUSINESS_2079' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Surface Pro with LTE Advanced: restrict to the LTE Advanced (5th Gen) SKU.
|
||||||
|
if ($ModelName -match '(?i)^Surface\s+Pro\s+with\s+LTE\s+Advanced\b') {
|
||||||
|
$skuMatches = @($skuMatches | Where-Object { $_.SystemSku -eq 'SURFACE_PRO_1807' })
|
||||||
|
}
|
||||||
|
|
||||||
|
# Surface Laptop 3/4: filter to AMD vs Intel rows (prevents AMD packs from inheriting Intel SKUs and vice-versa).
|
||||||
|
if ($ModelName -match '(?i)^Surface\s+Laptop\s+(3|4)\b') {
|
||||||
|
if ($ModelName -match '(?i)\bAMD\b') {
|
||||||
|
$skuMatches = @($skuMatches | Where-Object { $_.Device -match '(?i)\bAMD\b' })
|
||||||
|
}
|
||||||
|
elseif ($ModelName -match '(?i)\bIntel\b') {
|
||||||
|
$skuMatches = @($skuMatches | Where-Object { $_.Device -match '(?i)\bIntel\b' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Apply architecture filtering when we can infer it
|
||||||
|
if ($archHint -eq 'ARM64') {
|
||||||
|
# ARM variants are typically called out as Snapdragon / SQ3 / 5G in the Learn table
|
||||||
|
$skuMatches = @($skuMatches | Where-Object {
|
||||||
|
($_.Device -match '(?i)Snapdragon|SQ3|with 5G') -or
|
||||||
|
($_.SystemModel -match '(?i)Snapdragon|SQ3|with 5G')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
elseif ($archHint -eq 'x64') {
|
||||||
|
# x64 variants are often NOT labeled "Intel" in the Learn table (e.g. Surface Pro 9).
|
||||||
|
# Treat "not Snapdragon/SQ3/5G" as the x64 bucket.
|
||||||
|
$skuMatches = @($skuMatches | Where-Object {
|
||||||
|
($_.Device -notmatch '(?i)Snapdragon|SQ3|with 5G') -and
|
||||||
|
($_.SystemModel -notmatch '(?i)Snapdragon|SQ3|with 5G')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$skus = @($skuMatches | ForEach-Object { $_.SystemSku } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique)
|
||||||
|
return $skus
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function `
|
||||||
|
Get-SurfaceDriverIndexCachePath, `
|
||||||
|
Import-SurfaceDriverIndexCache, `
|
||||||
|
Save-SurfaceDriverIndexCache, `
|
||||||
|
ConvertTo-SurfaceComparableName, `
|
||||||
|
Get-SurfaceSystemSkuReferenceIndex, `
|
||||||
|
Get-SurfaceDownloadCenterDetails, `
|
||||||
|
Get-SurfaceSystemSkuListForMicrosoftDriver
|
||||||
@@ -276,6 +276,20 @@ function Update-DriverMappingJson {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Microsoft Surface: resolve System SKU list (best-effort) using Sources A + C and cached results
|
||||||
|
$surfaceSystemSkuList = @()
|
||||||
|
if ($driver.Make -eq 'Microsoft') {
|
||||||
|
if ($driver.PSObject.Properties['Link'] -and -not [string]::IsNullOrWhiteSpace($driver.Link)) {
|
||||||
|
try {
|
||||||
|
$surfaceSystemSkuList = Get-SurfaceSystemSkuListForMicrosoftDriver -DriversFolder $DriversFolder -ModelName $driver.Model -ModelLink $driver.Link
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Warning: Failed to resolve Surface SystemSku list for '$($driver.Model)'. Error: $($_.Exception.Message)"
|
||||||
|
$surfaceSystemSkuList = @()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$existingEntry = $mappingList | Where-Object { $_.Manufacturer -eq $driver.Make -and $_.Model -eq $driver.Model } | Select-Object -First 1
|
$existingEntry = $mappingList | Where-Object { $_.Manufacturer -eq $driver.Make -and $_.Model -eq $driver.Model } | Select-Object -First 1
|
||||||
|
|
||||||
if ($null -ne $existingEntry) {
|
if ($null -ne $existingEntry) {
|
||||||
@@ -316,6 +330,26 @@ function Update-DriverMappingJson {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($driver.Make -eq 'Microsoft' -and $surfaceSystemSkuList -and $surfaceSystemSkuList.Count -gt 0) {
|
||||||
|
$desiredSkus = @($surfaceSystemSkuList | Sort-Object -Unique)
|
||||||
|
if ($existingEntry.PSObject.Properties['SystemSku']) {
|
||||||
|
$currentSkus = @($existingEntry.SystemSku)
|
||||||
|
$currentNormalized = @($currentSkus | ForEach-Object { if ($null -ne $_) { $_.ToString().Trim().ToUpperInvariant() } }) | Sort-Object -Unique
|
||||||
|
$desiredNormalized = @($desiredSkus | ForEach-Object { if ($null -ne $_) { $_.ToString().Trim().ToUpperInvariant() } }) | Sort-Object -Unique
|
||||||
|
|
||||||
|
if (($currentNormalized -join '|') -ne ($desiredNormalized -join '|')) {
|
||||||
|
WriteLog "Updating SystemSku list for 'Microsoft - $($driver.Model)'."
|
||||||
|
$existingEntry.SystemSku = $desiredSkus
|
||||||
|
$entryUpdated = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "Adding SystemSku list for 'Microsoft - $($driver.Model)'."
|
||||||
|
$existingEntry | Add-Member -NotePropertyName SystemSku -NotePropertyValue $desiredSkus
|
||||||
|
$entryUpdated = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($entryUpdated) {
|
if ($entryUpdated) {
|
||||||
$updatedCount++
|
$updatedCount++
|
||||||
}
|
}
|
||||||
@@ -333,6 +367,9 @@ function Update-DriverMappingJson {
|
|||||||
if ($driver.Make -eq 'Lenovo' -and -not [string]::IsNullOrWhiteSpace($machineTypeValue)) {
|
if ($driver.Make -eq 'Lenovo' -and -not [string]::IsNullOrWhiteSpace($machineTypeValue)) {
|
||||||
$newEntry | Add-Member -NotePropertyName MachineType -NotePropertyValue $machineTypeValue
|
$newEntry | Add-Member -NotePropertyName MachineType -NotePropertyValue $machineTypeValue
|
||||||
}
|
}
|
||||||
|
if ($driver.Make -eq 'Microsoft' -and $surfaceSystemSkuList -and $surfaceSystemSkuList.Count -gt 0) {
|
||||||
|
$newEntry | Add-Member -NotePropertyName SystemSku -NotePropertyValue @($surfaceSystemSkuList | Sort-Object -Unique)
|
||||||
|
}
|
||||||
|
|
||||||
$mappingList.Add($newEntry)
|
$mappingList.Add($newEntry)
|
||||||
WriteLog "Adding new mapping for '$($driver.Make) - $($driver.Model)' with path '$($driver.DriverPath)'."
|
WriteLog "Adding new mapping for '$($driver.Make) - $($driver.Model)' with path '$($driver.DriverPath)'."
|
||||||
@@ -778,4 +815,8 @@ function Get-LenovoPSREFToken {
|
|||||||
# SECTION: Module Export
|
# SECTION: Module Export
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
Export-ModuleMember -Function Compress-DriverFolderToWim, Update-DriverMappingJson, Test-ExistingDriver, Get-LenovoPSREFToken
|
Export-ModuleMember -Function `
|
||||||
|
Compress-DriverFolderToWim, `
|
||||||
|
Update-DriverMappingJson, `
|
||||||
|
Test-ExistingDriver, `
|
||||||
|
Get-LenovoPSREFToken
|
||||||
@@ -67,6 +67,7 @@ Description = 'Common functions shared between FFU Builder UI and the BuildFFUVM
|
|||||||
|
|
||||||
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
|
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
|
||||||
NestedModules = @('FFU.Common.Drivers.psm1',
|
NestedModules = @('FFU.Common.Drivers.psm1',
|
||||||
|
'FFU.Common.Drivers.Microsoft.psm1',
|
||||||
'FFU.Common.Drivers.Dell.psm1',
|
'FFU.Common.Drivers.Dell.psm1',
|
||||||
'FFU.Common.Winget.psm1',
|
'FFU.Common.Winget.psm1',
|
||||||
'FFU.Common.Parallel.psm1',
|
'FFU.Common.Parallel.psm1',
|
||||||
|
|||||||
@@ -10,12 +10,33 @@ function Get-MicrosoftDriversModelList {
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[hashtable]$Headers, # Pass necessary headers
|
[hashtable]$Headers, # Pass necessary headers
|
||||||
[string]$UserAgent # Pass UserAgent
|
[string]$UserAgent, # Pass UserAgent
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$DriversFolder
|
||||||
)
|
)
|
||||||
|
|
||||||
$url = "https://support.microsoft.com/en-us/surface/download-drivers-and-firmware-for-surface-09bb2e09-2a4b-cb69-0951-078a7739e120"
|
$url = "https://support.microsoft.com/en-us/surface/download-drivers-and-firmware-for-surface-09bb2e09-2a4b-cb69-0951-078a7739e120"
|
||||||
$models = @()
|
$models = @()
|
||||||
|
|
||||||
|
# Load cached model list first (Source B) to keep the UI fast.
|
||||||
|
# The cache is refreshed automatically when missing or invalid.
|
||||||
|
try {
|
||||||
|
$cachePath = Get-SurfaceDriverIndexCachePath -DriversFolder $DriversFolder
|
||||||
|
if (Test-Path -Path $cachePath -PathType Leaf) {
|
||||||
|
$cacheAgeDays = ((Get-Date) - (Get-Item -Path $cachePath).LastWriteTime).TotalDays
|
||||||
|
if ($cacheAgeDays -lt 7) {
|
||||||
|
$cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder
|
||||||
|
if ($cache.ModelIndex -and $cache.ModelIndex.Count -gt 0) {
|
||||||
|
WriteLog "Surface cache: Using cached Microsoft model list ($($cache.ModelIndex.Count) models)."
|
||||||
|
return @($cache.ModelIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Surface cache: Failed to load cached Microsoft model list. Falling back to online parse. Error: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
WriteLog "Getting Surface driver information from $url"
|
WriteLog "Getting Surface driver information from $url"
|
||||||
$OriginalVerbosePreference = $VerbosePreference
|
$OriginalVerbosePreference = $VerbosePreference
|
||||||
@@ -70,6 +91,18 @@ function Get-MicrosoftDriversModelList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
WriteLog "Parsing complete. Found $($models.Count) models."
|
WriteLog "Parsing complete. Found $($models.Count) models."
|
||||||
|
|
||||||
|
# Persist model list (Source B) into the local cache for fast UI population.
|
||||||
|
try {
|
||||||
|
$cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder
|
||||||
|
$cache.ModelIndex = @($models)
|
||||||
|
Save-SurfaceDriverIndexCache -Cache $cache -DriversFolder $DriversFolder
|
||||||
|
WriteLog "Surface cache: Saved Microsoft model list to cache."
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Surface cache: Failed to save Microsoft model list. Error: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
return $models
|
return $models
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
@@ -152,6 +185,47 @@ function Save-MicrosoftDriversTask {
|
|||||||
### GET THE DOWNLOAD LINK
|
### GET THE DOWNLOAD LINK
|
||||||
$status = "Getting download link..."
|
$status = "Getting download link..."
|
||||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
||||||
|
|
||||||
|
# Initialize Win10/Win11 link variables
|
||||||
|
$win10Link = $null
|
||||||
|
$win10FileName = $null
|
||||||
|
$win11Link = $null
|
||||||
|
$win11FileName = $null
|
||||||
|
|
||||||
|
# Prefer cached Download Center details (Source C) to avoid unnecessary internet calls and cache rewrites
|
||||||
|
$useCachedDownloadCenterDetails = $false
|
||||||
|
try {
|
||||||
|
$cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder
|
||||||
|
$cachedDetails = @($cache.DownloadCenterDetails | Where-Object { $_.Link -eq $modelLink } | Select-Object -First 1)
|
||||||
|
if ($cachedDetails.Count -gt 0 -and $cachedDetails[0].Files -and $cachedDetails[0].Files.Count -gt 0) {
|
||||||
|
$useCachedDownloadCenterDetails = $true
|
||||||
|
WriteLog "Surface cache: Using cached Download Center details for $modelName from $modelLink"
|
||||||
|
|
||||||
|
foreach ($downloadFile in @($cachedDetails[0].Files)) {
|
||||||
|
if ($null -eq $downloadFile) { continue }
|
||||||
|
$currentFileName = $downloadFile.Name
|
||||||
|
$fileUrl = $downloadFile.Url
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentFileName) -or [string]::IsNullOrWhiteSpace($fileUrl)) { continue }
|
||||||
|
|
||||||
|
if ($currentFileName -match "Win10") {
|
||||||
|
$win10Link = $fileUrl
|
||||||
|
$win10FileName = $currentFileName
|
||||||
|
WriteLog "Found Win10 link (cached): $win10FileName"
|
||||||
|
}
|
||||||
|
elseif ($currentFileName -match "Win11") {
|
||||||
|
$win11Link = $fileUrl
|
||||||
|
$win11FileName = $currentFileName
|
||||||
|
WriteLog "Found Win11 link (cached): $win11FileName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Surface cache: Failed loading cached Download Center details for '$modelName'. Error: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache miss: download and parse the model's Download Center page (Source C), then backfill the cache
|
||||||
|
if (-not $useCachedDownloadCenterDetails) {
|
||||||
WriteLog "Getting download page content for $modelName from $modelLink"
|
WriteLog "Getting download page content for $modelName from $modelLink"
|
||||||
$OriginalVerbosePreference = $VerbosePreference
|
$OriginalVerbosePreference = $VerbosePreference
|
||||||
$VerbosePreference = 'SilentlyContinue'
|
$VerbosePreference = 'SilentlyContinue'
|
||||||
@@ -172,12 +246,6 @@ function Save-MicrosoftDriversTask {
|
|||||||
$downloadFilePattern = '"name":"([^"]+\.(?:msi|zip))",[^}]*?"url":"(.*?)"'
|
$downloadFilePattern = '"name":"([^"]+\.(?:msi|zip))",[^}]*?"url":"(.*?)"'
|
||||||
$downloadFileMatches = [regex]::Matches($scriptContent, $downloadFilePattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
$downloadFileMatches = [regex]::Matches($scriptContent, $downloadFilePattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
||||||
|
|
||||||
|
|
||||||
$win10Link = $null
|
|
||||||
$win10FileName = $null
|
|
||||||
$win11Link = $null
|
|
||||||
$win11FileName = $null
|
|
||||||
|
|
||||||
# Iterate through all matches to find potential Win10 and Win11 links
|
# Iterate through all matches to find potential Win10 and Win11 links
|
||||||
foreach ($downloadFile in $downloadFileMatches) {
|
foreach ($downloadFile in $downloadFileMatches) {
|
||||||
$currentFileName = $downloadFile.Groups[1].Value
|
$currentFileName = $downloadFile.Groups[1].Value
|
||||||
@@ -195,6 +263,45 @@ function Save-MicrosoftDriversTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Update local cache with Download Center file details (Source C) for this model.
|
||||||
|
# This runs during download (not during Get Models) so it won't slow the listview population.
|
||||||
|
try {
|
||||||
|
$filesForCache = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||||
|
if ($win10Link -and $win10FileName) {
|
||||||
|
$filesForCache.Add([pscustomobject]@{ Name = $win10FileName; Url = $win10Link })
|
||||||
|
}
|
||||||
|
if ($win11Link -and $win11FileName) {
|
||||||
|
$filesForCache.Add([pscustomobject]@{ Name = $win11FileName; Url = $win11Link })
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($filesForCache.Count -gt 0) {
|
||||||
|
$cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder
|
||||||
|
$detailsEntry = [pscustomobject][ordered]@{
|
||||||
|
Model = $modelName
|
||||||
|
Link = $modelLink
|
||||||
|
Files = @($filesForCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
$newDetails = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||||
|
foreach ($item in @($cache.DownloadCenterDetails)) {
|
||||||
|
if ($null -ne $item -and $item.PSObject.Properties['Link'] -and $item.Link -ne $modelLink) {
|
||||||
|
$newDetails.Add($item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$newDetails.Add($detailsEntry)
|
||||||
|
$cache.DownloadCenterDetails = @($newDetails)
|
||||||
|
Save-SurfaceDriverIndexCache -Cache $cache -DriversFolder $DriversFolder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Surface cache: Failed updating Download Center details cache for '$modelName'. Error: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$useCachedDownloadCenterDetails = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($useCachedDownloadCenterDetails) {
|
||||||
# Decision logic to select the appropriate download link
|
# Decision logic to select the appropriate download link
|
||||||
$downloadLink = $null
|
$downloadLink = $null
|
||||||
$fileName = $null
|
$fileName = $null
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ function Convert-DriverItemToJsonModel {
|
|||||||
|
|
||||||
switch ($SelectedMake) {
|
switch ($SelectedMake) {
|
||||||
'Microsoft' {
|
'Microsoft' {
|
||||||
$rawModels = Get-MicrosoftDriversModelList -Headers $Headers -UserAgent $UserAgent
|
$rawModels = Get-MicrosoftDriversModelList -Headers $Headers -UserAgent $UserAgent -DriversFolder $localDriversFolder
|
||||||
}
|
}
|
||||||
'Dell' {
|
'Dell' {
|
||||||
$rawModels = Get-DellDriversModelList -WindowsRelease $localWindowsRelease -DriversFolder $localDriversFolder -Make $SelectedMake
|
$rawModels = Get-DellDriversModelList -WindowsRelease $localWindowsRelease -DriversFolder $localDriversFolder -Make $SelectedMake
|
||||||
@@ -969,6 +969,11 @@ function Invoke-DownloadSelectedDrivers {
|
|||||||
Model = $modelName
|
Model = $modelName
|
||||||
DriverPath = $driverPath
|
DriverPath = $driverPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($driverMetadata.PSObject.Properties['Link'] -and -not [string]::IsNullOrWhiteSpace($driverMetadata.Link)) {
|
||||||
|
$driverRecord | Add-Member -NotePropertyName Link -NotePropertyValue $driverMetadata.Link
|
||||||
|
}
|
||||||
|
|
||||||
if ($driverMetadata.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($driverMetadata.SystemId)) {
|
if ($driverMetadata.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($driverMetadata.SystemId)) {
|
||||||
$driverRecord | Add-Member -NotePropertyName SystemId -NotePropertyValue $driverMetadata.SystemId
|
$driverRecord | Add-Member -NotePropertyName SystemId -NotePropertyValue $driverMetadata.SystemId
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function Get-HardDrive() {
|
|||||||
if ($manufacturer -eq 'Microsoft Corporation' -and $model -eq 'Virtual Machine') {
|
if ($manufacturer -eq 'Microsoft Corporation' -and $model -eq 'Virtual Machine') {
|
||||||
WriteLog 'Running in a Hyper-V VM. Getting virtual disk on Index 0 and SCSILogicalUnit 0'
|
WriteLog 'Running in a Hyper-V VM. Getting virtual disk on Index 0 and SCSILogicalUnit 0'
|
||||||
$diskDriveCandidates = @(Get-CimInstance -Class 'Win32_DiskDrive' | Where-Object { $_.MediaType -eq 'Fixed hard disk media' `
|
$diskDriveCandidates = @(Get-CimInstance -Class 'Win32_DiskDrive' | Where-Object { $_.MediaType -eq 'Fixed hard disk media' `
|
||||||
-and $_.Model -eq 'Microsoft Virtual Disk'
|
-and $_.Model -eq 'Microsoft Virtual Disk' `
|
||||||
-and $_.Index -eq 0 `
|
-and $_.Index -eq 0 `
|
||||||
-and $_.SCSILogicalUnit -eq 0
|
-and $_.SCSILogicalUnit -eq 0
|
||||||
})
|
})
|
||||||
@@ -74,7 +74,13 @@ function Invoke-Process {
|
|||||||
|
|
||||||
[Parameter()]
|
[Parameter()]
|
||||||
[ValidateNotNullOrEmpty()]
|
[ValidateNotNullOrEmpty()]
|
||||||
[string]$ArgumentList
|
[string]$ArgumentList,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[switch]$IgnoreExitCode,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[switch]$PassThruExitCode
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
@@ -96,19 +102,39 @@ function Invoke-Process {
|
|||||||
$cmd = Start-Process @startProcessParams
|
$cmd = Start-Process @startProcessParams
|
||||||
$cmdOutput = Get-Content -Path $stdOutTempFile -Raw
|
$cmdOutput = Get-Content -Path $stdOutTempFile -Raw
|
||||||
$cmdError = Get-Content -Path $stdErrTempFile -Raw
|
$cmdError = Get-Content -Path $stdErrTempFile -Raw
|
||||||
|
|
||||||
if ($cmd.ExitCode -ne 0) {
|
if ($cmd.ExitCode -ne 0) {
|
||||||
|
# Non-terminating mode: capture output to Scriptlog and continue
|
||||||
|
if ($IgnoreExitCode) {
|
||||||
|
if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) {
|
||||||
|
WriteLog $cmdOutput
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrEmpty($cmdError) -eq $false) {
|
||||||
|
WriteLog $cmdError
|
||||||
|
}
|
||||||
|
if ($PassThruExitCode) {
|
||||||
|
return $cmd.ExitCode
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if ($cmdError) {
|
if ($cmdError) {
|
||||||
throw $cmdError.Trim()
|
throw $cmdError.Trim()
|
||||||
}
|
}
|
||||||
if ($cmdOutput) {
|
if ($cmdOutput) {
|
||||||
throw $cmdOutput.Trim()
|
throw $cmdOutput.Trim()
|
||||||
}
|
}
|
||||||
|
throw "Process failed. ExitCode = $($cmd.ExitCode)."
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) {
|
if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) {
|
||||||
WriteLog $cmdOutput
|
WriteLog $cmdOutput
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($PassThruExitCode) {
|
||||||
|
return $cmd.ExitCode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
@@ -558,6 +584,21 @@ function Find-DriverMappingRule {
|
|||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
'Microsoft' {
|
'Microsoft' {
|
||||||
|
# Prefer System SKU matching for Microsoft/Surface when available.
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($systemSkuNormalized)) {
|
||||||
|
foreach ($rule in $rulesForMake) {
|
||||||
|
if ($rule.PSObject.Properties['SystemSku'] -and $null -ne $rule.SystemSku) {
|
||||||
|
foreach ($sku in @($rule.SystemSku)) {
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($sku) -and $sku.Trim().ToUpperInvariant() -eq $systemSkuNormalized) {
|
||||||
|
WriteLog "DriverMapping: Microsoft SystemSku '$systemSkuNormalized' matched '$($rule.Model)'."
|
||||||
|
return $rule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback to model string comparison (legacy behavior).
|
||||||
foreach ($rule in $rulesForMake) {
|
foreach ($rule in $rulesForMake) {
|
||||||
$ruleModelNorm = ConvertTo-ComparableModelName -Text $rule.Model
|
$ruleModelNorm = ConvertTo-ComparableModelName -Text $rule.Model
|
||||||
if (-not [string]::IsNullOrWhiteSpace($ruleModelNorm) -and $ruleModelNorm -eq $normalizedModel) {
|
if (-not [string]::IsNullOrWhiteSpace($ruleModelNorm) -and $ruleModelNorm -eq $normalizedModel) {
|
||||||
@@ -1562,65 +1603,182 @@ if ($null -ne $DriverSourcePath) {
|
|||||||
Write-Host "Installing drivers from WIM: $DriverSourcePath"
|
Write-Host "Installing drivers from WIM: $DriverSourcePath"
|
||||||
$TempDriverDir = "W:\TempDrivers"
|
$TempDriverDir = "W:\TempDrivers"
|
||||||
try {
|
try {
|
||||||
|
# Create working folder for WIM-based drivers
|
||||||
WriteLog "Creating temporary directory for drivers at $TempDriverDir"
|
WriteLog "Creating temporary directory for drivers at $TempDriverDir"
|
||||||
New-Item -Path $TempDriverDir -ItemType Directory -Force | Out-Null
|
New-Item -Path $TempDriverDir -ItemType Directory -Force | Out-Null
|
||||||
|
|
||||||
|
# Mount the driver WIM read-only so DISM can recurse the extracted INF tree
|
||||||
WriteLog "Mounting WIM contents to $TempDriverDir"
|
WriteLog "Mounting WIM contents to $TempDriverDir"
|
||||||
Write-Host "Mounting WIM contents to $TempDriverDir"
|
Write-Host "Mounting WIM contents to $TempDriverDir"
|
||||||
# For some reason can't use /mount-image with invoke-process, so using dism.exe directly
|
# For some reason can't use /mount-image with invoke-process, so using dism.exe directly
|
||||||
dism.exe /Mount-Image /ImageFile:$DriverSourcePath /Index:1 /MountDir:$TempDriverDir /ReadOnly /optimize
|
dism.exe /Mount-Image /ImageFile:$DriverSourcePath /Index:1 /MountDir:$TempDriverDir /ReadOnly /optimize
|
||||||
|
$mountExitCode = $LASTEXITCODE
|
||||||
|
if ($mountExitCode -ne 0) {
|
||||||
|
throw "DISM WIM mount failed. LastExitCode = $mountExitCode."
|
||||||
|
}
|
||||||
WriteLog "WIM mount successful."
|
WriteLog "WIM mount successful."
|
||||||
|
|
||||||
|
# Inject drivers into the offline Windows image; failures here should not stop deployment
|
||||||
WriteLog "Injecting drivers from $TempDriverDir"
|
WriteLog "Injecting drivers from $TempDriverDir"
|
||||||
Write-Host "Injecting drivers from $TempDriverDir"
|
Write-Host "Injecting drivers from $TempDriverDir"
|
||||||
Write-Host "This may take a while, please be patient."
|
Write-Host "This may take a while, please be patient."
|
||||||
Invoke-Process dism.exe "/image:W:\ /Add-Driver /Driver:""$TempDriverDir"" /Recurse"
|
$driverInjectExitCode = Invoke-Process -FilePath dism.exe -ArgumentList "/image:W:\ /Add-Driver /Driver:""$TempDriverDir"" /Recurse" -IgnoreExitCode -PassThruExitCode
|
||||||
WriteLog "Driver injection from WIM succeeded."
|
if ($driverInjectExitCode -ne 0) {
|
||||||
Write-Host "Driver injection from WIM succeeded."
|
$warningMessage = "Warning: One or more drivers failed to inject from WIM. ExitCode = $driverInjectExitCode. Continuing deployment."
|
||||||
|
WriteLog $warningMessage
|
||||||
|
Write-Host $warningMessage -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Copy setupapi.offline.log to the USB drive when driver injection fails
|
||||||
|
|
||||||
|
$setupApiLogPath = 'W:\Windows\INF\setupapi.offline.log'
|
||||||
|
if (Test-Path -Path $setupApiLogPath) {
|
||||||
|
try {
|
||||||
|
Invoke-Process xcopy.exe """$setupApiLogPath"" ""$USBDrive"" /Y"
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
WriteLog "An error occurred during WIM driver installation: $_"
|
WriteLog "Warning: Failed to copy setupapi.offline.log to $USBDrive. "
|
||||||
# Copy DISM log to USBDrive for debugging
|
}
|
||||||
invoke-process xcopy.exe "X:\Windows\logs\dism\dism.log $USBDrive /Y"
|
}
|
||||||
throw $_
|
else {
|
||||||
|
WriteLog "Warning: setupapi.offline.log not found at $setupApiLogPath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "Driver injection from WIM succeeded."
|
||||||
|
Write-Host "Driver injection from WIM succeeded."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$warningMessage = "Warning: An error occurred during WIM driver installation. Continuing deployment."
|
||||||
|
WriteLog $warningMessage
|
||||||
|
Write-Host $warningMessage -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Copy troubleshooting logs to the USB drive when driver installation fails
|
||||||
|
try {
|
||||||
|
Invoke-Process cmd.exe "/c copy /Y ""X:\Windows\logs\dism\dism.log"" ""$($USBDrive)dism_driverinject.log"""
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Warning: Failed to copy dism.log to $USBDrive."
|
||||||
|
}
|
||||||
|
|
||||||
|
$setupApiLogPath = 'W:\Windows\INF\setupapi.offline.log'
|
||||||
|
if (Test-Path -Path $setupApiLogPath) {
|
||||||
|
try {
|
||||||
|
Invoke-Process xcopy.exe """$setupApiLogPath"" ""$USBDrive"" /Y"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Warning: Failed to copy setupapi.offline.log to $USBDrive."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "Warning: setupapi.offline.log not found at $setupApiLogPath"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
if (Test-Path -Path $TempDriverDir) {
|
if (Test-Path -Path $TempDriverDir) {
|
||||||
|
# Always attempt to unmount and clean up; unmount failures should not stop deployment
|
||||||
WriteLog "Unmounting WIM from $TempDriverDir"
|
WriteLog "Unmounting WIM from $TempDriverDir"
|
||||||
Write-Host "Unmounting WIM from $TempDriverDir"
|
Write-Host "Unmounting WIM from $TempDriverDir"
|
||||||
|
try {
|
||||||
Invoke-Process dism.exe "/Unmount-Image /MountDir:""$TempDriverDir"" /Discard"
|
Invoke-Process dism.exe "/Unmount-Image /MountDir:""$TempDriverDir"" /Discard"
|
||||||
WriteLog "Unmount successful."
|
WriteLog "Unmount successful."
|
||||||
Write-Host "Unmount successful."
|
Write-Host "Unmount successful."
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$warningMessage = "Warning: Failed to unmount WIM from $TempDriverDir. Continuing cleanup."
|
||||||
|
WriteLog $warningMessage
|
||||||
|
Write-Host $warningMessage -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
WriteLog "Cleaning up temporary driver directory: $TempDriverDir"
|
WriteLog "Cleaning up temporary driver directory: $TempDriverDir"
|
||||||
Write-Host "Cleaning up temporary driver directory: $TempDriverDir"
|
Write-Host "Cleaning up temporary driver directory: $TempDriverDir"
|
||||||
|
try {
|
||||||
Remove-Item -Path $TempDriverDir -Recurse -Force
|
Remove-Item -Path $TempDriverDir -Recurse -Force
|
||||||
WriteLog "Cleanup successful."
|
WriteLog "Cleanup successful."
|
||||||
Write-Host "Cleanup successful."
|
Write-Host "Cleanup successful."
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
$warningMessage = "Warning: Failed to clean up temporary driver directory: $TempDriverDir."
|
||||||
|
WriteLog $warningMessage
|
||||||
|
Write-Host $warningMessage -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elseif ($DriverSourceType -eq 'Folder') {
|
elseif ($DriverSourceType -eq 'Folder') {
|
||||||
$substMapping = $null
|
$substMapping = $null
|
||||||
try {
|
try {
|
||||||
|
# Use SUBST to shorten long paths for DISM /Add-Driver
|
||||||
$substMapping = New-DriverSubstMapping -SourcePath $DriverSourcePath
|
$substMapping = New-DriverSubstMapping -SourcePath $DriverSourcePath
|
||||||
$shortDriverPath = $substMapping.DrivePath
|
$shortDriverPath = $substMapping.DrivePath
|
||||||
WriteLog "Injecting drivers from folder via SUBST. Source: $DriverSourcePath, Mapped: $($substMapping.DriveName)"
|
WriteLog "Injecting drivers from folder via SUBST. Source: $DriverSourcePath, Mapped: $($substMapping.DriveName)"
|
||||||
Write-Host "Injecting drivers from folder: $shortDriverPath"
|
Write-Host "Injecting drivers from folder: $shortDriverPath"
|
||||||
Write-Host "This may take a while, please be patient."
|
Write-Host "This may take a while, please be patient."
|
||||||
Invoke-Process dism.exe "/image:W:\ /Add-Driver /Driver:$shortDriverPath /Recurse"
|
|
||||||
|
# Inject drivers into the offline Windows image; failures here should not stop deployment
|
||||||
|
$driverInjectExitCode = Invoke-Process -FilePath dism.exe -ArgumentList "/image:W:\ /Add-Driver /Driver:$shortDriverPath /Recurse" -IgnoreExitCode -PassThruExitCode
|
||||||
|
if ($driverInjectExitCode -ne 0) {
|
||||||
|
$warningMessage = "Warning: One or more drivers failed to inject from folder. ExitCode = $driverInjectExitCode. Continuing deployment."
|
||||||
|
WriteLog $warningMessage
|
||||||
|
Write-Host $warningMessage -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Copy setupapi.offline.log to the USB drive when driver injection fails
|
||||||
|
$setupApiLogPath = 'W:\Windows\INF\setupapi.offline.log'
|
||||||
|
if (Test-Path -Path $setupApiLogPath) {
|
||||||
|
try {
|
||||||
|
Invoke-Process xcopy.exe """$setupApiLogPath"" ""$USBDrive"" /Y"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Warning: Failed to copy setupapi.offline.log to $USBDrive. "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "Warning: setupapi.offline.log not found at $setupApiLogPath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
WriteLog "Driver injection from folder succeeded."
|
WriteLog "Driver injection from folder succeeded."
|
||||||
Write-Host "Driver injection from folder succeeded."
|
Write-Host "Driver injection from folder succeeded."
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch {
|
catch {
|
||||||
WriteLog "An error occurred during folder driver installation: $_"
|
$warningMessage = "Warning: An error occurred during folder driver installation. Continuing deployment."
|
||||||
|
WriteLog $warningMessage
|
||||||
|
Write-Host $warningMessage -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Copy troubleshooting logs to the USB drive when driver installation fails
|
||||||
|
try {
|
||||||
Invoke-Process xcopy.exe "X:\Windows\logs\dism\dism.log $USBDrive /Y"
|
Invoke-Process xcopy.exe "X:\Windows\logs\dism\dism.log $USBDrive /Y"
|
||||||
throw $_
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Warning: Failed to copy dism.log to $USBDrive."
|
||||||
|
}
|
||||||
|
|
||||||
|
$setupApiLogPath = 'W:\Windows\INF\setupapi.offline.log'
|
||||||
|
if (Test-Path -Path $setupApiLogPath) {
|
||||||
|
try {
|
||||||
|
Invoke-Process xcopy.exe """$setupApiLogPath"" ""$USBDrive"" /Y"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Warning: Failed to copy setupapi.offline.log to $USBDrive."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "Warning: setupapi.offline.log not found at $setupApiLogPath"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
# Always attempt to remove SUBST mapping; failures here should not stop deployment
|
||||||
if ($null -ne $substMapping) {
|
if ($null -ne $substMapping) {
|
||||||
|
try {
|
||||||
Remove-DriverSubstMapping -DriveLetter $substMapping.DriveLetter
|
Remove-DriverSubstMapping -DriveLetter $substMapping.DriveLetter
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
$warningMessage = "Warning: Failed to remove SUBST mapping $($substMapping.DriveLetter). Continuing deployment."
|
||||||
|
WriteLog $warningMessage
|
||||||
|
Write-Host $warningMessage -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user