Public/Get-HDTAdkPath.ps1

function Get-HDTAdkPath {
    <#
        .SYNOPSIS
            Resolves a Windows ADK asset to a path, at runtime, through the
            registry rather than from a literal.
 
        .DESCRIPTION
            PROJECT.md: "Resolve ADK paths at runtime via Get-HDTAdkPath; the
            layout has moved between ADK releases." This is that command, and it
            is the only place in the engine that knows where anything in the ADK
            lives.
 
            THE ROOT COMES FROM THE REGISTRY, IN THIS ORDER, STOPPING AT THE
            FIRST THAT ANSWERS:
 
              1. -Root, when the caller supplies one;
              2. HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed
                 Roots -> KitsRoot10;
              3. HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots ->
                 KitsRoot10, the 32-bit view, for a session that is not
                 WOW-redirected.
 
            THERE IS NO FALLBACK TO A HARDCODED KIT PATH, and this file
            deliberately does not write one down even as an example - the
            contract check for that is a grep. A hardcoded fallback is how
            "resolve at runtime" gets quietly broken:
            it would work on the developer's machine forever and fail in the
            field, which is the failure mode the rule exists to prevent. When no
            key answers, this throws and names both keys.
 
            KitsRoot10 ends with a separator ('...\Windows Kits\10\'). It is
            trimmed, because the build manifest records these strings and a
            doubled separator in a manifest is noise.
 
            EVERY ASSET IS EXISTENCE-CHECKED THROUGH IFileSystem AND A MISS
            THROWS, naming the asset, the path it looked at, and which ADK
            feature installs it - Deployment Tools or the Windows PE add-on,
            which are separate installers and separately forgettable. The class
            is HDTDependencyError, matching ConvertFrom-HDTYaml's missing-parser
            gate, because a missing ADK is the same kind of problem: not the
            administrator's document, the machine's software.
 
            -All never throws for a missing asset. It returns Name / Path /
            Exists for every asset in table order, which is what the boot image
            manifest records and what an operator runs when a build fails.
 
            THE ASSET SET IS CLOSED, exactly as Get-HDTWorkspacePath -Kind is:
            an unknown asset is a defect, not a path.
 
              Root <KitsRoot10>Assessment and Deployment Kit
              DeploymentTools <root>\Deployment Tools\<arch>
              OscdimgDirectory <root>\Deployment Tools\<arch>\Oscdimg
              Oscdimg ...\Oscdimg\oscdimg.exe
              EtfsBoot ...\Oscdimg\etfsboot.com
              EfiSys ...\Oscdimg\efisys.bin
              EfiSysNoPrompt ...\Oscdimg\efisys_noprompt.bin
              WinPeRoot <root>\Windows Preinstallation Environment\<arch>
              WinPeWim <winPeRoot>\<language>\winpe.wim
              WinPeMedia <winPeRoot>\Media
              WinPeOptionalComponent <winPeRoot>\WinPE_OCs
              WinPeOptionalComponentLanguage <winPeRoot>\WinPE_OCs\<language>
 
            efisys_noprompt.bin lives under Deployment Tools\<arch>\Oscdimg, NOT
            under the WinPE add-on's Media\EFI tree, which carries bootloaders
            and no El Torito boot image. This table is where that is enforced.
 
        .PARAMETER Asset
            The ADK asset to resolve. Closed set; see the table above.
 
        .PARAMETER All
            Return every asset as a row of Name, Path and Exists, in table
            order, without throwing for a missing one.
 
        .PARAMETER Architecture
            The target architecture folder. amd64 (default) or arm64. Root is
            the one asset it does not affect.
 
        .PARAMETER Language
            The language folder, used by WinPeWim and
            WinPeOptionalComponentLanguage only. Defaults to en-us.
 
        .PARAMETER Root
            An explicit ADK root, which wins over the registry. For a build host
            pointed at a copied ADK, and for a test that must not read this
            machine's registry.
 
        .PARAMETER Registry
            An IRegistryService. Defaults to the real adapter.
 
        .PARAMETER FileSystem
            An IFileSystem. Defaults to the real adapter.
 
        .PARAMETER SkipExistenceCheck
            Construct the path without checking that it is there. For the unit
            test that proves path CONSTRUCTION independently of what happens to
            be installed, and for a caller building a path on a machine that is
            not the build host.
 
        .INPUTS
            None. This command does not accept pipeline input.
 
        .OUTPUTS
            System.String for -Asset.
            System.Management.Automation.PSCustomObject for -All: Name, Path,
            Exists.
 
        .EXAMPLE
            Get-HDTAdkPath -Asset Oscdimg
 
            The oscdimg.exe that builds the ISO.
 
        .EXAMPLE
            Get-HDTAdkPath -Asset EfiSysNoPrompt
 
            The El Torito image that removes "Press any key to boot from CD or
            DVD". Under Oscdimg, not under Media\EFI.
 
        .EXAMPLE
            Get-HDTAdkPath -Asset WinPeWim -Language en-us
 
            The 340 MB source WinPE the boot image is built from.
 
        .EXAMPLE
            Get-HDTAdkPath -All | Format-Table Name, Exists, Path -AutoSize
 
            What to run when a boot image build says the ADK is incomplete.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Asset')]
    [OutputType([string], ParameterSetName = 'Asset')]
    [OutputType([pscustomobject], ParameterSetName = 'All')]
    param(
        # Closed on purpose: an unknown asset is a defect, not a path.
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Asset')]
        [ValidateSet('Root', 'DeploymentTools', 'OscdimgDirectory', 'Oscdimg', 'EtfsBoot',
            'EfiSys', 'EfiSysNoPrompt', 'WinPeRoot', 'WinPeWim', 'WinPeMedia',
            'WinPeOptionalComponent', 'WinPeOptionalComponentLanguage')]
        [string] $Asset,

        [Parameter(Mandatory = $true, ParameterSetName = 'All')]
        [switch] $All,

        [Parameter()]
        [ValidateSet('amd64', 'arm64')]
        [string] $Architecture = 'amd64',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $Language = 'en-us',

        [Parameter()]
        [string] $Root,

        [Parameter()]
        [AllowNull()]
        [object] $Registry,

        [Parameter()]
        [AllowNull()]
        [object] $FileSystem,

        [Parameter(ParameterSetName = 'Asset')]
        [switch] $SkipExistenceCheck
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    if ($null -eq $Registry) { $Registry = New-HDTRegistryService }
    if ($null -eq $FileSystem) { $FileSystem = New-HDTFileSystem }

    $wowKey = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed Roots'
    $nativeKey = 'HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots'

    # -- the root -------------------------------------------------------------

    $adkRoot = ''

    if (-not [string]::IsNullOrWhiteSpace($Root)) {
        $adkRoot = $Root.TrimEnd('\', '/')
    } else {
        $kitsRoot = ''

        foreach ($key in @($wowKey, $nativeKey)) {
            if (-not [string]::IsNullOrWhiteSpace($kitsRoot)) { continue }

            $value = $Registry.GetValue($key, 'KitsRoot10')
            if ($null -ne $value -and -not [string]::IsNullOrWhiteSpace([string] $value)) {
                $kitsRoot = ([string] $value).TrimEnd('\', '/')
            }
        }

        if ([string]::IsNullOrWhiteSpace($kitsRoot)) {
            $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -ErrorId 'HDTDependencyError' -Category NotInstalled `
                        -TargetObject $wowKey `
                        -Message ("the Windows ADK could not be located: neither '{0}' nor '{1}' carries a KitsRoot10 value. Install the Windows ADK Deployment Tools (and the Windows PE add-on) on this machine, or pass -Root to name an ADK root explicitly." -f $wowKey, $nativeKey)))
        }

        $adkRoot = [System.IO.Path]::Combine($kitsRoot, 'Assessment and Deployment Kit')
    }

    # -- the table ------------------------------------------------------------
    #
    # [IO.Path]::Combine, not Join-Path: Get-HDTWorkspacePath's comment explains
    # why - a path must be constructible for a drive that is not mounted, and an
    # ADK root can name a share or a staged copy this session cannot see.

    $deploymentTools = [System.IO.Path]::Combine($adkRoot, 'Deployment Tools', $Architecture)
    $oscdimgDirectory = [System.IO.Path]::Combine($deploymentTools, 'Oscdimg')
    $winPeRoot = [System.IO.Path]::Combine($adkRoot, 'Windows Preinstallation Environment', $Architecture)
    $optionalComponent = [System.IO.Path]::Combine($winPeRoot, 'WinPE_OCs')

    # Ordered, because -All returns rows in this order and the boot image
    # manifest records them in it.
    $table = [System.Collections.Specialized.OrderedDictionary]::new()
    $table['Root'] = $adkRoot
    $table['DeploymentTools'] = $deploymentTools
    $table['OscdimgDirectory'] = $oscdimgDirectory
    $table['Oscdimg'] = [System.IO.Path]::Combine($oscdimgDirectory, 'oscdimg.exe')
    $table['EtfsBoot'] = [System.IO.Path]::Combine($oscdimgDirectory, 'etfsboot.com')
    $table['EfiSys'] = [System.IO.Path]::Combine($oscdimgDirectory, 'efisys.bin')
    $table['EfiSysNoPrompt'] = [System.IO.Path]::Combine($oscdimgDirectory, 'efisys_noprompt.bin')
    $table['WinPeRoot'] = $winPeRoot
    $table['WinPeWim'] = [System.IO.Path]::Combine($winPeRoot, $Language, 'winpe.wim')
    $table['WinPeMedia'] = [System.IO.Path]::Combine($winPeRoot, 'Media')
    $table['WinPeOptionalComponent'] = $optionalComponent
    $table['WinPeOptionalComponentLanguage'] = [System.IO.Path]::Combine($optionalComponent, $Language)

    # Which ADK installer puts each asset there. Two separate downloads, so an
    # operator who has one and not the other reads the right sentence.
    $feature = @{
        Root                           = 'Deployment Tools'
        DeploymentTools                = 'Deployment Tools'
        OscdimgDirectory               = 'Deployment Tools'
        Oscdimg                        = 'Deployment Tools'
        EtfsBoot                       = 'Deployment Tools'
        EfiSys                         = 'Deployment Tools'
        EfiSysNoPrompt                 = 'Deployment Tools'
        WinPeRoot                      = 'Windows PE add-on'
        WinPeWim                       = 'Windows PE add-on'
        WinPeMedia                     = 'Windows PE add-on'
        WinPeOptionalComponent         = 'Windows PE add-on'
        WinPeOptionalComponentLanguage = 'Windows PE add-on'
    }

    if ($All) {
        $row = New-Object -TypeName System.Collections.ArrayList

        foreach ($name in @($table.Keys)) {
            [void] $row.Add([pscustomobject] @{
                    Name   = [string] $name
                    Path   = [string] $table[$name]
                    Exists = [bool] $FileSystem.TestPath([string] $table[$name])
                })
        }

        return [pscustomobject[]] @($row)
    }

    $path = [string] $table[$Asset]

    if (-not $SkipExistenceCheck) {
        if (-not $FileSystem.TestPath($path)) {
            $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -ErrorId 'HDTDependencyError' -Category ObjectNotFound `
                        -TargetObject $path `
                        -Message ("the ADK asset '{0}' is not at '{1}'. It is installed by the Windows ADK {2}; install that feature, or pass -Root to name the ADK root that has it. Get-HDTAdkPath -All lists every asset and whether it is present." -f $Asset, $path, $feature[$Asset])))
        }
    }

    return $path
}