public/Update-OSDeployCoreRE.ps1
|
#Requires -PSEdition Core #Requires -Version 7.4 function Update-OSDeployCoreRE { <# .SYNOPSIS Exports Windows Recovery Environment images from cached Enterprise ESD files .DESCRIPTION Retrieves Enterprise ESD files that match the newest OS catalog and pass SHA256 verification, optionally limiting cache verification to one architecture, and exports Windows Recovery Environment images into the OSDeployCore Windows RE cache. To produce each WinRE source, the command also stages a Windows OS cache layout. It uses ESD index 1 for setup media, index 2 for WinPE, and index 3 for WinSetup, then exports the first Enterprise non-N image as install.wim. It mounts that image to extract WinRE, boot files, registry hives, selected system files, and Ethernet and Wi-Fi drivers. An ESD is treated as a duplicate only when both its matching Windows OS and WinRE destination directories already exist. Duplicate exports are skipped and return no object. A completed export returns its supporting Windows OS destination directory. .PARAMETER Architecture Limits ESD cache verification and export to amd64 or arm64. When omitted, all verified cached architectures are processed. .EXAMPLE PS> Update-OSDeployCoreRE Scans cached Enterprise ESD files and exports Windows RE sources for all available architectures. .EXAMPLE PS> Update-OSDeployCoreRE -Verbose Exports Windows RE sources while displaying detailed processing information. .EXAMPLE PS> Update-OSDeployCoreRE -WhatIf Shows which Windows OS and WinRE cache directories would be created without exporting content. .INPUTS None. This function does not accept pipeline input. .OUTPUTS System.IO.DirectoryInfo. Returns the Windows OS destination directory for each completed Windows RE export. Duplicate or skipped exports return no object. .NOTES Author: David Segura Company: Recast Software Version: 1.0.0 Date: 2026-08-28 Requires Windows 11 25H2 or later, PowerShell 7.4 or later installed from MSI, curl.exe, the DISM PowerShell cmdlets, robocopy, and Administrator rights. The selected install image is determined from image names, not from a fixed ESD index. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([System.IO.DirectoryInfo])] param ( [Parameter()] [ValidateSet('amd64', 'arm64')] [System.String] $Architecture ) begin { #================================================= Write-HostOSDeployBanner #================================================= # Require License for this function. If the license is not valid, return without executing the function. if (-not (Test-OSDeployLicenseGate -CommandName $MyInvocation.MyCommand.Name)) { return } #================================================= # Stop before importing operating system content when a required host capability is missing. if (-not (Test-IsWindows11)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Windows 11 is required." } if (-not (Test-IsWindows1125H2)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Windows 11 25H2 (build 26200) is required." } if (-not (Test-PwshVersionMin)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] PowerShell 7.4 or higher is required." } if (-not (Test-PwshPSHome)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] The MSI installation of PowerShell 7 is required." } if (-not (Test-CommandCurl)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] curl.exe is required but was not found in the current PATH. curl.exe ships with Windows 10 1803+." } if (-not (Test-IsAdministrator)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Administrator rights are required. Re-run PowerShell as Administrator and try again." } #================================================= Initialize-OSDeployCorePaths #================================================= # Retrieve only verified ESD files for the requested architecture. Write-Verbose "[$($MyInvocation.MyCommand.Name)] Retrieving verified ESD files via Get-OSDeployCoreESD" $esdFiles = if ($Architecture) { Get-OSDeployCoreESD -Architecture $Architecture } else { Get-OSDeployCoreESD } Write-Verbose "[$($MyInvocation.MyCommand.Name)] Architecture filter: $(if ($Architecture) { $Architecture } else { 'all' }) ($($esdFiles.Count) ESD(s) matched)" } process { # Stop when no verified ESD cache entries are available to import. if (-not $esdFiles) { Write-Warning "[$(Get-Date -Format s)] No verified ESD files found. Run Update-OSDeployCoreESD to download ESD files first." return } $WindowsOSRoot = [System.IO.Path]::Combine($Script:OSDeployCorePath, 'cache', 'windows-os') Write-HostDateTimeDarkCyan "Windows OS image cache: $WindowsOSRoot" $WindowsRERoot = [System.IO.Path]::Combine($Script:OSDeployCorePath, 'cache', 'windows-re') Write-HostDateTimeDarkCyan "Windows Recovery Environment cache: $WindowsRERoot" foreach ($esdFile in $esdFiles) { $esdPath = $esdFile.FullName $esdFileName = $esdFile.Name Write-Verbose "[$($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)] 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)] 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 "[$($MyInvocation.MyCommand.Name)] DestinationName: $DestinationName" # Skip ESD files whose matching OS and WinRE imports already exist. if ([System.IO.Directory]::Exists($DestinationDirectory) -and [System.IO.Directory]::Exists($ImportWinREDirectory)) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Existing Windows OS and WinRE image directories detected for $DestinationName" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Skipping duplicate import" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Build-OSDeployBoot can use this image to create bootable media." continue } # Honor WhatIf and Confirm before creating the Windows OS and WinRE cache layouts. if (-not $PSCmdlet.ShouldProcess($DestinationName, 'Export Windows RE image from ESD')) { continue } Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Exporting Windows RE source for $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-HostDateTimeDarkGray '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-HostDateTimeDarkGray '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-HostDateTimeDarkGray '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-HostDateTimeDarkGray '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 # ------------------------------------------------------------------------- # Select the Enterprise non-N image; skip this ESD when it is unavailable. # ------------------------------------------------------------------------- Write-HostDateTimeDarkGray '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)] No Enterprise (non-N) image found in '$esdFileName'. Skipping." continue } $enterpriseIndex = $enterpriseEntry.ImageIndex $DestinationImagePath = Join-Path $DestinationMedia 'sources\install.wim' Write-Verbose "[$($MyInvocation.MyCommand.Name)] Enterprise index: $enterpriseIndex ($($enterpriseEntry.ImageName))" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] 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-HostDateTimeDarkGray '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-HostDateTimeDarkGray '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-HostDateTimeDarkGray '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-HostDateTimeDarkGray '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-HostDateTimeDarkGray 'Backing up OS system files ...' $BackupOSFiles = @( 'aerolite*.*' 'bcp47*.dll' 'bits*.*' 'BitsTransfer*.*' 'BranchCache*.*' 'cacls.exe*' 'choice.exe*' 'comp.exe*.*' 'credssp*.*' 'curl.exe' 'dism.exe*' 'dismApi.dll*' '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 } # Dism $System32Src = Join-Path $MountDirectory 'Windows\System32\Dism' $System32Dst = Join-Path $DestinationCore 'os-files\Windows\System32\Dism' robocopy "$System32Src" "$System32Dst" *.* /e /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-HostDateTimeDarkGray '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 "[$($MyInvocation.MyCommand.Name)] Ethernet .mum files found: $($EthernetClientMums.Length)" if ($EthernetClientMums.Length -gt 0) { $EthernetDrivers = foreach ($mumPath in $EthernetClientMums) { Write-Verbose "[$($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') $DriverElement = $MumXml.Root.Descendants($ns + 'driver') | Select-Object -First 1 $DriverInfAttr = if ($DriverElement) { $DriverElement.Attribute('inf') } else { $null } $DriverInf = if ($DriverInfAttr) { $DriverInfAttr.Value } else { $MumXml.Root.Descendants($ns + 'inf') | Select-Object -First 1 | ForEach-Object { $_.Value } } $identityNameAttr = if ($Identity) { $Identity.Attribute('name') } else { $null } $identityVersionAttr = if ($Identity) { $Identity.Attribute('version') } else { $null } $identityArchAttr = if ($Identity) { $Identity.Attribute('processorArchitecture') } else { $null } $identityName = if ($identityNameAttr) { $identityNameAttr.Value } else { $null } $identityVersion = if ($identityVersionAttr) { $identityVersionAttr.Value } else { $null } $identityArch = if ($identityArchAttr) { $identityArchAttr.Value } else { $null } Write-Verbose "[$($MyInvocation.MyCommand.Name)] Ethernet Identity.name: $identityName | version: $identityVersion | arch: $identityArch | inf: $DriverInf" if ($Identity -and $DriverInf -and $identityName -and $identityVersion -and $identityArch) { [PSCustomObject]@{ Name = $identityName -replace '^Microsoft-Windows-Ethernet-Client-', '' -replace '-FOD-Package$', '' Version = [version]$identityVersion Architecture = $identityArch InfFile = $DriverInf } } else { $skipReason = if ($identityName -like '*-Wrapper') { 'wrapper manifest does not contain a driver element' } elseif (-not $DriverElement) { 'driver element not found' } elseif (-not $DriverInf) { "driver element has no inf attribute; attributes: $($DriverElement.Attributes() -join ', ')" } else { 'missing assembly identity or required identity attributes' } Write-Verbose "[$($MyInvocation.MyCommand.Name)] Ethernet .mum skipped - $skipReason" } } # 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 "[$($MyInvocation.MyCommand.Name)] Ethernet unique drivers after dedup: $($EthernetDrivers.Count)" foreach ($Driver in $EthernetDrivers) { $InfFileWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($Driver.InfFile) Write-Verbose "[$($MyInvocation.MyCommand.Name)] Ethernet driver: $($Driver.Name) v$($Driver.Version) arch=$($Driver.Architecture) inf=$($Driver.InfFile) infBase=$InfFileWithoutExtension" Write-Verbose "[$($MyInvocation.MyCommand.Name)] Searching DriverStore: $driverStoreRepo\$InfFileWithoutExtension*" $DriverFolder = [System.IO.Directory]::EnumerateDirectories($driverStoreRepo, "$InfFileWithoutExtension*") | Select-Object -First 1 if ($DriverFolder) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Ethernet driver folder found: $DriverFolder" $EthernetDst = [System.IO.Path]::Combine($Script:OSDeployBootAssetsPath, "winpedrivers-$($Driver.Architecture)", "microsoft-windows-ethernet-$($Driver.Version)", $Driver.Name) Write-Host -ForegroundColor DarkGray $EthernetDst Write-Verbose "[$($MyInvocation.MyCommand.Name)] Ethernet destination: $EthernetDst" if (Test-Path "$EthernetDst\*") { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Skipping existing Ethernet driver: $($Driver.Name)-$($Driver.Version)" } else { Write-Verbose "[$($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 "[$($MyInvocation.MyCommand.Name)] Ethernet driver folder NOT found for inf base: $InfFileWithoutExtension" } } } else { Write-Verbose "[$($MyInvocation.MyCommand.Name)] No Ethernet .mum files found in: $packagesPath" } #endregion #region Wi-Fi drivers Write-HostDateTimeDarkGray 'Extracting Wi-Fi drivers ...' $WifiClientMums = if ([System.IO.Directory]::Exists($packagesPath)) { [System.IO.Directory]::GetFiles($packagesPath, 'Microsoft-Windows-Wifi-Client-*.mum') } else { @() } Write-Verbose "[$($MyInvocation.MyCommand.Name)] Wi-Fi .mum files found: $($WifiClientMums.Length)" if ($WifiClientMums.Length -gt 0) { $WifiDrivers = foreach ($mumPath in $WifiClientMums) { Write-Verbose "[$($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') $DriverElement = $MumXml.Root.Descendants($ns + 'driver') | Select-Object -First 1 $DriverInfAttr = if ($DriverElement) { $DriverElement.Attribute('inf') } else { $null } $DriverInf = if ($DriverInfAttr) { $DriverInfAttr.Value } else { $MumXml.Root.Descendants($ns + 'inf') | Select-Object -First 1 | ForEach-Object { $_.Value } } $identityNameAttr = if ($Identity) { $Identity.Attribute('name') } else { $null } $identityVersionAttr = if ($Identity) { $Identity.Attribute('version') } else { $null } $identityArchAttr = if ($Identity) { $Identity.Attribute('processorArchitecture') } else { $null } $identityName = if ($identityNameAttr) { $identityNameAttr.Value } else { $null } $identityVersion = if ($identityVersionAttr) { $identityVersionAttr.Value } else { $null } $identityArch = if ($identityArchAttr) { $identityArchAttr.Value } else { $null } Write-Verbose "[$($MyInvocation.MyCommand.Name)] Wi-Fi Identity.name: $identityName | version: $identityVersion | arch: $identityArch | inf: $DriverInf" if ($Identity -and $DriverInf -and $identityName -and $identityVersion -and $identityArch) { [PSCustomObject]@{ Name = $identityName -replace '^Microsoft-Windows-Wifi-Client-', '' -replace '-FOD-Package$', '' Version = [version]$identityVersion Architecture = $identityArch InfFile = $DriverInf } } else { $skipReason = if ($identityName -like '*-Wrapper') { 'wrapper manifest does not contain a driver element' } elseif (-not $DriverElement) { 'driver element not found' } elseif (-not $DriverInf) { "driver element has no inf attribute; attributes: $($DriverElement.Attributes() -join ', ')" } else { 'missing assembly identity or required identity attributes' } Write-Verbose "[$($MyInvocation.MyCommand.Name)] Wi-Fi .mum skipped - $skipReason" } } # 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 "[$($MyInvocation.MyCommand.Name)] Wi-Fi unique drivers after dedup: $($WifiDrivers.Count)" foreach ($Driver in $WifiDrivers) { $InfFileWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($Driver.InfFile) Write-Verbose "[$($MyInvocation.MyCommand.Name)] Wi-Fi driver: $($Driver.Name) v$($Driver.Version) arch=$($Driver.Architecture) inf=$($Driver.InfFile) infBase=$InfFileWithoutExtension" Write-Verbose "[$($MyInvocation.MyCommand.Name)] Searching DriverStore: $driverStoreRepo\$InfFileWithoutExtension*" $DriverFolder = [System.IO.Directory]::EnumerateDirectories($driverStoreRepo, "$InfFileWithoutExtension*") | Select-Object -First 1 if ($DriverFolder) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Wi-Fi driver folder found: $DriverFolder" $WifiDst = [System.IO.Path]::Combine($Script:OSDeployBootAssetsPath, "winpedrivers-$($Driver.Architecture)", "microsoft-windows-wifi-$($Driver.Version)", $Driver.Name) Write-Host -ForegroundColor DarkGray $WifiDst Write-Verbose "[$($MyInvocation.MyCommand.Name)] Wi-Fi destination: $WifiDst" if (Test-Path "$WifiDst\*") { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Skipping existing Wi-Fi driver: $($Driver.Name)-$($Driver.Version)" } else { Write-Verbose "[$($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 "[$($MyInvocation.MyCommand.Name)] Wi-Fi driver folder NOT found for inf base: $InfFileWithoutExtension" } } } else { Write-Verbose "[$($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-HostDateTimeDarkGray 'Dismounting Windows image ...' $dismountSucceeded = $false foreach ($dismountAttempt in 1..3) { try { Dismount-WindowsImage -Path $MountPath -Discard -ErrorAction Stop | Out-Null $dismountSucceeded = $true break } catch { if ($dismountAttempt -lt 3) { Write-Warning "[$(Get-Date -Format s)] Unable to dismount the Windows image. Retrying in 5 seconds." Start-Sleep -Seconds 5 } } } if (-not $dismountSucceeded) { Write-Warning "[$(Get-Date -Format s)] Close all Windows Explorer windows and pause any antivirus scans that may be accessing the mounted image." Write-Host 'Press any key to retry the dismount ...' [void][Console]::ReadKey($true) Dismount-WindowsImage -Path $MountPath -Discard -ErrorAction Stop | 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-HostDateTimeDarkGray '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-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Windows RE export complete: $DestinationName" Get-Item -Path $DestinationDirectory } } end { #================================================= Write-Verbose "[$($MyInvocation.MyCommand.Name)] End" #================================================= } } |