public/Build-OSDeployBootMedia.ps1

#Requires -PSEdition Core
#Requires -Version 7.4

function Build-OSDeployBootMedia {
    <#
    .SYNOPSIS
        Builds a customized WinPE boot image from a WinRE or WinPE source.
 
    .DESCRIPTION
        Creates a bootable WinPE media from an imported WinRE image or Windows ADK WinPE.
        Applies ADK optional components, drivers, PowerShell updates, applications,
        console settings, wallpaper, and user scripts.
 
        Output is written to %ProgramData%\OSDeployCore\boot-media.
 
    .PARAMETER Name
        Specifies a friendly name for the build.
 
    .PARAMETER Architecture
        Processor architecture: 'amd64' or 'arm64'. Default is 'amd64'.
 
    .PARAMETER Languages
        Windows ADK language packs to add. Default is 'en-us'.
 
    .PARAMETER SetAllIntl
        Sets all international settings. Default is 'en-us'.
 
    .PARAMETER SetInputLocale
        Sets the default input locale. Default is 'en-us'.
 
    .PARAMETER SetTimeZone
        Sets the WinPE timezone. Default is the current system timezone.
 
    .PARAMETER UpdateUSB
        Copies the completed media to any USB partition labeled 'USB-WinPE'.
 
    .PARAMETER SkipAdkPackages
        Skips adding ADK optional component packages. Useful for quick testing.
 
    .PARAMETER UseAdkWinPE
        Uses the Windows ADK winpe.wim instead of an imported WinRE source.
 
    .EXAMPLE
        Build-OSDeployBootMedia -Name 'MyPE'
 
    .EXAMPLE
        Build-OSDeployBootMedia -Name 'TestBuild' -SkipAdkPackages
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        None. Results are written to %ProgramData%\OSDeployCore\boot-media.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Requires: Windows 11 25H2+, PowerShell 7.6, Windows ADK, Run as Administrator
    #>


    [CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess)]
    param (
        [Parameter(Mandatory)]
        [System.String]
        $Name,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(Mandatory, ParameterSetName = 'ADK')]
        [ValidateSet('amd64', 'arm64')]
        [System.String]
        $Architecture,

        [ValidateSet(
            '*', 'ar-sa', 'bg-bg', 'cs-cz', 'da-dk', 'de-de', 'el-gr',
            'en-gb', 'en-us', 'es-es', 'es-mx', 'et-ee', 'fi-fi',
            'fr-ca', 'fr-fr', 'he-il', 'hr-hr', 'hu-hu', 'it-it',
            'ja-jp', 'ko-kr', 'lt-lt', 'lv-lv', 'nb-no', 'nl-nl',
            'pl-pl', 'pt-br', 'pt-pt', 'ro-ro', 'ru-ru', 'sk-sk',
            'sl-si', 'sr-latn-rs', 'sv-se', 'th-th', 'tr-tr',
            'uk-ua', 'zh-cn', 'zh-tw'
        )]
        [System.String[]]
        $Languages,

        [System.String]
        $SetAllIntl,

        [System.String]
        $SetInputLocale,

        [ValidateScript({
            $tz = (tzutil /l)
            $validOptions = foreach ($t in $tz) {
                if (($tz.IndexOf($t) - 1) % 3 -eq 0) {
                    $t.Trim()
                }
            }
            $validOptions -contains $_
        })]
        [System.String]
        $SetTimeZone = (tzutil /g),

        [System.Management.Automation.SwitchParameter]
        $SkipAdkPackages,

        [Parameter(Mandatory, ParameterSetName = 'ADK')]
        [System.Management.Automation.SwitchParameter]
        $UseAdkWinPE,

        [System.Management.Automation.SwitchParameter]
        $UpdateUSB
    )

    Write-OSDeployBanner
    #region Prerequisites
    # Requires Run as Administrator
    $IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
    if (-not $IsAdmin) {
        Write-Warning "Build-OSDeployBootMedia must be Run as Administrator"
        return
    }

    # Requires Windows 11
    $osVersion = [System.Environment]::OSVersion.Version
    if ($osVersion.Build -lt 26100) {
        Write-Warning "Build-OSDeployBootMedia requires Windows 11 25H2 or later (Build 26100+)"
        return
    }

    # Requires OSDCloud module 26.4.17.1 or newer
    $RequiredOSDCloudVersion = [System.Version]'26.4.17.1'
    if (-not (Get-Command -Name 'Get-OSDCloudModuleVersion' -ErrorAction SilentlyContinue)) {
        Write-Warning "Build-OSDeployBootMedia requires OSDCloud module $RequiredOSDCloudVersion or newer. OSDCloud module is not loaded."
        return
    }
    $OSDCloudVersion = Get-OSDCloudModuleVersion
    if ($OSDCloudVersion -lt $RequiredOSDCloudVersion) {
        Write-Warning "Build-OSDeployBootMedia requires OSDCloud module $RequiredOSDCloudVersion or newer. Loaded version: $OSDCloudVersion"
        return
    }

    # Ensure directory structure exists
    Initialize-OSDeployCoreBootImage

    $BuildDateTime = (Get-Date).ToString('yyMMdd-HHmm')
    Write-OSDeployCoreProgress "Starting Build-OSDeployBootMedia"
    #endregion

    #region TLS and Proxy
    $PSDefaultParameterValues['Invoke-WebRequest:UseBasicParsing'] = $true
    $currentProgressPref = $ProgressPreference
    $ProgressPreference = 'SilentlyContinue'

    $regProxy = Get-ItemProperty -Path 'Registry::HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction SilentlyContinue
    if ($regProxy -and $regProxy.PSObject.Properties['ProxyServer'] -and $regProxy.ProxyServer -and -not ([System.Net.WebRequest]::DefaultWebProxy).Address -and $regProxy.ProxyEnable) {
        [System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy $regProxy.ProxyServer
        [System.Net.WebRequest]::DefaultWebProxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials
    }

    $currentVersionTls = [Net.ServicePointManager]::SecurityProtocol
    $currentSupportableTls = [Math]::Max($currentVersionTls.value__, [Net.SecurityProtocolType]::Tls.value__)
    $availableTls = [enum]::GetValues('Net.SecurityProtocolType') | Where-Object { $_ -gt $currentSupportableTls }
    $availableTls | ForEach-Object {
        [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor $_
    }
    #endregion

    #region ADK Detection
    $AdkInfo = Get-WindowsAdkInstallInfo
    if (-not $AdkInfo.IsInstalled) {
        Write-Warning "Windows ADK is not installed"
        Write-Warning "Install the Windows ADK from https://learn.microsoft.com/en-us/windows-hardware/get-started/adk-install"
        return
    }

    $AdkRootPath = $AdkInfo.InstallPath
    Write-OSDeployCoreProgress "Windows ADK version $($AdkInfo.InstallVersion) at $AdkRootPath"

    #region Select Boot Image Source
    if ($UseAdkWinPE) {
        $WimSourceType = 'WinPE'
    }
    else {
        $WimSourceType = 'WinRE'
        if ($Architecture) {
            $GetWindowsImage = Select-OSDeployCoreWinRESource -Architecture $Architecture
        }
        else {
            $GetWindowsImage = Select-OSDeployCoreWinRESource
        }

        if (-not $GetWindowsImage -or $GetWindowsImage.Count -eq 0) {
            Write-Warning "No WinRE source selected. Defaulting to Windows ADK WinPE."
            $WimSourceType = 'WinPE'
            if (-not $Architecture) {
                $Architecture = ($env:PROCESSOR_ARCHITECTURE).ToLower() -replace 'x86_', ''
                if ($Architecture -notin @('amd64', 'arm64')) {
                    Write-Warning "Unsupported architecture '$Architecture'. Only 'amd64' and 'arm64' are supported."
                    return
                }
            }
        }
        else {
            $Architecture = $GetWindowsImage.Architecture
            $ImportImageCorePath = Join-Path $GetWindowsImage.Path '.core'
            $ImportImageOSFilesPath = Join-Path $GetWindowsImage.Path '.core' 'os-files'

            Write-OSDeployCoreProgress "Using Recovery Image at $($GetWindowsImage.ImagePath)"
        }
    }
    #endregion

    #region ADK Paths
    if (($Architecture -ne 'amd64') -and ($Architecture -ne 'arm64')) {
        Write-Warning "Unknown architecture: $Architecture"
        return
    }

    $WindowsAdkPaths = Get-WindowsAdkPaths -Architecture $Architecture -AdkRoot $AdkRootPath
    if (-not $WindowsAdkPaths) {
        Write-Warning "Unable to resolve Windows ADK paths for architecture $Architecture"
        return
    }

    if ($WimSourceType -eq 'WinPE') {
        $GetWindowsImage = Get-WindowsImage -ImagePath $WindowsAdkPaths.WimSourcePath -Index 1
        $ImportImageWimPath = $GetWindowsImage.ImagePath
        $sourceVersion = $GetWindowsImage.Version.ToString()
    }
    elseif ($WimSourceType -eq 'WinRE') {
        $ImportImageWimPath = $GetWindowsImage.ImagePath
        $sourceVersion = $GetWindowsImage.Version
    }

    # Build the media name as {build}.{revision}-{architecture}-{name}
    $versionParts = $sourceVersion.Split('.')
    $trimmedVersion = if ($versionParts.Count -ge 4) {
        "$($versionParts[2]).$($versionParts[3])"
    }
    else {
        $sourceVersion
    }
    $MediaName = "$trimmedVersion-$Architecture-$Name"
    $MediaIsoLabel = "$trimmedVersion-$Name"

    $WindowsAdkPaths.WimSourcePath = $ImportImageWimPath
    #endregion

    #region Build Paths
    $BuildsPath = Join-Path $Script:OSDeployCorePath 'boot-media'

    # Handle duplicate build names by appending -001, -002, etc.
    $MediaRootPath = Join-Path $BuildsPath $MediaName
    if (Test-Path -Path $MediaRootPath) {
        $suffix = 1
        do {
            $candidateName = '{0}-{1:d3}' -f $MediaName, $suffix
            $candidatePath = Join-Path $BuildsPath $candidateName
            $suffix++
        } while (Test-Path -Path $candidatePath)
        $MediaName = $candidateName
        $MediaRootPath = $candidatePath
    }
    $CorePath = Join-Path $MediaRootPath '.core'
    $TempPath = Join-Path $MediaRootPath '.temp'
    $LogsPath = Join-Path $TempPath 'logs'
    $MediaPath = Join-Path $MediaRootPath 'bootmedia'
    $SourcesPath = Join-Path $MediaPath 'sources'
    #endregion

    #region Select Profile, Drivers, Scripts
    $MyBuildProfile = Select-OSDeployCoreBuildProfile

    if ($MyBuildProfile) {
        $BuildProfile = Get-Content $MyBuildProfile.FullName -Raw | ConvertFrom-Json
        $WinPEDriver         = Expand-OSDeployBuildProfileToken $BuildProfile.WinPEDriver
        if ($WimSourceType -eq 'WinPE' -and $WinPEDriver) {
            Write-OSDeployCoreProgress 'ADK WinPE does not support wireless hardware - excluding Wi-Fi drivers'
            $WinPEDriver = $WinPEDriver | Where-Object { (Split-Path $_ -Leaf) -notmatch 'wifi|wireless' }
        }
        $WinPEAppScript      = Expand-OSDeployBuildProfileToken $BuildProfile.WinPEAppScript
        $WinPEScript         = Expand-OSDeployBuildProfileToken $BuildProfile.WinPEScript
        $WinPEMediaScript    = Expand-OSDeployBuildProfileToken $BuildProfile.WinPEMediaScript
        $WinPEStartupProfile = Expand-OSDeployBuildProfileToken $BuildProfile.WinPEStartupProfile
        $WinPECustomWallpaper = Expand-OSDeployBuildProfileToken $BuildProfile.WinPECustomWallpaper
        [System.String[]]$Languages = $BuildProfile.Languages
        $SetAllIntl = $BuildProfile.SetAllIntl
        $SetInputLocale = $BuildProfile.SetInputLocale
        $SetTimeZone = $BuildProfile.SetTimeZone
        $MyBuildProfilePath = $MyBuildProfile.FullName
    }
    else {
        $skipWifi = $WimSourceType -eq 'WinPE'
        if ($skipWifi) {
            Write-OSDeployCoreProgress 'ADK WinPE does not support wireless hardware - excluding Wi-Fi drivers'
        }
        $OSDWorkspaceWinPEDriver = Select-OSDeployCoreBuildDriver -Architecture $Architecture -SkipWifiDrivers:$skipWifi
        if ($OSDWorkspaceWinPEDriver) {
            $WinPEDriver = $OSDWorkspaceWinPEDriver | Select-Object -ExpandProperty FullName
        }
        else {
            $WinPEDriver = $null
        }

        $OSDWorkspaceWinPEScript = @()
        $result = Select-OSDeployCoreBuildScript
        if ($null -ne $result) { $OSDWorkspaceWinPEScript = @($result) }
        $WinPEAppScript = $null
        $WinPEScript = $null
        $WinPEMediaScript = $null
        $WinPEStartupProfile = $null

        if ($OSDWorkspaceWinPEScript | Where-Object { $_.Type -eq 'winpe-appscript' }) {
            $WinPEAppScript = $OSDWorkspaceWinPEScript | Where-Object { $_.Type -eq 'winpe-appscript' } | Select-Object -ExpandProperty FullName
        }
        if ($OSDWorkspaceWinPEScript | Where-Object { $_.Type -eq 'winpe-script' }) {
            $WinPEScript = $OSDWorkspaceWinPEScript | Where-Object { $_.Type -eq 'winpe-script' } | Select-Object -ExpandProperty FullName
        }
        if ($OSDWorkspaceWinPEScript | Where-Object { $_.Type -eq 'media-script' }) {
            $WinPEMediaScript = $OSDWorkspaceWinPEScript | Where-Object { $_.Type -eq 'media-script' } | Select-Object -ExpandProperty FullName
        }

        $OSDWorkspaceStartupProfile = Select-OSDeployCoreBuildStartupProfile
        if ($OSDWorkspaceStartupProfile) {
            $WinPEStartupProfile = $OSDWorkspaceStartupProfile | Select-Object -ExpandProperty FullName
        }

        $WinPECustomWallpaper = $null
        $wallpaperRoots = @($Script:OSDeployCoreRepositoryPath, (Join-Path $Script:OSDeployModuleBase 'core' 'OSDRe'))
        $osdCloudBase = if (Get-Command -Name 'Get-OSDCloudModulePath' -ErrorAction SilentlyContinue) { Get-OSDCloudModulePath }
        if ($osdCloudBase) { $wallpaperRoots += Join-Path $osdCloudBase 'core' 'OSDRe' }
        $osdBase = if (Get-Command -Name 'Get-OSDModulePath' -ErrorAction SilentlyContinue) { Get-OSDModulePath }
        if ($osdBase) { $wallpaperRoots += Join-Path $osdBase 'core' 'OSDRe' }

        $WallpaperFiles = @()
        foreach ($root in $wallpaperRoots) {
            $wpPath = Join-Path $root 'winpe-wallpaper'
            if (Test-Path -LiteralPath $wpPath) {
                $WallpaperFiles += Get-ChildItem -LiteralPath $wpPath -Filter '*.jpg' -File -ErrorAction SilentlyContinue
            }
        }
        $WallpaperFiles = @($WallpaperFiles | Sort-Object FullName -Unique)

        if ($WallpaperFiles.Count -eq 1) {
            $WinPECustomWallpaper = $WallpaperFiles[0].FullName
        }
        elseif ($WallpaperFiles.Count -gt 1) {
            $SelectedWallpaper = $WallpaperFiles | Select-Object Name, FullName | Out-GridView -Title 'Select WinPE Wallpaper' -OutputMode Single
            if ($SelectedWallpaper) {
                $WinPECustomWallpaper = $SelectedWallpaper.FullName
            }
        }

        $BuildProfile = [ordered]@{
            Architecture     = [System.String]$Architecture
            WinPEDriver          = ConvertTo-OSDeployBuildProfileToken $WinPEDriver
            WinPEAppScript       = ConvertTo-OSDeployBuildProfileToken $WinPEAppScript
            WinPEScript          = ConvertTo-OSDeployBuildProfileToken $WinPEScript
            WinPEMediaScript     = ConvertTo-OSDeployBuildProfileToken $WinPEMediaScript
            WinPEStartupProfile  = ConvertTo-OSDeployBuildProfileToken $WinPEStartupProfile
            WinPECustomWallpaper = ConvertTo-OSDeployBuildProfileToken $WinPECustomWallpaper
            Languages            = [System.String[]]$Languages
            SetAllIntl       = [System.String]$SetAllIntl
            SetInputLocale   = [System.String]$SetInputLocale
            SetTimeZone      = [System.String]$SetTimeZone
        }

        $BuildProfileCachePath = Join-Path $script:OSDeployCoreRepositoryPath 'build-profiles'
        if (-not (Test-Path $BuildProfileCachePath)) {
            New-Item -Path $BuildProfileCachePath -ItemType Directory -Force | Out-Null
        }
        $MyBuildProfilePath = Join-Path $BuildProfileCachePath "$Name.json"

        Write-OSDeployCoreProgress "Exporting Build Profile to $MyBuildProfilePath"
        $BuildProfile | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue | Out-File $MyBuildProfilePath -Encoding utf8 -Force
    }
    #endregion

    #region BuildMedia
    $global:BuildMedia = [ordered]@{
        AdkRootPath       = $AdkRootPath
        AdkPaths          = $WindowsAdkPaths
        SkipAdkPackages = [System.Boolean]$SkipAdkPackages
        Architecture      = [System.String]$Architecture
        BuildProfile      = $MyBuildProfilePath
        ContentStartnet   = [System.String]''
        ContentWinpeshl   = [System.String]''
        CorePath          = $CorePath
        InstalledApps     = @()
        ImportImageWimPath = $ImportImageWimPath
        Languages         = [System.String[]]$Languages
        LogsPath          = $LogsPath
        MediaIsoLabel     = $MediaIsoLabel
        MediaName         = $MediaName
        MediaPath         = $MediaPath
        MediaPathEX       = $null
        MediaRootPath     = $MediaRootPath
        MountPath         = $null
        Name              = [System.String]$Name
        SetAllIntl        = [System.String]$SetAllIntl
        SetInputLocale    = [System.String]$SetInputLocale
        SetTimeZone       = [System.String]$SetTimeZone
        SourcesPath       = $SourcesPath
        SourcesPathEX     = $null
        UpdateUSB         = [System.Boolean]$UpdateUSB
        WimSourceType     = $WimSourceType
        WindowsImage      = $null
        WinPEAppScript    = $WinPEAppScript
        WinPEAppsPath     = Join-Path $Script:OSDeployCorePath 'cache' 'winpe-apps'
        WinPEDriver           = $WinPEDriver
        WinPEMediaScript      = $WinPEMediaScript
        WinPEScript           = $WinPEScript
        WinPEStartupProfile   = $WinPEStartupProfile
        WinPECustomWallpaper  = $WinPECustomWallpaper
    }
    #endregion

    #region Point of No Return
    Write-OSDeployCoreProgress 'Build Configuration'
    $global:BuildMedia | Out-Host
    Write-Host -ForegroundColor DarkCyan 'Press CTRL+C to cancel'
    pause
    $BuildStartTime = Get-Date
    #endregion

    #region Create Build Directories
    if (-not $PSCmdlet.ShouldProcess($MediaRootPath, 'Create build directories')) {
        return
    }

    foreach ($dir in @($MediaRootPath, $CorePath, $TempPath, $LogsPath)) {
        if (-not (Test-Path $dir)) {
            New-Item -Path $dir -ItemType Directory -Force | Out-Null
        }
    }

    $Transcript = "$((Get-Date).ToString('yyMMdd-HHmmss'))-Build-BootImage.log"
    Start-Transcript -Path (Join-Path $LogsPath $Transcript) -ErrorAction SilentlyContinue
    #endregion

    #region Hydrate Core Metadata
    if ($WimSourceType -eq 'WinRE' -and $ImportImageCorePath -and (Test-Path $ImportImageCorePath)) {
        Write-OSDeployCoreProgress "Hydrate $CorePath"
        $null = robocopy.exe "$ImportImageCorePath" "$CorePath" *.json /nfl /ndl /np /r:0 /w:0 /xj /mt:128 /LOG+:"$LogsPath\core.log"
        $null = robocopy.exe "$ImportImageCorePath" "$CorePath" *.xml /nfl /ndl /np /r:0 /w:0 /xj /mt:128 /LOG+:"$LogsPath\core.log"
    }

    $ImportId = @{ id = $MediaName }
    if (-not (Test-Path $CorePath)) {
        New-Item -Path $CorePath -ItemType Directory -Force | Out-Null
    }
    $ImportId | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue | Out-File "$CorePath\id.json" -Encoding utf8 -Force
    #endregion

    #region Hydrate Media
    Write-OSDeployCoreProgress "Hydrate $MediaPath"
    $null = robocopy.exe "$($WindowsAdkPaths.PathWinPEMedia)" "$MediaPath" *.* /mir /b /ndl /np /r:0 /w:0 /xj /njs /mt:128 /LOG+:"$LogsPath\media.log"

    if ($WimSourceType -eq 'WinRE' -and $ImportImageCorePath) {
        Copy-Item -Path "$ImportImageCorePath\os-boot\DVD\EFI\en-US\efisys.bin" -Destination "$MediaPath\EFI\Microsoft\Boot\efisys.bin" -Force -ErrorAction SilentlyContinue
        Copy-Item -Path "$ImportImageCorePath\os-boot\DVD\EFI\en-US\efisys_noprompt.bin" -Destination "$MediaPath\EFI\Microsoft\Boot\efisys_noprompt.bin" -Force -ErrorAction SilentlyContinue

        $Fonts = @('malgunn_boot.ttf', 'meiryon_boot.ttf', 'msjhn_boot.ttf', 'msyhn_boot.ttf', 'segoen_slboot.ttf')
        foreach ($Font in $Fonts) {
            if (Test-Path "$ImportImageCorePath\os-boot\Fonts\$Font") {
                Copy-Item -Path "$ImportImageCorePath\os-boot\Fonts\$Font" -Destination "$MediaPath\EFI\Microsoft\Boot\Fonts\$Font" -Force -ErrorAction SilentlyContinue
            }
        }
    }
    #endregion

    #region Build MediaEX (BlackLotus CVE-2022-21894 Mitigation)
    $MediaPathEX = $null
    if ($WimSourceType -eq 'WinRE' -and $ImportImageCorePath -and (Test-Path "$ImportImageCorePath\os-boot\EFI_EX")) {
        $MediaPathEX = Join-Path $MediaRootPath 'bootmedia-ca2023'
        $global:BuildMedia.MediaPathEX = $MediaPathEX

        Write-OSDeployCoreProgress "Hydrate $MediaPathEX"
        $null = robocopy.exe "$($WindowsAdkPaths.PathWinPEMedia)" "$MediaPathEX" *.* /mir /b /ndl /np /r:0 /w:0 /xj /mt:128 /LOG+:"$LogsPath\mediaex.log"

        Write-OSDeployCoreProgress 'Mitigate CVE-2022-21894 Secure Boot Security Feature Bypass Vulnerability'
        Remove-Item -Path "$MediaPathEX\EFI\Microsoft\Boot\Fonts" -Recurse -Force -ErrorAction SilentlyContinue
        if (-not (Test-Path "$MediaPathEX\EFI\Microsoft\Boot\Fonts")) {
            New-Item -Path "$MediaPathEX\EFI\Microsoft\Boot\Fonts" -ItemType Directory -Force | Out-Null
        }

        $ExFiles = @(
            @{ Src = "$ImportImageCorePath\os-boot\EFI_EX\bootmgr_ex.efi"; Dst = "$MediaPathEX\bootmgr.efi" },
            @{ Src = "$ImportImageCorePath\os-boot\EFI_EX\bootmgfw_ex.efi"; Dst = "$MediaPathEX\EFI\Boot\bootx64.efi" }
        )
        foreach ($f in $ExFiles) {
            Copy-Item -Path $f.Src -Destination $f.Dst -Force -ErrorAction SilentlyContinue
        }

        $ExFonts = @(
            'chs_boot', 'cht_boot', 'jpn_boot', 'kor_boot',
            'malgun_boot', 'malgunn_boot', 'meiryo_boot', 'meiryon_boot',
            'msjh_boot', 'msjhn_boot', 'msyh_boot', 'msyhn_boot',
            'segmono_boot', 'segoe_slboot', 'segoen_slboot', 'wgl4_boot'
        )
        foreach ($fontBase in $ExFonts) {
            $srcFont = "$ImportImageCorePath\os-boot\Fonts_EX\${fontBase}_EX.ttf"
            $dstFont = "$MediaPathEX\EFI\Microsoft\Boot\Fonts\$fontBase.ttf"
            Copy-Item -Path $srcFont -Destination $dstFont -Force -ErrorAction SilentlyContinue
        }

        Copy-Item -Path "$ImportImageCorePath\os-boot\DVD_EX\EFI\en-US\efisys_EX.bin" -Destination "$MediaPathEX\EFI\Microsoft\Boot\efisys.bin" -Force -ErrorAction SilentlyContinue
        Copy-Item -Path "$ImportImageCorePath\os-boot\DVD_EX\EFI\en-US\efisys_noprompt_EX.bin" -Destination "$MediaPathEX\EFI\Microsoft\Boot\efisys_noprompt.bin" -Force -ErrorAction SilentlyContinue
    }
    #endregion

    #region Build Sources (boot.wim)
    if (-not (Test-Path $SourcesPath)) {
        New-Item -Path $SourcesPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
    }
    $bootWimPath = Join-Path $SourcesPath 'boot.wim'
    Write-OSDeployCoreProgress "Hydrate $bootWimPath"
    Copy-Item -Path $WindowsAdkPaths.WimSourcePath -Destination $bootWimPath -Force -ErrorAction Stop | Out-Null

    if (-not (Test-Path $bootWimPath)) {
        Write-Warning "Failed to copy boot.wim to $bootWimPath"
        Stop-Transcript
        return
    }
    attrib -s -h -r $SourcesPath
    attrib -s -h -r $bootWimPath

    if ($MediaPathEX) {
        $SourcesPathEX = Join-Path $MediaPathEX 'sources'
        $global:BuildMedia.SourcesPathEX = $SourcesPathEX
        if (-not (Test-Path $SourcesPathEX)) {
            New-Item -Path $SourcesPathEX -ItemType Directory -Force -ErrorAction Stop | Out-Null
        }
        attrib -s -h -r $SourcesPathEX
    }
    #endregion

    #region Mount Image
    Write-OSDeployCoreProgress 'Mount Windows Image'
    $MountPath = Join-Path $TempPath 'mount'
    if (-not (Test-Path $MountPath)) {
        New-Item -Path $MountPath -ItemType Directory -Force | Out-Null
    }

    $CurrentLog = "$LogsPath\$((Get-Date).ToString('yyMMdd-HHmmss'))-Mount-WindowsImage.log"
    $WindowsImage = Mount-WindowsImage -ImagePath $bootWimPath -Index 1 -Path $MountPath -LogPath $CurrentLog
    $global:BuildMedia.MountPath = $MountPath
    $global:BuildMedia.WindowsImage = $WindowsImage
    Write-OSDeployCoreProgress "MountPath: $MountPath"
    #endregion

    #region Registry Information
    Write-OSDeployCoreProgress 'Get WinPE Registry CurrentVersion'
    $softwareHivePath = Join-Path $MountPath 'Windows\System32\Config\SOFTWARE'
    if (Test-Path $softwareHivePath) {
        $hiveName = 'OSDeployCoreBoot'
        try {
            reg.exe LOAD "HKLM\$hiveName" "$softwareHivePath" 2>&1 | Out-Null
            $regPath = "HKLM:\$hiveName\Microsoft\Windows NT\CurrentVersion"
            if (Test-Path $regPath) {
                Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue |
                    Select-Object -Property CurrentBuild, BuildLabEx, EditionID, InstallationType, ProductName | Out-Host
            }
        }
        finally {
            Start-Sleep -Seconds 3
            reg.exe UNLOAD "HKLM\$hiveName" 2>&1 | Out-Null
        }
    }
    #endregion

    #region Export Initial Packages
    Write-OSDeployCoreProgress "Export initial Get-WindowsPackage to $CorePath"
    $WindowsPackage = $WindowsImage | Get-WindowsPackage
    if ($WindowsPackage) {
        $WindowsPackage | Select-Object * | Export-Clixml -Path "$CorePath\winpe-windowspackage-initial.xml" -Force
    }
    #endregion

    #region Adding OS Files
    if ($WimSourceType -eq 'WinRE' -and $ImportImageOSFilesPath -and (Test-Path $ImportImageOSFilesPath)) {
        Write-OSDeployCoreProgress "Adding OS Files from $ImportImageOSFilesPath"
        $null = robocopy.exe "$ImportImageOSFilesPath" "$MountPath" *.* /s /b /ndl /nfl /np /ts /r:0 /w:0 /xf bcp47*.dll /xx /xj /mt:128 /LOG+:"$LogsPath\os-files.log"
    }
    #endregion

    #region Adding OA3Tool
    $OA3ToolPath = $WindowsAdkPaths.oa3toolexe
    if ($OA3ToolPath -and (Test-Path $OA3ToolPath)) {
        Write-OSDeployCoreProgress "Adding OA3Tool from $OA3ToolPath"
        Copy-Item -Path $OA3ToolPath -Destination "$MountPath\Windows\System32\oa3tool.exe" -Force -ErrorAction SilentlyContinue | Out-Null
    }
    #endregion

    #region ADK Optional Component Packages
    if (-not $SkipAdkPackages) {
        $WinPEOCs = $WindowsAdkPaths.WinPEOCs
        $WindowsAdkWinpePackages = $global:OSDeployModule.BootImage.adkwinpepackages

        # Install default en-us packages
        Write-OSDeployCoreProgress 'Adding ADK Packages for Language en-us'
        $Lang = 'en-us'

        foreach ($Package in $WindowsAdkWinpePackages) {
            $PackageFile = "$WinPEOCs\WinPE-$Package.cab"
            if (Test-Path $PackageFile) {
                Write-Host -ForegroundColor Gray "$PackageFile"
                $PackageName = "Add-WindowsPackage-WinPE-$Package"
                $CurrentLog = "$LogsPath\$((Get-Date).ToString('yyMMdd-HHmmss'))-$PackageName.log"
                try {
                    $WindowsImage | Add-WindowsPackage -PackagePath $PackageFile -LogPath "$CurrentLog" -ErrorAction Stop | Out-Null
                }
                catch {
                    if ($_.Exception.ErrorCode -eq '-2148468766') {
                        Write-Warning '0x800f081e CBS_E_NOT_APPLICABLE The Windows ADK version does not support this WinPE version'
                    }
                    if ($_.Exception.ErrorCode -eq '-2146498512') {
                        Write-Warning '0x800f0830 CBS_E_IMAGE_UNSERVICEABLE The image may be corrupted. Discard and start again'
                    }
                }
            }
        }

        # Verify PowerShell was installed
        if (-not ($WindowsImage | Get-WindowsPackage | Where-Object { $_.PackageName -match 'PowerShell' })) {
            Write-Warning 'PowerShell Optional Component did not install. Required ADK Packages did not install properly.'
            Write-Warning 'Ensure the Windows ADK version supports the WinRE version being serviced.'
            Write-Warning 'Build will continue so you can review the logs.'
        }

        # Install en-us language pack
        $PackageFile = "$WinPEOCs\$Lang\lp.cab"
        if (Test-Path $PackageFile) {
            Write-Host -ForegroundColor Gray "$PackageFile"
            $CurrentLog = "$LogsPath\$((Get-Date).ToString('yyMMdd-HHmmss'))-Add-WindowsPackage-WinPE-lp_$Lang.log"
            try {
                $WindowsImage | Add-WindowsPackage -PackagePath $PackageFile -LogPath "$CurrentLog" -ErrorAction Stop | Out-Null
            }
            catch {
                if ($_.Exception.ErrorCode -eq '-2148468766') {
                    Write-Warning '0x800f081e CBS_E_NOT_APPLICABLE'
                }
                if ($_.Exception.ErrorCode -eq '-2146498512') {
                    Write-Warning '0x800f0830 CBS_E_IMAGE_UNSERVICEABLE'
                }
            }
        }

        # Install en-us language-specific OCs
        foreach ($Package in $WindowsAdkWinpePackages) {
            $PackageFile = "$WinPEOCs\$Lang\WinPE-${Package}_$Lang.cab"
            if (Test-Path $PackageFile) {
                Write-Host -ForegroundColor Gray "$PackageFile"
                $CurrentLog = "$LogsPath\$((Get-Date).ToString('yyMMdd-HHmmss'))-Add-WindowsPackage-WinPE-${Package}_$Lang.log"
                try {
                    $WindowsImage | Add-WindowsPackage -PackagePath $PackageFile -LogPath "$CurrentLog" -ErrorAction Stop | Out-Null
                }
                catch {
                    if ($_.Exception.ErrorCode -eq '-2148468766') {
                        Write-Warning '0x800f081e CBS_E_NOT_APPLICABLE'
                    }
                    if ($_.Exception.ErrorCode -eq '-2146498512') {
                        Write-Warning '0x800f0830 CBS_E_IMAGE_UNSERVICEABLE'
                    }
                }
            }
        }

        # Save after default language
        Step-BootImageSave

        # Install additional selected languages
        if ($Languages -contains '*') {
            $Languages = Get-ChildItem $WinPEOCs -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne 'en-us' } | Select-Object -ExpandProperty Name
        }

        foreach ($Lang in $Languages) {
            if ($Lang -eq 'en-us') { continue }

            Write-OSDeployCoreProgress "Adding ADK Packages for Language $Lang"
            $PackageFile = "$WinPEOCs\$Lang\lp.cab"
            if (Test-Path $PackageFile) {
                Write-Host -ForegroundColor Gray "$PackageFile"
                $CurrentLog = "$LogsPath\$((Get-Date).ToString('yyMMdd-HHmmss'))-Add-WindowsPackage-WinPE-lp_$Lang.log"
                try {
                    $WindowsImage | Add-WindowsPackage -PackagePath $PackageFile -LogPath "$CurrentLog" -ErrorAction Stop | Out-Null
                }
                catch {
                    if ($_.Exception.ErrorCode -eq '-2148468766') {
                        Write-Warning '0x800f081e CBS_E_NOT_APPLICABLE'
                    }
                    if ($_.Exception.ErrorCode -eq '-2146498512') {
                        Write-Warning '0x800f0830 CBS_E_IMAGE_UNSERVICEABLE'
                    }
                }
            }

            foreach ($Package in $WindowsAdkWinpePackages) {
                $PackageFile = "$WinPEOCs\$Lang\WinPE-${Package}_$Lang.cab"
                if (Test-Path $PackageFile) {
                    Write-Host -ForegroundColor Gray "$PackageFile"
                    $CurrentLog = "$LogsPath\$((Get-Date).ToString('yyMMdd-HHmmss'))-Add-WindowsPackage-WinPE-${Package}_$Lang.log"
                    try {
                        $WindowsImage | Add-WindowsPackage -PackagePath $PackageFile -LogPath "$CurrentLog" -ErrorAction Stop | Out-Null
                    }
                    catch {
                        if ($_.Exception.ErrorCode -eq '-2148468766') {
                            Write-Warning '0x800f081e CBS_E_NOT_APPLICABLE'
                        }
                        if ($_.Exception.ErrorCode -eq '-2146498512') {
                            Write-Warning '0x800f0830 CBS_E_IMAGE_UNSERVICEABLE'
                        }
                    }
                }
            }

            # Update lang.ini
            if (Test-Path "$MountPath\sources\lang.ini") {
                Write-OSDeployCoreProgress "Updating lang.ini for $Lang"
                $CurrentLog = "$LogsPath\$((Get-Date).ToString('yyMMdd-HHmmss'))-Gen-LangINI.log"
                dism.exe /image:"$MountPath" /Gen-LangINI /distribution:"$MountPath" /LogPath:"$CurrentLog"
            }

            Step-BootImageSave
        }
    }
    #endregion

    #region Step Functions
    Step-BootImageDismSettings
    Step-BootImageAddWallpaper
    Step-BootImageCustomWallpaper
    Step-BootImagePowerShellUpdate
    Step-BootImageAppAzCopy
    Step-BootImageAppSysinternals
    Step-BootImageAppCurl
    Step-BootImageAppZip
    Step-BootImageAppScript
    Step-BootImageSave
    Step-BootImageRemoveWinpeshl
    Step-BootImageConsoleSettings
    Step-BootImageSetEnvVars
    Step-BootImageCopyOSDModule
    Step-BootImageCopyOSDCloudModule
    Step-BootImageScript
    Step-BootImageSetStartnet
    Step-BootImageStartupProfile
    Step-BootImageDriver
    Step-BootImageExportDrivers
    Step-BootImageExportPackages
    Step-BootImageRegCurrentVersion
    Step-BootImageDismGetIntl
    Step-BootImageGetStartnet
    Step-BootImageGetWinpeshl
    Step-BootImageDismount
    Step-BootImageExport
    Step-BootImageMediaScript
    Step-BootImageIso
    Step-BootImageUpdateUSB
    #endregion

    #region Complete
    Write-OSDeployCoreProgress "Exporting Build Profile to $CorePath\buildprofile.json"
    $BuildProfile | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue | Out-File "$CorePath\buildprofile.json" -Encoding utf8 -Force

    Write-OSDeployCoreProgress "Exporting Build Context to $CorePath\buildcontext.json"
    $global:BuildMedia | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue | Out-File "$CorePath\buildcontext.json" -Encoding utf8 -Force

    #region Write properties.json
    Write-OSDeployCoreProgress "Writing properties.json to $MediaRootPath"
    $bootWimFinal = Join-Path $SourcesPath 'boot.wim'
    $wimInfo = Get-WindowsImage -ImagePath $bootWimFinal -Index 1

    $buildProperties = [ordered]@{
        Type             = 'WinPE'
        Id               = $MediaName
        Name             = $Name
        ModifiedTime     = $wimInfo.ModifiedTime
        InstallationType = $wimInfo.InstallationType
        Version          = $wimInfo.Version.ToString()
        Architecture     = $Architecture
        Languages        = [System.String[]]$Languages
        SetAllIntl       = $SetAllIntl
        InputLocale      = $SetInputLocale
        TimeZone         = $SetTimeZone
        ContentStartnet  = $global:BuildMedia.ContentStartnet
        ContentWinpeshl  = $global:BuildMedia.ContentWinpeshl
        InstalledApps    = $global:BuildMedia.InstalledApps
        AdkVersion       = $AdkInfo.InstallVersion
        BuildProfile     = $MyBuildProfilePath
        WinPEAppScript   = $WinPEAppScript
        WinPEScript      = $WinPEScript
        WinPEDriver      = $WinPEDriver
        WinPEMediaScript = $WinPEMediaScript
        CreatedTime      = $wimInfo.CreatedTime
        ImageName        = $wimInfo.ImageName
        ImagePath        = $bootWimFinal
        ImageIndex       = 1
        ImageSize        = $wimInfo.ImageSize
        DirectoryCount   = $wimInfo.DirectoryCount
        FileCount        = $wimInfo.FileCount
        Path             = $MediaRootPath
    }

    # Add OS source info if WinRE-based
    if ($WimSourceType -eq 'WinRE' -and $GetWindowsImage.OSCreatedTime) {
        $buildProperties.OSCreatedTime  = $GetWindowsImage.OSCreatedTime
        $buildProperties.OSModifiedTime = $GetWindowsImage.OSModifiedTime
        $buildProperties.OSImageName    = $GetWindowsImage.OSImageName
        $buildProperties.OSEditionId    = $GetWindowsImage.OSEditionId
        $buildProperties.OSVersion      = $GetWindowsImage.OSVersion
    }

    $buildProperties | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue |
        Out-File (Join-Path $MediaRootPath 'properties.json') -Encoding utf8 -Force
    #endregion

    # Restore settings
    [Net.ServicePointManager]::SecurityProtocol = $currentVersionTls
    $ProgressPreference = $currentProgressPref

    $BuildEndTime = Get-Date
    $BuildTimeSpan = New-TimeSpan -Start $BuildStartTime -End $BuildEndTime
    Write-OSDeployCoreProgress "Build-OSDeployBootMedia completed in $($BuildTimeSpan.ToString("mm' minutes 'ss' seconds'"))"
    Stop-Transcript
    #endregion
}