Public/Get-HDTBootImageComponent.ps1
|
function Get-HDTBootImageComponent { <# .SYNOPSIS The ordered, dependency-validated, existence-checked list of WinPE optional components a boot image build will apply. .DESCRIPTION Update-HDTBootImage mounts a 340 MB WIM and applies these cabs to it, a run that takes about fifteen minutes and needs elevation. Every decision that can be made before that run is made here, against injected services, in milliseconds - which cabs, in what order, whether each one is actually on disk, and whether the admin's declaration is internally consistent. THE REQUIRED SET, AND ITS ORDER, IS THE BOOT-VERIFIED ONE: WinPE-WMI -> WinPE-NetFx -> WinPE-Scripting -> WinPE-PowerShell -> WinPE-StorageWMI -> WinPE-DismCmdlets THAT ORDER WAS VERIFIED BY A MACHINE THAT BOOTED. DO NOT SORT IT and do not "tidy" it alphabetically. Note WinPE-NetFx - LOWERCASE x. An earlier draft capitalised that x, and no cab of the capitalised name exists. The misspelling is written nowhere in this repository's source, deliberately, so that a grep for it stays a usable check. UNSET AND SET-TO-NOTHING ARE DIFFERENT INSTRUCTIONS. Omitting -OptionalComponent means "the admin did not say", and takes the defaults - WinPE-SecureStartup, WinPE-EnhancedStorage, WinPE-WDS-Tools. Passing an explicit empty array means "the six and nothing else", and is honoured. MERGE RULES: - the six always come first, in their order, whatever was declared; - a declared component that is already required is dropped from the tail rather than applied twice, and does not reorder the six; - the remaining declarations keep the order they were written in ("merged with the required set below, order preserved"); - comparison is case-insensitive throughout. DEPENDENCY VALIDATION comes from Get-HDTBootImageComponentDependency, whose every row is transcribed from the component's own package manifest inside the ADK cab. A component NOT in that table is allowed with no dependencies and no warning - the point is that a fleet needs components this project never anticipated. A component IN the table whose dependency is absent from the final list is REFUSED, naming both and telling the admin to add it. HDT does not insert it silently: an admin who is told gets a boot image they understand. -Dependency IS NOT A TEST HOOK. It is the same injection every other decision in this engine takes, it defaults to the shipped table, and it is what lets the RULE be proven against rows nobody has to believe while the shipped TABLE is proven separately by its own provenance test. FILESYSTEM CONTACT, AND ONLY THIS: - CabPath must exist, or this throws naming the component, the path and the component root. A build that applies a cab which is not there fails fifteen minutes in, with a WIM mounted. - LanguageCabPath is PROBED. Absent, the property is '' and one warning names the component. Twelve of this ADK's 33 components have no en-us pack - WinPE-FMAPI among them - so this must never be an error ("the builder must probe rather than assume"). .PARAMETER OptionalComponent The components declared in workspace.yaml, or on Update-HDTBootImage -OptionalComponent. Omit for the defaults; pass @() for none. .PARAMETER ComponentRoot The WinPE_OCs folder, from Get-HDTAdkPath -Asset WinPeOptionalComponent. .PARAMETER Language The language pack folder under the component root. Defaults to en-us. .PARAMETER FileSystem An IFileSystem. Defaults to the real adapter. .PARAMETER Dependency The dependency table. Defaults to Get-HDTBootImageComponentDependency. .INPUTS None. This command does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject, one per component, in the order they must be applied: Order [int] 1-based Name [string] WinPE-WMI Required [bool] true for the six CabPath [string] <root>\WinPE-WMI.cab LanguageCabPath [string] <root>\en-us\WinPE-WMI_en-us.cab, or '' .EXAMPLE Get-HDTBootImageComponent -ComponentRoot (Get-HDTAdkPath -Asset WinPeOptionalComponent) The nine components a default HDT boot image carries. .EXAMPLE $workspace = Import-HDTWorkspaceDocument -Path $path -FileSystem $fs Get-HDTBootImageComponent -OptionalComponent $workspace.BootImage.OptionalComponent ` -ComponentRoot (Get-HDTAdkPath -Asset WinPeOptionalComponent) ` -Language $workspace.BootImage.Language -FileSystem $fs What Update-HDTBootImage does: the document decides, this plans. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter()] [AllowNull()] [AllowEmptyCollection()] [string[]] $OptionalComponent, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $ComponentRoot, [Parameter()] [ValidateNotNullOrEmpty()] [string] $Language = 'en-us', [Parameter()] [AllowNull()] [object] $FileSystem, [Parameter()] [AllowNull()] [hashtable] $Dependency ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if ($null -eq $FileSystem) { $FileSystem = New-HDTFileSystem } if ($null -eq $Dependency) { $Dependency = Get-HDTBootImageComponentDependency } # SPIKES S1, verified by a machine that booted. Do not sort this. $requiredComponent = @('WinPE-WMI', 'WinPE-NetFx', 'WinPE-Scripting', 'WinPE-PowerShell', 'WinPE-StorageWMI', 'WinPE-DismCmdlets') # DESIGN 5.1's defaults, applied only when the caller said nothing at all. $defaultOptionalComponent = @('WinPE-SecureStartup', 'WinPE-EnhancedStorage', 'WinPE-WDS-Tools') $declared = $defaultOptionalComponent if ($PSBoundParameters.ContainsKey('OptionalComponent')) { $declared = @($OptionalComponent) } # -- the merge ------------------------------------------------------------ $plan = New-Object -TypeName System.Collections.ArrayList $seen = New-Object -TypeName System.Collections.ArrayList foreach ($name in $requiredComponent) { [void] $plan.Add([pscustomobject] @{ Name = $name; Required = $true }) [void] $seen.Add($name.ToLowerInvariant()) } foreach ($name in @($declared)) { if ([string]::IsNullOrWhiteSpace($name)) { continue } $comparable = $name.ToLowerInvariant() if ($seen -contains $comparable) { continue } [void] $plan.Add([pscustomobject] @{ Name = $name; Required = $false }) [void] $seen.Add($comparable) } # -- dependency validation ------------------------------------------------ # # Against the FINAL list, so a dependency declared later in the file still # satisfies one declared earlier - the admin's ordering of the tail is their # business, and refusing on it would be refusing a build that works. $planned = @($plan | ForEach-Object { $_.Name }) foreach ($item in $plan) { if (-not $Dependency.ContainsKey($item.Name)) { continue } foreach ($requirement in @($Dependency[$item.Name].Requires)) { if ($seen -contains ([string] $requirement).ToLowerInvariant()) { continue } $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $item.Name ` -Message ("the WinPE component '{0}' requires '{1}', which is not in this boot image. Add '{1}' to optionalComponents in workspace.yaml, before or after '{0}' - HDT will not add it for you, because a boot image should hold what its administrator asked for. The components planned are: {2}. Source for this dependency: {3}" -f $item.Name, $requirement, ($planned -join ', '), $Dependency[$item.Name].Source))) } } # -- the paths ------------------------------------------------------------ $row = New-Object -TypeName System.Collections.ArrayList $order = 0 foreach ($item in $plan) { $order++ $cabPath = [System.IO.Path]::Combine($ComponentRoot, ('{0}.cab' -f $item.Name)) if (-not $FileSystem.TestPath($cabPath)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $cabPath -Category ObjectNotFound ` -Message ("the WinPE component '{0}' has no cab at this path. It is not among the components '{1}' carries - check the spelling (note WinPE-NetFx has a lowercase x) or install the matching Windows PE add-on." -f $item.Name, $ComponentRoot))) } $languageCabPath = [System.IO.Path]::Combine($ComponentRoot, $Language, ('{0}_{1}.cab' -f $item.Name, $Language)) if (-not $FileSystem.TestPath($languageCabPath)) { # NOT an error. WinPE-FMAPI and eleven others ship no language pack # in this ADK, and DESIGN 5.1 says to probe rather than assume. Write-Warning ("The WinPE component '{0}' has no {1} language pack in '{2}'. The component will be applied without one." -f $item.Name, $Language, $ComponentRoot) $languageCabPath = '' } [void] $row.Add([pscustomobject] @{ Order = $order Name = [string] $item.Name Required = [bool] $item.Required CabPath = $cabPath LanguageCabPath = $languageCabPath }) } return [pscustomobject[]] @($row) } |