private/core-operatingsystem/Initialize-ModuleCoreOperatingSystems.ps1
|
function Initialize-ModuleCoreOperatingSystems { <# .SYNOPSIS Gets normalized module core operating system catalog records. .DESCRIPTION Reads operating system catalog XML files from core\operatingsystems under the module root, ProgramData, and OSDCloud catalog directories on mounted drive letters. For each major build, only catalogs with the latest valid build revision are imported. Each PublishedMedia file node is converted into a PowerShell object, excluded metadata properties are removed, and duplicate properties are normalized. Duplicate catalog rows are grouped by FilePath, FileName, LanguageCode, and Architecture, then the preferred row is selected by hash availability. .EXAMPLE Initialize-ModuleCoreOperatingSystems Returns all normalized catalog records discovered in the module core operating systems cache. .EXAMPLE Initialize-ModuleCoreOperatingSystems | Where-Object { $_.LanguageCode -eq 'en-us' } Returns only catalog records for en-us language media. .INPUTS None You cannot pipe input to this function. .OUTPUTS PSCustomObject[] Normalized raw catalog records imported from module XML metadata. .LINK https://github.com/OSDeploy/OSD/tree/master/docs .NOTES Author: David Segura - Recast Software 2026-07-22 - Initial help block created 2026-08-05 - Expanded help content and examples 2026-09-17 - Added external catalog discovery and latest build selection #> [CmdletBinding()] [OutputType([pscustomobject[]])] param () #================================================= # Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] [$($MyInvocation.MyCommand.Name)]" #================================================= $ErrorActionPreference = 'Stop' $Error.Clear() $records = @() $mctRecords = @() $srcRoot = Join-Path $($MyInvocation.MyCommand.Module.ModuleBase) 'core\operatingsystems' $catalogSources = @( [pscustomobject]@{ Path = $srcRoot External = $false } ) $externalCatalogPaths = @() if (-not [string]::IsNullOrWhiteSpace($env:ProgramData)) { $externalCatalogPaths += Join-Path -Path $env:ProgramData -ChildPath 'OSDeployCore\OSDCloud\catalogs\operatingsystems' } $externalCatalogPaths += 'C:\ProgramData\OSDeployCore\OSDCloud\catalogs\operatingsystems' $driveCatalogPaths = Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue | Where-Object { $_.Root -match '^[A-Z]:\\$' } | ForEach-Object { Join-Path -Path $_.Root -ChildPath 'OSDCloud\catalogs\operatingsystems' } $externalCatalogPaths += $driveCatalogPaths foreach ($catalogPath in ($externalCatalogPaths | Sort-Object -Unique)) { $catalogSources += [pscustomobject]@{ Path = $catalogPath External = $true } } $catalogSources = $catalogSources | Group-Object -Property { ([System.IO.Path]::GetFullPath($_.Path)).TrimEnd('\').ToLowerInvariant() } | ForEach-Object { $_.Group | Sort-Object -Property External | Select-Object -First 1 } | Sort-Object -Property Path $catalogFiles = @() foreach ($catalogSource in $catalogSources) { try { if (-not (Test-Path -LiteralPath $catalogSource.Path -PathType Container -ErrorAction Stop)) { continue } Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Searching $($catalogSource.Path)" $sourceFiles = Get-ChildItem -LiteralPath $catalogSource.Path -Filter '*.xml' -Recurse -File -ErrorAction Stop foreach ($file in $sourceFiles) { $catalogFiles += [pscustomobject]@{ File = $file External = $catalogSource.External } } } catch { if ($catalogSource.External) { Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Unable to search external operating system catalogs at '$($catalogSource.Path)': $($_.Exception.Message)" continue } throw } } $catalogFiles = $catalogFiles | Group-Object -Property { ([System.IO.Path]::GetFullPath($_.File.FullName)).ToLowerInvariant() } | ForEach-Object { $_.Group | Sort-Object -Property External | Select-Object -First 1 } | Sort-Object -Property { $_.File.FullName } $catalogCandidates = @() foreach ($catalogFile in $catalogFiles) { $file = $catalogFile.File if ($file.Name -notmatch '^(?<MajorBuild>\d{5})\.(?<Revision>0|[1-9]\d*)\.(?<Timestamp>\d{6}-\d{4})\.xml$') { $message = "Operating system catalog filename '$($file.FullName)' does not match '<major>.<ubr>.<yyMMdd>-<HHmm>.xml'." if ($catalogFile.External) { Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] $message Skipping external catalog." continue } throw $message } $majorBuild = $Matches.MajorBuild $revision = $Matches.Revision try { $xml = [xml](Get-Content -LiteralPath $file.FullName -Raw -ErrorAction Stop) $fileNodes = $xml.MCT.Catalogs.Catalog.PublishedMedia.Files.File if (-not $fileNodes) { $message = "Operating system catalog '$($file.FullName)' does not contain PublishedMedia file records." if ($catalogFile.External) { Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] $message Skipping external catalog." continue } throw $message } $catalogCandidates += [pscustomobject]@{ File = $file FileNodes = $fileNodes MajorBuild = $majorBuild BuildVersion = [version]"$majorBuild.$revision" External = $catalogFile.External } } catch { if ($catalogFile.External) { Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Unable to import external operating system catalog '$($file.FullName)': $($_.Exception.Message)" continue } throw } } $selectedCatalogs = $catalogCandidates | Group-Object -Property MajorBuild | ForEach-Object { $latestBuildVersion = $_.Group | Sort-Object -Property BuildVersion -Descending | Select-Object -First 1 -ExpandProperty BuildVersion $_.Group | Where-Object { $_.BuildVersion -eq $latestBuildVersion } } | Sort-Object -Property MajorBuild, BuildVersion, @{ Expression = { $_.File.FullName } } foreach ($catalog in $selectedCatalogs) { $file = $catalog.File Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Importing $($file.FullName)" $fileNodes = $catalog.FileNodes foreach ($node in ($fileNodes | Sort-Object FileName, LanguageCode, Edition)) { $properties = [ordered]@{ Sha1 = $null Sha256 = $null } $excludedProperties = @('Edition', 'Key', 'Architecture_Loc', 'ArchitectureLoc', 'Edition_Loc', 'EditionLoc', 'IsRetailOnly') foreach ($child in $node.ChildNodes) { if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } $name = $child.LocalName $value = $child.InnerText if ($name -match '^Sha1$') { $name = 'Sha1' } elseif ($name -match '^Sha256$') { $name = 'Sha256' } if ($excludedProperties -contains $name) { continue } if ($properties.Contains($name)) { if ($name -in @('Sha1', 'Sha256') -or [string]::IsNullOrWhiteSpace($properties[$name])) { $properties[$name] = $value } else { $suffix = 2 while ($properties.Contains("$name$suffix")) { $suffix++ } $properties["$name$suffix"] = $value } } else { $properties[$name] = $value } } $properties['OSBuild'] = [string]$catalog.MajorBuild $properties['OSBuildVersion'] = $catalog.BuildVersion.ToString() $mctRecords += [pscustomobject]$properties } } $mctRecords = $mctRecords | Group-Object -Property FilePath, FileName, LanguageCode, Architecture | ForEach-Object { $_.Group | Sort-Object -Property @{ Expression = { [string]::IsNullOrWhiteSpace($_.Sha256) }; Ascending = $true }, @{ Expression = { [string]::IsNullOrWhiteSpace($_.Sha1) }; Ascending = $true } | Select-Object -First 1 } | Sort-Object -Property FilePath, FileName, LanguageCode, Architecture if (-not $mctRecords) { $global:ModuleCoreOperatingSystems = $records # return $records } else { # return $mctRecords $global:ModuleCoreOperatingSystems = $mctRecords } $global:ModuleCoreOperatingSystems | Export-Clixml -Path (Join-Path -Path $env:TEMP -ChildPath 'ModuleCoreOperatingSystems.xml') -Force # Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Ready: ModuleCoreOperatingSystems" } |