public/Update-OSDeployCoreOS.ps1
|
#Requires -PSEdition Core #Requires -Version 7.4 function Update-OSDeployCoreOS { <# .SYNOPSIS Imports Windows OS images from cached Enterprise ESD files to OSDeployCore. .DESCRIPTION The Update-OSDeployCoreOS function builds a complete Windows OS image layout from Enterprise ESD files already present in the OSDeployCore cache. This function performs the following operations: 1. Validates administrator privileges 2. Retrieves verified ESD files from the OSDeployCore cache via Get-OSDeployCoreESD 3. For each available ESD (x64 and ARM64): a. Expands ESD Index 1 (Windows Setup Media) to create the WinOS-Media layout b. Exports ESD Index 2 (WinPE) to .wim\winpe.wim c. Exports ESD Index 3 (WinSetup) to .wim\winse.wim d. Constructs WinOS-Media\sources\boot.wim from the WinPE and WinSetup images e. Exports the Enterprise (non-N) image to WinOS-Media\sources\install.wim f. Mounts install.wim to extract WinRE, registry hives, boot files, OS system files, and Ethernet/Wi-Fi drivers 4. Creates a parallel windows-re directory with WinRE-specific content The imported images are stored under $env:ProgramData\OSDeployCore with a naming convention of "version-architecture-editionid-language" (e.g., "26200.8457-amd64-enterprise-en-us"). Duplicate imports are detected and skipped. .EXAMPLE Update-OSDeployCoreOS Scans for existing cached Enterprise ESD files and imports all available architectures to OSDeployCore. .EXAMPLE Update-OSDeployCoreOS -Verbose Imports Windows OS images from ESD with detailed verbose output showing each step of the extraction and import process. .EXAMPLE Update-OSDeployCoreOS -WhatIf Shows which OS image directories would be created without performing any work. .INPUTS None This function does not accept pipeline input. .OUTPUTS [System.IO.DirectoryInfo] Returns the destination directory object for each imported image. .NOTES Author: David Segura Version: 0.1.0 ESD index layout: Index 1 Windows Setup Media (expanded to WinOS-Media\) Index 2 Microsoft Windows PE (exported to .wim\winpe.wim + boot.wim index 1) Index 3 Microsoft Windows Setup (exported to .wim\winse.wim + boot.wim index 2) Index 4+ Windows OS editions (Enterprise non-N is selected for install.wim) Prerequisites: - PowerShell 7.4 or higher - Windows 10 or higher - Run as Administrator Dependencies: Module Functions: Get-OSDeployCoreESD, Initialize-OSDeployCorePaths, Test-IsAdministrator, Update-OSDeployCoreESD, Write-OSDeployBanner, Write-OSDeployCoreProgress Executables: curl.exe, makecab.exe, robocopy.exe Windows Features: DISM, Windows ADK WinPE Addon .NET Classes: [IO.Path], [System.IO.DirectoryInfo] #> [CmdletBinding(SupportsShouldProcess)] [OutputType([System.IO.DirectoryInfo])] param ( [Parameter()] [ValidateSet('amd64', 'arm64')] [System.String] $Architecture ) begin { Write-OSDeployBanner Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Start" Initialize-OSDeployCorePaths # Requires Run as Administrator if (-not (Test-IsAdministrator)) { Write-Warning "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] This function must be Run as Administrator" return } # Retrieve verified ESD files from the OSDeployCore cache Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Retrieving verified ESD files via Get-OSDeployCoreESD" $esdFiles = Get-OSDeployCoreESD if ($Architecture -and $esdFiles) { $archPattern = if ($Architecture -eq 'amd64') { '_x64FRE_' } else { '_A64FRE_' } $esdFiles = @($esdFiles | Where-Object { $_.Name -match $archPattern }) Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Architecture filter applied: $Architecture ($($esdFiles.Count) ESD(s) matched)" } } process { if (-not $esdFiles) { Write-Warning "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] No verified ESD files found. Run Update-OSDeployCoreESD to download ESD files first." return } $WindowsOSRoot = [System.IO.Path]::Combine($Script:OSDeployCorePath, 'cache', 'windows-os') $WindowsRERoot = [System.IO.Path]::Combine($Script:OSDeployCorePath, 'cache', 'windows-re') foreach ($esdFile in $esdFiles) { $esdPath = $esdFile.FullName $esdFileName = $esdFile.Name Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Processing ESD: $esdFileName" # ------------------------------------------------------------------------- # Derive version and architecture from the ESD filename # Filename pattern: 26200.8457.260507-0702.25h2_ge_release_..._x64FRE_en-us.esd # ------------------------------------------------------------------------- if ($esdFileName -match '^(\d+\.\d+)') { $buildNumber = $Matches[1] } else { Write-Warning "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Cannot determine build number from '$esdFileName'. Skipping." continue } if ($esdFileName -match '_x64FRE_') { $archNorm = 'amd64' } elseif ($esdFileName -match '_A64FRE_') { $archNorm = 'arm64' } else { Write-Warning "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Cannot determine architecture from '$esdFileName'. Skipping." continue } $DestinationName = "$buildNumber-$archNorm-enterprise-en-us" $DestinationDirectory = Join-Path $WindowsOSRoot $DestinationName $ImportWinREDirectory = Join-Path $WindowsRERoot $DestinationName Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] DestinationName: $DestinationName" # Check for duplicate import if ([System.IO.Directory]::Exists($DestinationDirectory)) { Write-Warning "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] '$DestinationName' already exists in windows-os. Skipping duplicate import." continue } if ([System.IO.Directory]::Exists($ImportWinREDirectory)) { Write-Warning "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] '$DestinationName' already exists in windows-re. Skipping duplicate import." continue } if (-not $PSCmdlet.ShouldProcess($DestinationName, 'Import Windows OS image from ESD')) { continue } Write-OSDeployCoreProgress "Importing $DestinationName ..." $DestinationCore = Join-Path $DestinationDirectory '.core' $DestinationTemp = Join-Path $DestinationDirectory '.temp' $DestinationLogs = Join-Path $DestinationTemp 'logs' $DestinationWim = Join-Path $DestinationDirectory '.wim' $DestinationMedia = Join-Path $DestinationDirectory 'WinOS-Media' [System.IO.Directory]::CreateDirectory($DestinationCore) | Out-Null [System.IO.Directory]::CreateDirectory($DestinationLogs) | Out-Null [System.IO.Directory]::CreateDirectory($DestinationWim) | Out-Null [System.IO.Directory]::CreateDirectory($DestinationMedia) | Out-Null # Write id.json @{ id = $DestinationName } | ConvertTo-Json -Depth 5 | Out-File (Join-Path $DestinationCore 'id.json') -Encoding utf8 -Force # ------------------------------------------------------------------------- # Index 1 — Expand Windows Setup Media layout to WinOS-Media\ # ------------------------------------------------------------------------- Write-OSDeployCoreProgress 'Expanding Windows Setup Media (Index 1) ...' $CurrentLog = Join-Path $DestinationLogs "$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Expand-SetupMedia.log" Expand-WindowsImage -ImagePath $esdPath -Index 1 -ApplyPath $DestinationMedia -LogPath $CurrentLog | Out-Null # Remove read-only attributes set by Expand-WindowsImage foreach ($filePath in [System.IO.Directory]::EnumerateFiles($DestinationMedia, '*', [System.IO.SearchOption]::AllDirectories)) { $fi = [System.IO.FileInfo]::new($filePath) if ($fi.IsReadOnly) { $fi.IsReadOnly = $false } } # ------------------------------------------------------------------------- # Index 2 — WinPE → .wim\winpe.wim # ------------------------------------------------------------------------- Write-OSDeployCoreProgress 'Exporting WinPE (Index 2) ...' $CurrentLog = Join-Path $DestinationLogs "$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Export-WinPE.log" $WinpeWimPath = Join-Path $DestinationWim 'winpe.wim' Export-WindowsImage -SourceImagePath $esdPath -SourceIndex 2 -DestinationImagePath $WinpeWimPath -LogPath $CurrentLog | Out-Null $WinpeImage = Get-WindowsImage -ImagePath $WinpeWimPath -Index 1 $WinpeImage | ConvertTo-Json -Depth 5 | Out-File (Join-Path $DestinationCore 'winpe-windowsimage.json') -Encoding utf8 -Force $WinpeImage | Export-Clixml -Path (Join-Path $DestinationCore 'winpe-windowsimage.xml') $WinpeImageContent = Get-WindowsImageContent -ImagePath $WinpeWimPath -Index 1 $WinpeImageContent | Out-File (Join-Path $DestinationCore 'winpe-windowsimagecontent.txt') -Encoding ascii -Force # ------------------------------------------------------------------------- # Index 3 — WinSetup → .wim\winse.wim # ------------------------------------------------------------------------- Write-OSDeployCoreProgress 'Exporting WinSetup (Index 3) ...' $CurrentLog = Join-Path $DestinationLogs "$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Export-WinSE.log" $WinseWimPath = Join-Path $DestinationWim 'winse.wim' Export-WindowsImage -SourceImagePath $esdPath -SourceIndex 3 -DestinationImagePath $WinseWimPath -LogPath $CurrentLog | Out-Null $WinseImage = Get-WindowsImage -ImagePath $WinseWimPath -Index 1 $WinseImage | ConvertTo-Json -Depth 5 | Out-File (Join-Path $DestinationCore 'winse-windowsimage.json') -Encoding utf8 -Force $WinseImage | Export-Clixml -Path (Join-Path $DestinationCore 'winse-windowsimage.xml') $WinseImageContent = Get-WindowsImageContent -ImagePath $WinseWimPath -Index 1 $WinseImageContent | Out-File (Join-Path $DestinationCore 'winse-windowsimagecontent.txt') -Encoding ascii -Force # ------------------------------------------------------------------------- # Build WinOS-Media\sources\boot.wim from winpe.wim + winse.wim # boot.wim index 1 = WinPE, index 2 = WinSetup (standard layout) # ------------------------------------------------------------------------- Write-OSDeployCoreProgress 'Building boot.wim ...' $BootWimPath = Join-Path $DestinationMedia 'sources\boot.wim' $BootWimSourcesDir = Split-Path $BootWimPath -Parent [System.IO.Directory]::CreateDirectory($BootWimSourcesDir) | Out-Null # Remove an existing boot.wim placed by the media expansion (if any) if ([System.IO.File]::Exists($BootWimPath)) { [System.IO.File]::Delete($BootWimPath) } $CurrentLog = Join-Path $DestinationLogs "$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Export-BootWim-PE.log" Export-WindowsImage -SourceImagePath $WinpeWimPath -SourceIndex 1 -DestinationImagePath $BootWimPath -LogPath $CurrentLog | Out-Null $CurrentLog = Join-Path $DestinationLogs "$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Export-BootWim-SE.log" Export-WindowsImage -SourceImagePath $WinseWimPath -SourceIndex 1 -DestinationImagePath $BootWimPath -LogPath $CurrentLog | Out-Null # ------------------------------------------------------------------------- # Find Enterprise (non-N) index in the ESD, then export to install.wim # ------------------------------------------------------------------------- Write-OSDeployCoreProgress 'Locating Enterprise image index ...' $allEsdImages = Get-WindowsImage -ImagePath $esdPath $enterpriseEntry = $allEsdImages | Where-Object { $_.ImageName -like '*Enterprise*' -and $_.ImageName -notlike '*Enterprise N*' } | Select-Object -First 1 if (-not $enterpriseEntry) { Write-Warning "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] No Enterprise (non-N) image found in '$esdFileName'. Skipping." continue } $enterpriseIndex = $enterpriseEntry.ImageIndex $DestinationImagePath = Join-Path $DestinationMedia 'sources\install.wim' Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Enterprise index: $enterpriseIndex ($($enterpriseEntry.ImageName))" Write-OSDeployCoreProgress "Exporting Enterprise image (Index $enterpriseIndex) ..." $CurrentLog = Join-Path $DestinationLogs "$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Export-install.wim.log" Export-WindowsImage -SourceImagePath $esdPath -SourceIndex $enterpriseIndex -DestinationImagePath $DestinationImagePath -LogPath $CurrentLog | Out-Null # ------------------------------------------------------------------------- # Export install.wim metadata # ------------------------------------------------------------------------- $Image = Get-WindowsImage -ImagePath $DestinationImagePath -Index 1 $Image | Export-Clixml -Path (Join-Path $DestinationCore 'winos-windowsimage.xml') $Image | ConvertTo-Json -Depth 5 | Out-File (Join-Path $DestinationCore 'winos-windowsimage.json') -Encoding utf8 $ImageContent = Get-WindowsImageContent -ImagePath $DestinationImagePath -Index 1 $ImageContent | Out-File (Join-Path $DestinationCore 'winos-windowsimagecontent.txt') -Encoding ascii -Force # Resolve architecture from the exported image's metadata for properties.json $Architecture = $archNorm $Language = ($Image.Languages | Select-Object -First 1).ToLower() # Write windows-os properties.json $WinOSProperties = [ordered]@{ Type = 'WinOS' Id = $DestinationName Name = $DestinationName CreatedTime = $Image.CreatedTime ModifiedTime = $Image.ModifiedTime InstallationType = $Image.InstallationType Version = $Image.Version.ToString() Architecture = $Architecture Languages = @($Image.Languages) ImageSize = $Image.ImageSize DirectoryCount = $Image.DirectoryCount FileCount = $Image.FileCount ImageName = $Image.ImageName EditionId = $Image.EditionId Path = $DestinationDirectory ImagePath = $DestinationImagePath ImageIndex = 1 ImageDescription = $Image.ImageDescription WIMBoot = $Image.WIMBoot ImageType = $Image.ImageType ProductName = $Image.ProductName Hal = $Image.Hal ProductType = $Image.ProductType ProductSuite = $Image.ProductSuite MajorVersion = $Image.MajorVersion MinorVersion = $Image.MinorVersion Build = $Image.Build SPBuild = $Image.SPBuild SPLevel = $Image.SPLevel ImageBootable = $Image.ImageBootable SystemRoot = $Image.SystemRoot DefaultLanguageIndex = $Image.DefaultLanguageIndex } $WinOSProperties | ConvertTo-Json -Depth 5 | Out-File (Join-Path $DestinationDirectory 'properties.json') -Encoding utf8 -Force # ------------------------------------------------------------------------- # Mount install.wim read-only to extract WinRE and supplemental content # ------------------------------------------------------------------------- $MountPath = [System.IO.Path]::Combine($env:TEMP, "OSDeployCore-Mount-$([Guid]::NewGuid().ToString('N').Substring(0, 8))") [System.IO.Directory]::CreateDirectory($MountPath) | Out-Null Write-OSDeployCoreProgress 'Mounting Windows image (read-only) ...' try { Mount-WindowsImage -ImagePath $DestinationImagePath -Index 1 -Path $MountPath -ReadOnly -ErrorAction Stop | Out-Null $MountDirectory = $MountPath #region WinRE extraction Write-OSDeployCoreProgress 'Extracting WinRE ...' $winreSource = Join-Path $MountDirectory 'Windows\System32\Recovery\winre.wim' $reagentSource = Join-Path $MountDirectory 'Windows\System32\Recovery\ReAgent.xml' if (Test-Path $reagentSource) { Copy-Item -Path $reagentSource -Destination (Join-Path $DestinationTemp 'os-reagent.xml') | Out-Null } if (Test-Path $winreSource) { Copy-Item -Path $winreSource -Destination (Join-Path $DestinationWim 'winre.wim') | Out-Null $WinreWimPath = Join-Path $DestinationWim 'winre.wim' $WinreImage = Get-WindowsImage -ImagePath $WinreWimPath -Index 1 $WinreImage | ConvertTo-Json -Depth 5 | Out-File (Join-Path $DestinationCore 'winre-windowsimage.json') -Encoding utf8 -Force $WinreImage | Export-Clixml -Path (Join-Path $DestinationCore 'winre-windowsimage.xml') $WinreImageContent = Get-WindowsImageContent -ImagePath $WinreWimPath -Index 1 $WinreImageContent | Out-File (Join-Path $DestinationCore 'winre-windowsimagecontent.txt') -Encoding ascii -Force } #endregion #region Registry hives Write-OSDeployCoreProgress 'Backing up registry hives ...' $RegistryHives = @('SOFTWARE', 'SYSTEM') $RobocopyLog = Join-Path $DestinationLogs 'os-registry.log' foreach ($Item in $RegistryHives) { robocopy "$MountDirectory\Windows\System32\config" "$DestinationTemp" $Item /b /np /ts /tee /r:0 /w:0 /log+:"$RobocopyLog" | Out-Null } $softwareSrc = [System.IO.Path]::Combine($DestinationTemp, 'SOFTWARE') $systemSrc = [System.IO.Path]::Combine($DestinationTemp, 'SYSTEM') if ([System.IO.File]::Exists($softwareSrc)) { [System.IO.File]::Move($softwareSrc, [System.IO.Path]::Combine($DestinationTemp, 'os-software.hive'), $true) } if ([System.IO.File]::Exists($systemSrc)) { [System.IO.File]::Move($systemSrc, [System.IO.Path]::Combine($DestinationTemp, 'os-system.hive'), $true) } #endregion #region Boot files $BootPath = Join-Path $MountDirectory 'Windows\Boot' if (Test-Path $BootPath) { Write-OSDeployCoreProgress 'Backing up boot files ...' $RobocopyLog = Join-Path $DestinationLogs 'os-boot.log' robocopy "$BootPath" (Join-Path $DestinationCore 'os-boot') *.* /e /tee /r:0 /w:0 /log+:"$RobocopyLog" | Out-Null } #endregion #region Windows executables and subdirectories Write-OSDeployCoreProgress 'Backing up OS system files ...' $BackupOSFiles = @( 'aerolite*.*' 'bcp47*.dll' 'bits*.*' 'BitsTransfer*.*' 'BranchCache*.*' 'cacls.exe*' 'choice.exe*' 'comp.exe*.*' 'credssp*.*' 'curl.exe' 'ddp*.*' 'defrag.exe*' 'djoin*.*' 'dmcmnutils*.*' 'dssec*.*' 'dsuiext*.*' 'edputil*.*' 'es.dll*' 'explorerframe*.*' 'forfiles*.*' 'getmac*.*' 'gpedit*.*' 'hyyp.sys*' 'magnification*.*' 'magnify*.*' 'makecab.*' 'mdmpostprocessevaluator*.*' 'mdmregistration*.*' 'mscms*.*' 'msinfo32.*' 'mstsc*.*' 'netprofm*.*' 'npmproxy*.*' 'nslookup.*' 'osk*.*' 'PCPKsp.dll*' 'pdh.dll*' 'PeerDist*.*' 'perfmon*.*' 'setx.*' 'shellstyle*.*' 'shutdown.*' 'shutdownext.*' 'shutdownux.*' 'srpapi.dll*' 'ssdpapi*.*' 'StructuredQuery*.*' 'systeminfo.*' 'tar.exe' 'tskill.*' 'w32tm*.*' 'winver.*' 'WSDApi*.*' ) $RobocopyLog = Join-Path $DestinationLogs 'os-files.log' $System32Src = Join-Path $MountDirectory 'Windows\System32' $System32Dst = Join-Path $DestinationCore 'os-files\Windows\System32' foreach ($Item in $BackupOSFiles) { robocopy "$System32Src" "$System32Dst" $Item /s /xd rescache servicing /ndl /b /np /ts /tee /r:0 /w:0 /log+:"$RobocopyLog" | Out-Null } # PowerShell Modules $PsModuleSrc = Join-Path $MountDirectory 'Program Files\WindowsPowerShell' $PsModuleDst = Join-Path $DestinationCore 'os-files\Program Files\WindowsPowerShell' robocopy "$PsModuleSrc" "$PsModuleDst" *.* /e /tee /r:0 /w:0 /log+:"$RobocopyLog" | Out-Null #endregion #region Ethernet drivers Write-OSDeployCoreProgress 'Extracting Ethernet drivers ...' $packagesPath = [System.IO.Path]::Combine($MountDirectory, 'Windows', 'servicing', 'Packages') $driverStoreRepo = [System.IO.Path]::Combine($MountDirectory, 'Windows', 'System32', 'DriverStore', 'FileRepository') $EthernetClientMums = if ([System.IO.Directory]::Exists($packagesPath)) { [System.IO.Directory]::GetFiles($packagesPath, 'Microsoft-Windows-Ethernet-Client-*.mum') } else { @() } Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Ethernet .mum files found: $($EthernetClientMums.Length)" if ($EthernetClientMums.Length -gt 0) { $EthernetDrivers = foreach ($mumPath in $EthernetClientMums) { Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Parsing Ethernet .mum: $mumPath" $MumXml = [System.Xml.Linq.XDocument]::Load($mumPath) $ns = $MumXml.Root.Name.Namespace $Identity = $MumXml.Root.Element($ns + 'assemblyIdentity') $infEl = $MumXml.Root.Descendants($ns + 'inf') | Select-Object -First 1 $DriverInf = $infEl?.Value Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Ethernet Identity.name: $($Identity?.Attribute('name')?.Value) | version: $($Identity?.Attribute('version')?.Value) | arch: $($Identity?.Attribute('processorArchitecture')?.Value) | inf: $DriverInf" if ($Identity -and $DriverInf) { [PSCustomObject]@{ Name = $Identity.Attribute('name').Value -replace '^Microsoft-Windows-Ethernet-Client-', '' -replace '-FOD-Package$', '' Version = [version]$Identity.Attribute('version').Value Architecture = $Identity.Attribute('processorArchitecture').Value InfFile = $DriverInf } } else { Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Ethernet .mum skipped — Identity or DriverInf is null" } } # Deduplicate: keep highest version per driver name (O(n) hashtable vs O(n log n) Group-Object) $dedupHash = @{} foreach ($d in $EthernetDrivers) { if (-not $dedupHash.ContainsKey($d.Name) -or $d.Version -gt $dedupHash[$d.Name].Version) { $dedupHash[$d.Name] = $d } } $EthernetDrivers = $dedupHash.Values Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Ethernet unique drivers after dedup: $($EthernetDrivers.Count)" foreach ($Driver in $EthernetDrivers) { Write-Host -ForegroundColor DarkGray "$($Driver.Name)-$($Driver.Version)" $InfFileWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($Driver.InfFile) Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Ethernet driver: $($Driver.Name) v$($Driver.Version) arch=$($Driver.Architecture) inf=$($Driver.InfFile) infBase=$InfFileWithoutExtension" Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Searching DriverStore: $driverStoreRepo\$InfFileWithoutExtension*" $DriverFolder = [System.IO.Directory]::EnumerateDirectories($driverStoreRepo, "$InfFileWithoutExtension*") | Select-Object -First 1 if ($DriverFolder) { Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Ethernet driver folder found: $DriverFolder" $EthernetDst = [System.IO.Path]::Combine($Script:OSDeployCorePath, 'OSDRepo', 'winpe-drivers', $Driver.Architecture, "microsoft-windows-ethernet-$($Driver.Version)", $Driver.Name) Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Ethernet destination: $EthernetDst" if (Test-Path "$EthernetDst\*") { Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Skipping existing Ethernet driver: $($Driver.Name)-$($Driver.Version)" } else { Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Copying Ethernet driver: $($Driver.Name)-$($Driver.Version)" robocopy "$DriverFolder" "$EthernetDst" *.* /e /r:0 /w:0 /log+:"$RobocopyLog" | Out-Null } } else { Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Ethernet driver folder NOT found for inf base: $InfFileWithoutExtension" } } } else { Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] No Ethernet .mum files found in: $packagesPath" } #endregion #region Wi-Fi drivers Write-OSDeployCoreProgress 'Extracting Wi-Fi drivers ...' $WifiClientMums = if ([System.IO.Directory]::Exists($packagesPath)) { [System.IO.Directory]::GetFiles($packagesPath, 'Microsoft-Windows-Wifi-Client-*.mum') } else { @() } Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Wi-Fi .mum files found: $($WifiClientMums.Length)" if ($WifiClientMums.Length -gt 0) { $WifiDrivers = foreach ($mumPath in $WifiClientMums) { Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Parsing Wi-Fi .mum: $mumPath" $MumXml = [System.Xml.Linq.XDocument]::Load($mumPath) $ns = $MumXml.Root.Name.Namespace $Identity = $MumXml.Root.Element($ns + 'assemblyIdentity') $infEl = $MumXml.Root.Descendants($ns + 'inf') | Select-Object -First 1 $DriverInf = $infEl?.Value Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Wi-Fi Identity.name: $($Identity?.Attribute('name')?.Value) | version: $($Identity?.Attribute('version')?.Value) | arch: $($Identity?.Attribute('processorArchitecture')?.Value) | inf: $DriverInf" if ($Identity -and $DriverInf) { [PSCustomObject]@{ Name = $Identity.Attribute('name').Value -replace '^Microsoft-Windows-Wifi-Client-', '' -replace '-FOD-Package$', '' Version = [version]$Identity.Attribute('version').Value Architecture = $Identity.Attribute('processorArchitecture').Value InfFile = $DriverInf } } else { Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Wi-Fi .mum skipped — Identity or DriverInf is null" } } # Deduplicate: keep highest version per driver name (O(n) hashtable vs O(n log n) Group-Object) $dedupHashWifi = @{} foreach ($d in $WifiDrivers) { if (-not $dedupHashWifi.ContainsKey($d.Name) -or $d.Version -gt $dedupHashWifi[$d.Name].Version) { $dedupHashWifi[$d.Name] = $d } } $WifiDrivers = $dedupHashWifi.Values Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Wi-Fi unique drivers after dedup: $($WifiDrivers.Count)" foreach ($Driver in $WifiDrivers) { Write-Host -ForegroundColor DarkGray "$($Driver.Name)-$($Driver.Version)" $InfFileWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($Driver.InfFile) Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Wi-Fi driver: $($Driver.Name) v$($Driver.Version) arch=$($Driver.Architecture) inf=$($Driver.InfFile) infBase=$InfFileWithoutExtension" Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Searching DriverStore: $driverStoreRepo\$InfFileWithoutExtension*" $DriverFolder = [System.IO.Directory]::EnumerateDirectories($driverStoreRepo, "$InfFileWithoutExtension*") | Select-Object -First 1 if ($DriverFolder) { Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Wi-Fi driver folder found: $DriverFolder" $WifiDst = [System.IO.Path]::Combine($Script:OSDeployCorePath, 'OSDRepo', 'winpe-drivers', $Driver.Architecture, "microsoft-windows-wifi-$($Driver.Version)", $Driver.Name) Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Wi-Fi destination: $WifiDst" if (Test-Path "$WifiDst\*") { Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Skipping existing Wi-Fi driver: $($Driver.Name)-$($Driver.Version)" } else { Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Copying Wi-Fi driver: $($Driver.Name)-$($Driver.Version)" robocopy "$DriverFolder" "$WifiDst" *.* /e /r:0 /w:0 /log+:"$RobocopyLog" | Out-Null } } else { Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Wi-Fi driver folder NOT found for inf base: $InfFileWithoutExtension" } } } else { Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] No Wi-Fi .mum files found in: $packagesPath" } #endregion } finally { # Always dismount to avoid orphaned mounts if ([System.IO.Directory]::Exists($MountPath)) { Write-OSDeployCoreProgress 'Dismounting Windows image ...' Dismount-WindowsImage -Path $MountPath -Discard -ErrorAction SilentlyContinue | Out-Null try { [System.IO.Directory]::Delete($MountPath, $true) } catch { } } } # Remove Read-Only from all imported files foreach ($filePath in [System.IO.Directory]::EnumerateFiles($DestinationDirectory, '*', [System.IO.SearchOption]::AllDirectories)) { $fi = [System.IO.FileInfo]::new($filePath) if ($fi.IsReadOnly) { $fi.IsReadOnly = $false } } #region Build the WinRE directory Write-OSDeployCoreProgress 'Building WinRE directory ...' robocopy (Join-Path $DestinationDirectory '.core') (Join-Path $ImportWinREDirectory '.core') *.* /e /xf OSImage.* winpe-windowsimage* winse-windowsimage* /tee /r:0 /w:0 | Out-Null robocopy (Join-Path $DestinationDirectory '.temp') (Join-Path $ImportWinREDirectory '.temp') *.* /e /xd logs /tee /r:0 /w:0 | Out-Null robocopy (Join-Path $DestinationDirectory '.wim') (Join-Path $ImportWinREDirectory '.wim') winre.wim /e /tee /r:0 /w:0 | Out-Null # Write windows-re properties.json $WinreWimPath = Join-Path $ImportWinREDirectory '.wim\winre.wim' if (Test-Path $WinreWimPath) { $WinreImageForProps = Get-WindowsImage -ImagePath $WinreWimPath -Index 1 $WinREProperties = [ordered]@{ Type = 'WinRE' Id = $DestinationName Name = $DestinationName CreatedTime = $WinreImageForProps.CreatedTime ModifiedTime = $WinreImageForProps.ModifiedTime InstallationType = $WinreImageForProps.InstallationType Version = $WinreImageForProps.Version.ToString() Architecture = $Architecture Languages = @($WinreImageForProps.Languages) ImageSize = $WinreImageForProps.ImageSize DirectoryCount = $WinreImageForProps.DirectoryCount FileCount = $WinreImageForProps.FileCount ImageName = $WinreImageForProps.ImageName OSImageName = $Image.ImageName OSEditionId = $Image.EditionId OSVersion = $Image.Version.ToString() OSCreatedTime = $Image.CreatedTime OSModifiedTime = $Image.ModifiedTime Path = $ImportWinREDirectory ImagePath = $WinreWimPath ImageIndex = 1 } $WinREProperties | ConvertTo-Json -Depth 5 | Out-File (Join-Path $ImportWinREDirectory 'properties.json') -Encoding utf8 -Force } #endregion Write-OSDeployCoreProgress "Import complete: $DestinationName" Get-Item -Path $DestinationDirectory } } end { Write-Verbose "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] End" } } |