public/Build-OSDeployBoot.ps1

#Requires -PSEdition Core
#Requires -Version 7.4

function Build-OSDeployBoot {
    <#
    .SYNOPSIS
        Builds customized WinPE boot media from a WinRE or Windows ADK source
 
    .DESCRIPTION
        Creates boot media under %ProgramData%\OSDeployCore\boot from an imported WinRE
        image or the Windows ADK winpe.wim. In the Default parameter set, the command
        selects an imported WinRE source interactively unless Auto is specified, and falls
        back to ADK WinPE when no WinRE source is selected or available. The ADK parameter
        set requires an explicit architecture and UseAdkWinPE.
 
        ProfileName tab-completes saved architecture-specific profiles from flat
        <Name>-<Architecture> profile directories. It requires an exact match and supplies the
        architecture and content paths without displaying shared-content or wallpaper selectors.
        Recovery-image selection remains available and is filtered to the profile architecture.
        Languages, international settings, timezone, and options passed on the command line
        override saved values for that build only. The selected profile JSON and profile-local
        files are not changed.
 
        When ProfileName is omitted, shared-content selections are written to recent-amd64.json
        or recent-arm64.json under Boot-Assets. The matching architecture snapshot is
        overwritten after configuration, before build confirmation. Selected wallpaper and other
        profile-local companion content remain temporary and are discarded after the build. Use
        New-OSDeployBootProfilePreview and Update-OSDeployBootProfilePreview to manage named profiles.
 
        The build validates selected and profile-local WinPEStartup profile JSON before showing
        the build configuration. It then services boot.wim, creates boot media and ISO content,
        writes build metadata and logs, and can copy completed media to USB partitions labeled
        USB-WinPE. Before creating the build directories, it displays the configuration and waits
        five seconds; WhatIf and Confirm gate the build-directory operation and stop the build
        when declined.
 
    .PARAMETER ProfileName
        Specifies the exact canonical name of an existing profile, such as Contoso-amd64.
        Values tab-complete from saved profiles. Saved content is loaded without content
        selectors, command-line configuration values apply only to the current build, and the
        source profile is not modified.
 
    .PARAMETER Architecture
        Specifies amd64 or arm64. In the Default parameter set, this value limits WinRE source
        selection; when omitted, a selected WinRE source determines the architecture. Auto
        derives an omitted value from PROCESSOR_ARCHITECTURE. A matching saved profile supplies
        this value. The value must otherwise be specified when UseAdkWinPE is used.
 
    .PARAMETER Languages
        Specifies zero or more validated Windows ADK language identifiers to add. The en-us
        base packages are processed independently. Specify * to enumerate all additional
        language directories. When ProfileName is specified, an explicitly passed value applies
        only to the current build.
 
    .PARAMETER SetAllIntl
        Specifies the value used by the WinPE international-settings build step. A saved
        profile supplies this value when omitted. When ProfileName is specified, an explicitly
        passed value applies only to the current build.
 
    .PARAMETER SetInputLocale
        Specifies the WinPE input locale used by the servicing steps. A saved profile supplies
        this value when omitted. When ProfileName is specified, an explicitly passed value
        applies only to the current build.
 
    .PARAMETER SetTimeZone
        Specifies a timezone validated against tzutil /l. The default is the current system
        timezone returned by tzutil /g when no profile supplies it. When ProfileName is specified,
        an explicitly passed value applies only to the current build.
 
    .PARAMETER SkipAdkPackages
        Skips installation of the configured Windows ADK optional-component and language
        packages. Other image customization steps still run.
 
    .PARAMETER UseAdkWinPE
        Uses the Windows ADK winpe.wim instead of an imported WinRE source. This switch is
        mandatory in the ADK parameter set and cannot be combined with Auto. Architecture must
        be specified unless it is supplied by a matching saved profile.
 
    .PARAMETER UpdateUSB
        Runs the final USB update build step, which targets USB partitions labeled USB-WinPE.
 
    .PARAMETER Auto
        Selects the newest imported WinRE source for the resolved architecture without showing
        the WinRE picker. When none is available, uses ADK WinPE. ProfileName still loads saved
        content without selectors; temporary builds show shared-content and wallpaper selectors.
 
    .PARAMETER Options
        Specifies optional WinPE features to include. Valid values are pwsh and dart, and
        multiple values are allowed. Selecting pwsh installs PowerShell 7, and selecting dart
        installs available Microsoft DaRT content in the mounted image. When ProfileName is
        specified, an explicitly passed value applies only to the current build.
 
    .EXAMPLE
        PS> Build-OSDeployBoot -ProfileName 'MyPE-amd64' -Options pwsh
 
        Loads MyPE-amd64 without content selectors, uses pwsh for this build only, and prompts
        for a matching WinRE source without changing the profile JSON.
 
    .EXAMPLE
        PS> Build-OSDeployBoot
 
        Prompts for a WinRE source and build content, writes the matching recent architecture
        profile, builds media named OSDeploy, then removes temporary companion content.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        None. This function does not intentionally return the build context.
 
    .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,
        Windows ADK with the WinPE add-on, curl.exe, OSDCloud 26.7.25.2 or later, and
        Administrator rights.
 
        The function populates $global:BuildMedia for build steps and later inspection. That
        global value is process state, not pipeline output. It also changes process-wide web,
        TLS, and progress preferences during the build and restores the TLS and progress values
        only at normal completion.
 
        Recent architecture profile JSON remains after normal completion, cancellation, a declined
        build operation, or a terminating error after configuration. Temporary companion content is
        removed. Generated build output is not removed with the companion content.
 
    .LINK
        New-OSDeployBootProfilePreview
 
    .LINK
        Update-OSDeployBootProfilePreview
 
    .LINK
        https://learn.microsoft.com/en-us/windows-hardware/get-started/adk-install
    #>


    [CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, ParameterSetName = 'Profile')]
        [Parameter(Mandatory, ParameterSetName = 'ADKProfile')]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete)

            $profilePath = Join-Path $env:ProgramData 'OSDeployCore\boot-assets\osdeployboot-profiles'
            Get-ChildItem -LiteralPath $profilePath -Directory -ErrorAction SilentlyContinue |
                Where-Object {
                    $_.Name -like "$wordToComplete*" -and
                    (Test-Path -LiteralPath (Join-Path $_.FullName 'osdeployboot.json') -PathType Leaf)
                } |
                Sort-Object Name |
                ForEach-Object {
                    $completionText = if ($_.Name -match '\s') {
                        "'$($_.Name.Replace("'", "''"))'"
                    }
                    else {
                        $_.Name
                    }
                    [System.Management.Automation.CompletionResult]::new(
                        $completionText,
                        $_.Name,
                        [System.Management.Automation.CompletionResultType]::ParameterValue,
                        $_.FullName
                    )
                }
        })]
        [System.String]
        $ProfileName,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'ADK')]
        [Parameter(ParameterSetName = 'Profile')]
        [Parameter(ParameterSetName = 'ADKProfile')]
        [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')]
        [Parameter(Mandatory, ParameterSetName = 'ADKProfile')]
        [System.Management.Automation.SwitchParameter]
        $UseAdkWinPE,

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

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'Profile')]
        [System.Management.Automation.SwitchParameter]
        $Auto,

        [ValidateSet('pwsh', 'dart')]
        [System.String[]]
        $Options
    )
    #=================================================
    $BuildDateTime = (Get-Date).ToString('yyMMdd-HHmm')
    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Starting $($MyInvocation.MyCommand.Name) at $BuildDateTime"
    #=================================================
    # Stop before preparing build 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."
    }
    #=================================================
    # Stop when the required OSDCloud version is unavailable.
    #region OSDCloud Requirements
    $RequiredOSDCloudVersion = [System.Version]'26.7.25.2'
    if (-not (Get-Command -Name 'Get-OSDCloudModuleVersion' -ErrorAction SilentlyContinue)) {
        Write-Warning "[$(Get-Date -Format s)] OSDCloud module $RequiredOSDCloudVersion or newer is required."
        Write-Warning "[$(Get-Date -Format s)] Install-Module -Name OSDCloud -Force -SkipPublisherCheck"
        return
    }
    $OSDCloudVersion = Get-OSDCloudModuleVersion
    if ($OSDCloudVersion -lt $RequiredOSDCloudVersion) {
        Write-Warning "[$(Get-Date -Format s)] OSDCloud module $RequiredOSDCloudVersion or newer is required. Loaded version: $OSDCloudVersion"
        Write-Warning "[$(Get-Date -Format s)] Install-Module -Name OSDCloud -Force -SkipPublisherCheck"
        return
    }
    #endregion
    #=================================================
    Initialize-OSDCoreLicense
    #=================================================
    $ProfileNameSpecified = $PSBoundParameters.ContainsKey('ProfileName') -and -not [System.String]::IsNullOrWhiteSpace($ProfileName)
    if ($PSBoundParameters.ContainsKey('ProfileName') -and [System.String]::IsNullOrWhiteSpace($ProfileName)) {
        throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] ProfileName cannot be empty."
    }

    # Profile mode is read-only; Boot-Assets initialization can migrate and rewrite saved profiles.
    if (-not $ProfileNameSpecified) {
        Initialize-OSDeployCorePaths
    }

    $MyBuildProfile = $null
    $ProfileArchitecture = $null

    if ($ProfileNameSpecified) {
        $MyBuildProfile = Select-BuildOSDeployBootProfile -Name $ProfileName -SkipTokenMigration
        if ($MyBuildProfile) {
            $ProfileArchitecture = (Get-Content -LiteralPath $MyBuildProfile.FullName -Raw | ConvertFrom-Json).Architecture
        }
        else {
            throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Build profile was not found or could not be read: $ProfileName"
        }

        if ($Architecture -and $ProfileArchitecture -and $Architecture -ne $ProfileArchitecture) {
            throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Architecture '$Architecture' does not match build profile architecture '$ProfileArchitecture'."
        }
        if ($ProfileArchitecture) {
            $Architecture = $ProfileArchitecture
        }
    }

    if ($UseAdkWinPE -and -not $Architecture) {
        throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Architecture is required when no saved build profile supplies it."
    }

    $BuildName = if ($ProfileNameSpecified) {
        $ProfileName -replace '(?i)-(?:amd64|arm64)$', ''
    }
    else {
        'OSDeploy'
    }

    #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

    # Stop before source selection when the ADK required to build media is unavailable.
    #region ADK Detection
    $AdkInfo = Get-OSDeployWindowsAdkInstall
    if (-not $AdkInfo.IsInstalled) {
        Write-Warning "[$(Get-Date -Format s)] Windows ADK is not installed"
        Write-Warning "[$(Get-Date -Format s)] Install the Windows ADK from https://learn.microsoft.com/en-us/windows-hardware/get-started/adk-install"
        return
    }

    $AdkRootPath = $AdkInfo.InstallPath
    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Windows ADK $($AdkInfo.InstallVersion)"
    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Windows ADK: $AdkRootPath"

    # Select an explicit ADK source or a compatible WinRE source, with ADK as the fallback.
    #region Select Boot Image Source
    if ($Auto -and -not $Architecture) {
        $Architecture = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'amd64' }
        Write-Verbose "[$($MyInvocation.MyCommand.Name)] Auto mode: detected architecture '$Architecture'"
    }

    if ($UseAdkWinPE) {
        $WimSourceType = 'WinPE'
    }
    else {
        $WimSourceType = 'WinRE'
        if ($Auto) {
            $GetWindowsImage = Get-OSDeployCoreWindowsRE -Architecture $Architecture |
                Sort-Object OSVersion -Descending | Select-Object -First 1
            if ($GetWindowsImage) {
                Write-Verbose "[$($MyInvocation.MyCommand.Name)] Auto mode: selected latest WinRE '$($GetWindowsImage.Name)' version $($GetWindowsImage.OSVersion)"
            }
        }
        elseif ($Architecture) {
            $GetWindowsImage = Select-OSDeployCoreCacheWindowsRE -Architecture $Architecture
        }
        else {
            $GetWindowsImage = Select-OSDeployCoreCacheWindowsRE
        }

        if (-not $GetWindowsImage -or $GetWindowsImage.Count -eq 0) {
            Write-Warning "[$(Get-Date -Format s)] 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 "[$(Get-Date -Format s)] 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-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Recovery Image: $($GetWindowsImage.ImagePath)"
        }
    }
    #endregion

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

    $WindowsAdkPaths = Get-OSDeployWindowsAdkPaths -Architecture $Architecture -AdkRoot $AdkRootPath
    if (-not $WindowsAdkPaths) {
        Write-Warning "[$(Get-Date -Format s)] 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-$BuildName"
    $MediaIsoLabel = "$trimmedVersion-$BuildName"

    $WindowsAdkPaths.WimSourcePath = $ImportImageWimPath
    #endregion

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

    # 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

    $TemporaryBuildProfilePath = $null
    try {
    #region Select Profile, Drivers, Scripts
    if ($ProfileNameSpecified) {
        $SavedBuildProfile = Get-Content $MyBuildProfile.FullName -Raw | ConvertFrom-Json
        $WinPEDriver         = Expand-OSDeployBuildProfileToken $SavedBuildProfile.WinPEDriver
        if ($WimSourceType -eq 'WinPE' -and $WinPEDriver) {
            Write-HostDateTimeDarkGray 'ADK WinPE does not support wireless hardware - excluding Wi-Fi drivers'
            $WinPEDriver = $WinPEDriver | Where-Object { (Split-Path $_ -Leaf) -notmatch 'wifi|wireless' }
        }
        $WinPEScript         = Expand-OSDeployBuildProfileToken $SavedBuildProfile.WinPEScript
        $BuildMediaScript    = Expand-OSDeployBuildProfileToken $SavedBuildProfile.MediaScript
        $WinPEStartupProfile = Expand-OSDeployBuildProfileToken $SavedBuildProfile.WinPEStartupProfile
        if (-not $PSBoundParameters.ContainsKey('Languages')) {
            [System.String[]]$Languages = $SavedBuildProfile.Languages
        }
        if (-not $PSBoundParameters.ContainsKey('SetAllIntl')) {
            $SetAllIntl = $SavedBuildProfile.SetAllIntl
        }
        if (-not $PSBoundParameters.ContainsKey('SetInputLocale')) {
            $SetInputLocale = $SavedBuildProfile.SetInputLocale
        }
        if (-not $PSBoundParameters.ContainsKey('SetTimeZone')) {
            $SetTimeZone = $SavedBuildProfile.SetTimeZone
        }
        if (-not $PSBoundParameters.ContainsKey('Options')) {
            [System.String[]]$Options = $SavedBuildProfile.Options
        }
        $BuildProfile = [ordered]@{
            Name                 = [System.String]$MyBuildProfile.Name
            Architecture         = [System.String]$Architecture
            Languages            = [System.String[]]$Languages
            SetAllIntl           = [System.String]$SetAllIntl
            SetInputLocale       = [System.String]$SetInputLocale
            SetTimeZone          = [System.String]$SetTimeZone
            Options              = [System.String[]]$Options
            WinPEStartupProfile  = $SavedBuildProfile.WinPEStartupProfile
            WinPEDriver          = $SavedBuildProfile.WinPEDriver
            WinPEScript          = $SavedBuildProfile.WinPEScript
            MediaScript          = $SavedBuildProfile.MediaScript
        }
        $MyBuildProfilePath = $MyBuildProfile.FullName
    }
    else {
        $skipWifi = $WimSourceType -eq 'WinPE'
        if ($skipWifi) {
            Write-HostDateTimeDarkGray 'ADK WinPE does not support wireless hardware - excluding Wi-Fi drivers'
        }
        $SelectedContent = Select-OSDeployBootProfileContent -Architecture $Architecture -SkipWifiDrivers:$skipWifi
        $WinPEDriver = $SelectedContent.WinPEDriver
        $WinPEScript = $SelectedContent.WinPEScript
        $BuildMediaScript = $SelectedContent.MediaScript
        $WinPEStartupProfile = $SelectedContent.WinPEStartupProfile

        $BuildProfileName = "$BuildName-$($Architecture.ToLowerInvariant())"
        $BuildProfile = [ordered]@{
            Name                 = [System.String]$BuildProfileName
            Architecture         = [System.String]$Architecture
            Languages            = [System.String[]]$Languages
            SetAllIntl           = [System.String]$SetAllIntl
            SetInputLocale       = [System.String]$SetInputLocale
            SetTimeZone          = [System.String]$SetTimeZone
            Options              = [System.String[]]$Options
            WinPEStartupProfile  = ConvertTo-OSDeployBuildProfileToken $WinPEStartupProfile
            WinPEDriver          = ConvertTo-OSDeployBuildProfileToken $WinPEDriver
            WinPEScript          = ConvertTo-OSDeployBuildProfileToken $WinPEScript
            MediaScript          = ConvertTo-OSDeployBuildProfileToken $BuildMediaScript
        }

        $BuildProfileContentPath = Join-Path ([System.IO.Path]::GetTempPath()) "OSDeployBootProfile-$([System.Guid]::NewGuid().ToString('N'))"
        $TemporaryBuildProfilePath = $BuildProfileContentPath
        New-Item -Path $BuildProfileContentPath -ItemType Directory -ErrorAction Stop | Out-Null
        Initialize-OSDeployCoreBuildProfilePaths -Path $BuildProfileContentPath
        $BuildProfilesPath = Join-Path $Script:OSDeployBootAssetsPath 'osdeployboot-profiles'
        $MyBuildProfilePath = Join-Path $BuildProfilesPath "recent-$($Architecture.ToLowerInvariant()).json"

        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Exporting Build Profile to $MyBuildProfilePath"
        $BuildProfile | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue | Out-File -LiteralPath $MyBuildProfilePath -Encoding utf8 -Force
    }

    $BuildProfileDirectory = if ($ProfileNameSpecified) {
        Split-Path -Path $MyBuildProfilePath -Parent
    }
    else {
        $BuildProfileContentPath
    }
    if (-not $ProfileNameSpecified) {
        Set-OSDeployBootProfileWallpaper -ProfilePath $BuildProfileDirectory
    }

    $WinPEStartupProfilesToValidate = @($WinPEStartupProfile | Where-Object { $_ })
    $ProfileStartupProfilePath = Join-Path $BuildProfileDirectory 'WinPEStartup' 'profiles'
    if (Test-Path -LiteralPath $ProfileStartupProfilePath -PathType Container) {
        $WinPEStartupProfilesToValidate += Get-ChildItem -LiteralPath $ProfileStartupProfilePath -Filter '*.json' -File -ErrorAction Stop |
            Sort-Object FullName |
            Select-Object -ExpandProperty FullName
    }

    $SeenProfiles = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($StartupProfilePath in $WinPEStartupProfilesToValidate) {
        if ($SeenProfiles.Add([System.String]$StartupProfilePath)) {
            Test-OSDeployWinPEStartupProfile -Path $StartupProfilePath
        }
    }
    #endregion

    #region BuildMedia
    $global:BuildMedia = [ordered]@{
        AdkRootPath       = $AdkRootPath
        AdkPaths          = $WindowsAdkPaths
        SkipAdkPackages   = [System.Boolean]$SkipAdkPackages
        Architecture      = [System.String]$Architecture
        BootGuid          = [System.Guid]::NewGuid().ToString()
        BuildProfile      = $MyBuildProfilePath
        BuildProfileContentPath = $BuildProfileDirectory
        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]$BuildName
        SetAllIntl        = [System.String]$SetAllIntl
        SetInputLocale    = [System.String]$SetInputLocale
        SetTimeZone       = [System.String]$SetTimeZone
        SourcesPath       = $SourcesPath
        SourcesPathEX     = $null
        UpdateUSB         = [System.Boolean]$UpdateUSB
        WimSourceType     = $WimSourceType
        WindowsImage      = $null
        WinPEAppsPath     = Join-Path $Script:OSDeployCorePath 'cache' 'winpe-apps'
        WinPEDriver           = $WinPEDriver
        MediaScript           = $BuildMediaScript
        WinPEScript           = $WinPEScript
        WinPEStartupProfile   = $WinPEStartupProfile
    }
    #endregion

    # Present the final configuration before creating the build output.
    #region Point of No Return
    Write-HostDateTimeDarkGray 'Build Configuration'
    $global:BuildMedia | Out-Host
    Write-Host -ForegroundColor DarkCyan 'Proceeding with build in 5 seconds. Use Ctrl+C to cancel before the next operation starts.'
    Start-Sleep -Seconds 5
    $BuildStartTime = Get-Date
    #endregion

    # Send build telemetry with selected license identity fields.
    $EventLicense = Get-OSDCoreLicense
    Send-RecastOSDeployEvent -EventName 'Build-OSDeployBoot' -Properties @{
        BootGuid    = $global:BuildMedia.BootGuid
        Email       = if ($EventLicense) { [System.String]$EventLicense.Email } else { 'Unregistered' }
        LicenseGuid = if ($EventLicense) { [System.String]$EventLicense.LicenseGuid } else { 'Unregistered' }
    }

    # Honor WhatIf and Confirm before creating the build directory tree.
    #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-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] 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-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] 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-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Hydrate $MediaPathEX"
        $null = robocopy.exe "$($WindowsAdkPaths.PathWinPEMedia)" "$MediaPathEX" *.* /mir /b /ndl /np /r:0 /w:0 /xj /mt:128 /LOG+:"$LogsPath\mediaex.log"

        Write-HostDateTimeDarkGray '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-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Hydrate $bootWimPath"
    Copy-Item -Path $WindowsAdkPaths.WimSourcePath -Destination $bootWimPath -Force -ErrorAction Stop | Out-Null

    if (-not (Test-Path $bootWimPath)) {
        Write-Warning "[$(Get-Date -Format s)] 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-HostDateTimeDarkGray '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-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] MountPath: $MountPath"
    #endregion

    #region Recast Licenses
    $RecastLicensesPath = Join-Path $env:ProgramData 'Recast Software\Licenses'
    if (Test-Path $RecastLicensesPath) {
        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Copy Recast Software License files from $RecastLicensesPath to WinPE image"
        $DestinationPath = Join-Path $MountPath 'ProgramData\Recast Software\Licenses'
        if (-not (Test-Path $DestinationPath)) {
            New-Item -Path $DestinationPath -ItemType Directory -Force | Out-Null
        }
        Copy-Item -Path "$RecastLicensesPath\*" -Destination $DestinationPath -Recurse -Force -ErrorAction SilentlyContinue | Out-Null
    }
    #endregion

    #region Registry Information
    Write-HostDateTimeDarkGray '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-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] 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-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] 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-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] 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-HostDateTimeDarkGray '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 "[$(Get-Date -Format s)] 0x800f081e CBS_E_NOT_APPLICABLE The Windows ADK version does not support this WinPE version"
                    }
                    if ($_.Exception.ErrorCode -eq '-2146498512') {
                        Write-Warning "[$(Get-Date -Format s)] 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 "[$(Get-Date -Format s)] PowerShell Optional Component did not install. Required ADK Packages did not install properly."
            Write-Warning "[$(Get-Date -Format s)] Ensure the Windows ADK version supports the WinRE version being serviced."
            Write-Warning "[$(Get-Date -Format s)] 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 "[$(Get-Date -Format s)] 0x800f081e CBS_E_NOT_APPLICABLE"
                }
                if ($_.Exception.ErrorCode -eq '-2146498512') {
                    Write-Warning "[$(Get-Date -Format s)] 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 "[$(Get-Date -Format s)] 0x800f081e CBS_E_NOT_APPLICABLE"
                    }
                    if ($_.Exception.ErrorCode -eq '-2146498512') {
                        Write-Warning "[$(Get-Date -Format s)] 0x800f0830 CBS_E_IMAGE_UNSERVICEABLE"
                    }
                }
            }
        }

        # Save after default language
        Step-BuildBootSaveWindowsImage

        # 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-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] 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 "[$(Get-Date -Format s)] 0x800f081e CBS_E_NOT_APPLICABLE"
                    }
                    if ($_.Exception.ErrorCode -eq '-2146498512') {
                        Write-Warning "[$(Get-Date -Format s)] 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 "[$(Get-Date -Format s)] 0x800f081e CBS_E_NOT_APPLICABLE"
                        }
                        if ($_.Exception.ErrorCode -eq '-2146498512') {
                            Write-Warning "[$(Get-Date -Format s)] 0x800f0830 CBS_E_IMAGE_UNSERVICEABLE"
                        }
                    }
                }
            }

            # Update lang.ini
            if (Test-Path "$MountPath\sources\lang.ini") {
                Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] 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-BuildBootSaveWindowsImage
        }
    }
    #endregion

    #region Step Functions
    Step-BuildBootSetDismSettings
    Step-BuildBootAddWinpeJpg
    Step-BuildBootCoreWinPEWallpaper
    Step-BuildBootUpdatePowerShell
    Step-BuildBootInstallWinPEAppAzCopy
    Step-BuildBootInstallWinPEAppSysinternals
    Step-BuildBootInstallWinPEAppCurl
    Step-BuildBootInstallWinPEAppZip
    if ($Options -contains 'pwsh') {
        Step-BuildBootInstallWinPEAppPwsh
    }
    if ($Options -contains 'dart') {
        Step-BuildBootInstallWinPEAppDaRT
    }
    # Step-BuildBootCoreWinPEApp
    Step-BuildBootSaveWindowsImage
    Step-BuildBootRemoveWinpeshl
    Step-BuildBootConsoleSettings
    Step-BuildBootSetEnvironmentVariables
    Step-BuildBootCopyOSDModule
    Step-BuildBootCopyOSDCloudModule
    Step-BuildBootCoreWinPEScript
    Step-BuildBootSetContentStartnet
    Step-BuildBootCoreWinPEStartupAssets
    Step-BuildBootCoreWinPEStartupProfiles
    Step-BuildBootCoreWinPEDrivers
    Step-BuildBootLogWindowsDriver
    Step-BuildBootLogWindowsPackage
    Step-BuildBootLogRegCurrentVersion
    Step-BuildBootLogDismGetIntl
    Step-BuildBootGetContentStartnet
    Step-BuildBootGetContentWinpeshl
    Step-BuildBootDismountWindowsImage
    Step-BuildBootExportWindowsImage
    Step-BuildBootCoreMediaScript
    Step-BuildBootMediaIso
    Step-BuildBootUpdateUsbDrive
    #endregion

    #region Complete
    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Exporting Build Profile to $CorePath\osdeployboot.json"
    $BuildProfile | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue | Out-File "$CorePath\osdeployboot.json" -Encoding utf8 -Force

    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] 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-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] 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             = $BuildName
        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
        WinPEScript      = $WinPEScript
        WinPEDriver      = $WinPEDriver
        MediaScript      = $BuildMediaScript
        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-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Build-OSDeployBoot completed in $($BuildTimeSpan.ToString("mm' minutes 'ss' seconds'"))"
    Stop-Transcript
    #endregion
    }
    finally {
        if ($TemporaryBuildProfilePath -and (Test-Path -LiteralPath $TemporaryBuildProfilePath)) {
            Remove-Item -LiteralPath $TemporaryBuildProfilePath -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
}