public/Invoke-OSDeployMDT.ps1

function Invoke-OSDeployMDT {
    <#
    .SYNOPSIS
        MDT LiteTouchPE exit script — runs automatically on every Update Deployment Share.
 
    .DESCRIPTION
        Called automatically by MDT as a LiteTouchPE exit script at each stage
        of the deployment share update (WIM, POSTWIM, ISO, POSTISO). The
        function reads $Env:STAGE to determine which actions to take.
 
        If $Env:STAGE is not set (e.g. run manually), the function displays help and returns.
 
        STAGE = WIM (mounted WinPE image available):
          - Collects EFI boot files and ADK oscdimg binaries to DEPLOYROOT\Boot\bootbins\.
          - Copies ADK OA3Tool to WinPE System32.
          - Applies DISM locale and time-zone settings (Step-BuildMediaDismSettings).
          - Adds PackageManagement and PowerShellGet modules to WinPE
            (Step-BuildMediaPowerShellUpdate).
          - Adds AzCopy, curl, and 7-Zip to WinPE System32
            (Step-WinPEAppAzCopy, Step-WinPEAppCurl, Step-WinPEAppZip).
          - Saves the OSDCloud PowerShell module into the WinPE image.
          - Injects WinPE drivers via interactive picker.
 
        STAGE = POSTISO:
          - Builds a patched ISO with bootmgfw_EX.efi for UEFI CA 2023 Secure Boot.
 
        MDT environment variables consumed:
          STAGE - WIM | POSTWIM | ISO | POSTISO
          CONTENT - Path to the mounted WIM or captured output file
          ARCHITECTURE - amd64 | x86
          INSTALLDIR - MDT installation directory
          DEPLOYROOT - MDT deployment share root
 
    .PARAMETER SetInputLocale
        Sets the default InputLocale in WinPE. Default: en-us.
 
    .PARAMETER SetTimeZone
        Sets the WinPE time zone. Validated against tzutil /l.
        Default: the current system time zone.
 
    .EXAMPLE
        Invoke-OSDeployMDT
 
        Runs the MDT exit customization using current system locale and time zone.
 
    .EXAMPLE
        Invoke-OSDeployMDT -SetTimeZone 'Romance Standard Time'
 
        Runs exit customization configured for Romance Standard Time.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        None.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
    #>

    [CmdletBinding()]
    param (
        # Sets the default InputLocale in WinPE to the specified Input Locale. Default is en-US.
        [System.String]
        $SetInputLocale,
        # Set the WinPE SetTimeZone. Default is the current SetTimeZone.
        [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)
    )
    #=================================================
    Write-OSDeployBanner
    if (-not $Env:STAGE) {
        Get-Help -Name Invoke-OSDeployMDT
        return
    }
    Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] $($MyInvocation.MyCommand.Name)"
    Write-Host -ForegroundColor DarkYellow "[$(Get-Date -format s)] Env:ADKPath: $($Env:ADKPath)"
    Write-Host -ForegroundColor DarkYellow "[$(Get-Date -format s)] Env:INSTALLDIR: $($Env:INSTALLDIR)"
    Write-Host -ForegroundColor DarkYellow "[$(Get-Date -format s)] Env:DEPLOYROOT: $($Env:DEPLOYROOT)"
    Write-Host -ForegroundColor DarkYellow "[$(Get-Date -format s)] Env:TEMPLATE: $($Env:TEMPLATE)"
    Write-Host -ForegroundColor DarkYellow "[$(Get-Date -format s)] Env:STAGE: $($Env:STAGE)"
    Write-Host -ForegroundColor DarkYellow "[$(Get-Date -format s)] Env:CONTENT: $($Env:CONTENT)"
    #=================================================
    # Parameter Defaults
    $SetAllIntl = 'en-us'
    if ($null -eq $SetInputLocale) {
        $SetInputLocale = 'en-us'
    }
    if ($null -eq $SetTimeZone) {
        $SetTimeZone = (tzutil /g)
    }
    #=================================================
    #region BuildProfile
    $global:BuildMedia = $null
    $global:BuildMedia = [ordered]@{
        AdkPaths                = Get-MDTWindowsAdkPaths
        Architecture            = [System.String]$env:ARCHITECTURE
        BootBinsPath            = "$env:DEPLOYROOT\Boot\bootbins"
        InstalledApps           = @()
        LogsPath                = $env:TEMP
        MountPath               = $env:CONTENT
        PSRepository            = "$env:ProgramData\OSDeployCore\cache\psrepository"
        SetAllIntl              = [System.String]$SetAllIntl
        SetInputLocale          = [System.String]$SetInputLocale
        SetTimeZone             = [System.String]$SetTimeZone
        WimSourceType           = 'WinPE'
        WinPEAppsPath           = "$env:ProgramData\OSDeployCore\cache\winpe-apps"
        WSCachePath             = "$env:ProgramData\OSDeployCore\cache"
    }
    <#
    $global:BuildMedia = [ordered]@{
        AdkInstallPath = $WindowsAdkInstallPath
        AdkInstallVersion = $WindowsAdkInstallVersion
        AdkRootPath = $WindowsAdkRootPath
        AdkSkipOcPackages = $AdkSkipOcPackages
        BuildProfile = $MyBuildProfilePath
        ContentStartnet = [System.String]$ContentStartnet
        ContentWinpeshl = [System.String]$ContentWinpeshl
        ImportImageRootPath = $ImportImageRootPath
        ImportImageWimPath = $ImportImageWimPath
        Languages = [System.String[]]$Languages
        WinPEAppScript = $WinPEAppScript
        WinPEScript = $WinPEScript
        WinPEMediaScript = $WinPEMediaScript
        WinPEDriver = $WinPEDriver
        MediaIsoLabel = $MediaIsoLabel
        MediaIsoName = $MediaIsoName
        MediaIsoNameEX = $MediaIsoNameEX
        MediaName = $MediaName
        MediaPath = Join-Path $MediaRootPath 'WinPE-Media'
        MediaPathEX = $null
        MediaRootPath = $MediaRootPath
        MountPath = $env:CONTENT
        Name = [System.String]$Name
        PEVersion = $GetWindowsImage.Version
        AdkSelectCacheVersion = $AdkSelectCacheVersion
        UpdateUSB = [System.Boolean]$UpdateUSB
        AdkUseWinPE = $AdkUseWinPE
        WSCachePathAdk = $WSAdkVersionsPath
    }
    #>

    #endregion
    #=================================================
    $MountPath = $global:BuildMedia.MountPath
    #=================================================
    if ($Env:STAGE -eq "WIM") {
        #=================================================
        #region bootbins
        $BootBinsPath = $global:BuildMedia.BootBinsPath
        Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] Copying the latest ADK boot files to $BootBinsPath"
        if (-not (Test-Path -LiteralPath $BootBinsPath)) {
            New-Item -Path $BootBinsPath -ItemType Directory -Force | Out-Null
            Write-Verbose "[$(Get-Date -format s)] Created $BootBinsPath"
        }

        # Copy files from $MountPath to bootbins
        $EfiFiles = @(
            @{ Source = "$MountPath\Windows\Boot\EFI\bootmgfw.efi";     Dest = "$BootBinsPath\bootmgfw.efi" }
            @{ Source = "$MountPath\Windows\Boot\EFI_EX\bootmgfw_EX.efi"; Dest = "$BootBinsPath\bootmgfw_EX.efi" }
        )
        foreach ($EfiFile in $EfiFiles) {
            if (Test-Path -LiteralPath $EfiFile.Source) {
                Copy-Item -LiteralPath $EfiFile.Source -Destination $EfiFile.Dest -Force -ErrorAction SilentlyContinue
                Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] Source: $($EfiFile.Source)"
            }
            else {
                Write-Verbose "[$(Get-Date -format s)] Not found (skipping): $($EfiFile.Source)"
            }
        }

        # Copy files from Oscdimg directory to bootbins
        $PathOscdimg = $global:BuildMedia.AdkPaths.PathOscdimg
        $OscdimgFiles = @('efisys.bin', 'efisys_noprompt.bin', 'efisys_EX.bin', 'efisys_noprompt_EX.bin', 'etfsboot.com')
        foreach ($OscdimgFile in $OscdimgFiles) {
            $SourceFile = Join-Path $PathOscdimg $OscdimgFile
            if (Test-Path -LiteralPath $SourceFile) {
                Copy-Item -LiteralPath $SourceFile -Destination "$BootBinsPath\$OscdimgFile" -Force -ErrorAction SilentlyContinue
                Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] Source: $SourceFile"
            }
            else {
                Write-Verbose "[$(Get-Date -format s)] Not found (skipping): $SourceFile"
            }
        }
        #endregion
        #=================================================
        #region Adding OA3Tool
        Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] Adding OA3Tool to WinPE for Autopilot Hash Generation"
        $OA3ToolPath = $global:BuildMedia.AdkPaths.oa3toolexe
        if (Test-Path $OA3ToolPath) {
            Copy-Item -Path $OA3ToolPath -Destination "$MountPath\Windows\System32\oa3tool.exe" -Force -ErrorAction SilentlyContinue | Out-Null
            Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] Source: $OA3ToolPath"
        }
        #endregion
        #=================================================
        Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] Set WinPE TimeZone and International settings"
        Step-BuildMediaDismSettings
        # Step-BuildMediaAddWallpaper
        Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] Adding WinPE PowerShell Gallery support"
        Step-BuildMediaPowerShellUpdate
        Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] Adding WinPE Tools"
        Step-WinPEAppAzCopy
        Step-WinPEAppCurl
        Step-WinPEAppZip
        <#
        Step-BuildMediaWinPEAppScript
        Step-BuildMediaWindowsImageSave
        Step-BuildMediaRemoveWinpeshl
        Step-BuildMediaConsoleSettings
        Step-BuildMediaWinPEScript
        Step-BuildMediaWinPEDriver
        Step-BuildMediaExportWindowsDriverPE
        Step-BuildMediaExportWindowsPackagePE
        Step-BuildMediaRegCurrentVersionExport
        Step-BuildMediaDismGetIntl
        Step-BuildMediaGetContentStartnet
        Step-BuildMediaGetContentWinpeshl
        Step-BuildMediaWindowsImageDismount
        Step-BuildMediaWindowsImageExport
        Step-BuildMediaWinPEMediaScript
        Step-BuildMediaIso
        Step-BuildMediaUpdateUSB
        #>

        #=================================================
        # Add PowerShell Modules to BootImage
        # Copy-PSModuleToWindowsImage -Name OSDCloud -Path $MountPath
        # Copy-PSModuleToWindowsImage -Name OSDeployMDT -Path $MountPath
        Save-Module -Name OSDCloud -Path "$MountPath\Program Files\WindowsPowerShell\Modules"
        #=================================================
        #region WinPE Driver Log
        $WinPEDriverLogPath = Join-Path $MountPath 'winpe-drivers.json'
        $DeployRootDriverLogPath = Join-Path $env:DEPLOYROOT 'Boot\winpe-drivers.json'
        if (Test-Path -LiteralPath $WinPEDriverLogPath) {
            try {
                $AppliedDrivers = Get-Content -LiteralPath $WinPEDriverLogPath -Raw | ConvertFrom-Json
                if ($null -eq $AppliedDrivers) { $AppliedDrivers = @() }
            }
            catch {
                Write-Warning "[$(Get-Date -format s)] Could not read $WinPEDriverLogPath - starting fresh"
                $AppliedDrivers = @()
            }
            Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] WinPE driver log found: $(@($AppliedDrivers).Count) previously applied driver(s)"
        }
        else {
            Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] No WinPE driver log found - fresh build, resetting log"
            $AppliedDrivers = @()
            if (Test-Path -LiteralPath $DeployRootDriverLogPath) {
                Remove-Item -LiteralPath $DeployRootDriverLogPath -Force -ErrorAction SilentlyContinue
                Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] Removed $DeployRootDriverLogPath"
            }
        }
        #endregion
        #=================================================
        #region Add Drivers to BootImage - DEPLOYROOT (automatic)
        $DeployRootDrivers = Get-ChildItem -Path "$env:DEPLOYROOT\Templates\winpe-drivers\*" -ErrorAction SilentlyContinue |
            Where-Object { $_.PSIsContainer -eq $true }
        foreach ($Driver in $DeployRootDrivers) {
            if ($AppliedDrivers.Name -contains $Driver.Name) {
                Write-Host -ForegroundColor Yellow "[$(Get-Date -format s)] Already applied, skipping: $($Driver.FullName)"
            }
            else {
                Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] MDT Deployment Share WinPE Drivers"
                Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] Adding $($Driver.FullName) to WinPE"
                Add-WindowsDriver -Driver $Driver.FullName -ForceUnsigned -Recurse -Path $MountPath
                $AppliedDrivers += [PSCustomObject]@{
                    Type          = 'winpe-driver'
                    Name          = $Driver.Name
                    Architecture  = 'amd64'
                    FullName      = $Driver.FullName
                    LastWriteTime = $Driver.LastWriteTime.ToString('s')
                    AppliedAt     = (Get-Date -Format 's')
                }
            }
        }
        #endregion
        #=================================================
        #region Add Drivers to BootImage - ProgramData (interactive)
        $ProgramDataDrivers = Get-ChildItem -Path "$script:OSDeployCoreRepositoryPath\winpe-drivers\amd64\*" -ErrorAction SilentlyContinue |
            Where-Object { $_.PSIsContainer -eq $true }
        $AvailableDrivers = foreach ($Driver in $ProgramDataDrivers) {
            if ($AppliedDrivers.Name -contains $Driver.Name) {
                Write-Host -ForegroundColor Yellow "[$(Get-Date -format s)] Already applied, skipping: $($Driver.FullName)"
            }
            else {
                [PSCustomObject]@{
                    Type          = 'winpe-driver'
                    Name          = $Driver.Name
                    Architecture  = 'amd64'
                    FullName      = $Driver.FullName
                    LastWriteTime = $Driver.LastWriteTime
                }
            }
        }
        if ($AvailableDrivers) {
            Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] OSDeployCore WinPE Drivers"
            $SelectedDrivers = $AvailableDrivers | Out-GridView -Passthru -Title 'OSDeployCore: Select WinPE Drivers to add to this BootImage (Cancel to skip)'
            foreach ($Driver in $SelectedDrivers) {
                Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] Adding $($Driver.FullName) to WinPE"
                Add-WindowsDriver -Driver $Driver.FullName -ForceUnsigned -Recurse -Path $MountPath
                $AppliedDrivers += [PSCustomObject]@{
                    Type          = 'winpe-driver'
                    Name          = $Driver.Name
                    Architecture  = 'amd64'
                    FullName      = $Driver.FullName
                    LastWriteTime = $Driver.LastWriteTime.ToString('s')
                    AppliedAt     = (Get-Date -Format 's')
                }
            }
        }
        #endregion
        #=================================================
        #region Write WinPE Driver Log
        if ($AppliedDrivers) {
            $AppliedDrivers | ConvertTo-Json -Depth 3 | Set-Content -LiteralPath $WinPEDriverLogPath -Encoding UTF8
            Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] WinPE driver log written: $WinPEDriverLogPath"
            Copy-Item -LiteralPath $WinPEDriverLogPath -Destination $DeployRootDriverLogPath -Force -ErrorAction SilentlyContinue
            Write-Host -ForegroundColor Green "[$(Get-Date -format s)] $WinPEDriverLogPath -> $DeployRootDriverLogPath"
        }
        #endregion
    }
    if ($Env:STAGE -eq "POSTWIM") {
    }
    if ($Env:STAGE -eq "ISO") {
    }
    if ($Env:STAGE -eq "POSTISO") {
        #=================================================
        #region Resolve paths
        $TempRoot     = [System.IO.Path]::GetDirectoryName($Env:CONTENT)
        $IsoFolder    = Join-Path $TempRoot 'ISO'
        $IsoBaseName  = [System.IO.Path]::GetFileNameWithoutExtension($Env:CONTENT)
        $PatchIsoPath = Join-Path $env:DEPLOYROOT "Boot\${IsoBaseName}_uefi2023ca.iso"
        Write-Verbose "[$(Get-Date -format s)] ISO folder : $IsoFolder"
        Write-Verbose "[$(Get-Date -format s)] Patched ISO : $PatchIsoPath"
        #endregion
        #=================================================
        #region Copy bootmgfw_EX.efi into ISO folder
        Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] Build ISO for Microsoft Windows UEFI CA 2023 Compliance"
        $BootBinsPath = $global:BuildMedia.BootBinsPath
        $EfiBootDir   = Join-Path $IsoFolder 'EFI\MICROSOFT\BOOT'
        $EfiSrc       = Join-Path $BootBinsPath 'bootmgfw_EX.efi'
        $EfiDest      = Join-Path $EfiBootDir 'bootmgfw.efi'
        if (Test-Path -LiteralPath $EfiSrc) {
            if (-not (Test-Path -LiteralPath $EfiBootDir)) {
                New-Item -Path $EfiBootDir -ItemType Directory -Force | Out-Null
            }
            Copy-Item -LiteralPath $EfiSrc -Destination $EfiDest -Force
            Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [MDT Deployment Share] -> $EfiDest"
        }
        else {
            Write-Warning "[$(Get-Date -format s)] bootmgfw_EX.efi not found at $EfiSrc - skipping EFI patch"
        }
        #endregion
        #=================================================
        #region Build patched ISO with oscdimg
        $oscdimgexe   = $global:BuildMedia.AdkPaths.oscdimgexe
        $etfsbootcom  = $global:BuildMedia.AdkPaths.etfsbootcom
        # DEPLOYROOT\Boot\bootbins\efisys_EX.bin is a renamed copy of efisys.bin that we use to patch the ISO's EFI boot image without affecting the original efisys.bin (which is used for the WIM's EFI boot image)
        $efisysbin    = Join-Path $BootBinsPath 'efisys_EX.bin'
        $BootOrderTxt = Join-Path $Env:INSTALLDIR 'Templates\BootOrder.txt'

        if (Test-Path -LiteralPath $oscdimgexe) {
            Write-Verbose "[$(Get-Date -format s)] Build patched ISO"
            $OscdimgArgs = "-u2 -udfver102 -m -o -h -w4 -yo`"$BootOrderTxt`" -bootdata:2#p0,e,b`"$etfsbootcom`"#pEF,e,b`"$efisysbin`" `"$IsoFolder`" `"$PatchIsoPath`""
            # Write-Host "$OscdimgArgs"
            Start-Process -FilePath $oscdimgexe -ArgumentList $OscdimgArgs -Wait
            Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [TEMP ISO Folder] -> $PatchIsoPath"
            Write-Host -ForegroundColor Green "[$(Get-Date -format s)] Windows UEFI 2023 CA signed MDT boot image is created in the MDT Deployment Share Boot folder"
        }
        else {
            Write-Warning "[$(Get-Date -format s)] oscdimg.exe not found: $oscdimgexe"
        }
        #endregion
    }
    #=================================================
    Start-Sleep -Seconds 10
    if ($PauseOnExit) {
        Write-Host -ForegroundColor Yellow "[$(Get-Date -format s)] Pausing for $PauseOnExit seconds before exiting..."
        # Press Enter to continue immediately:
        $null = Read-Host "Press Enter to continue"
    }
    #=================================================
    <#
    UpdateExit Example Content:
        Microsoft (R) Windows Script Host Version 10.0
        Copyright (C) Microsoft Corporation. All rights reserved.
 
        ADKPath=C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit
        INSTALLDIR = C:\Program Files\Microsoft Deployment Toolkit
        DEPLOYROOT = C:\DeploymentShare
        PLATFORM = x64
        ARCHITECTURE = amd64
        TEMPLATE = LiteTouchPE
        STAGE = WIM
        CONTENT = C:\Users\DAVIDS~1\AppData\Local\Temp\MDTUpdate.79864\Mount
 
        Exit code = 0
 
    Environment Variables:
 
        INSTALLDIR = Path to MDT Installation, typically "C:\Program Files\Microsoft Deployment Toolkit"
        DEPLOYROOT = Path to MDT Deployment Share
        PLATFORM = x86 or x64
        ARCHITECTURE = amd64
        TEMPLATE = LiteTouchPE or Generic
 
        STAGE = WIM
        CONTENT = Path to mounted WIM
        Do any desired WIM customizations (right before the WIM changes are committed)
        Example: "C:\Users\DAVIDS~1\AppData\Local\Temp\MDTUpdate.81804\Mount"
 
        STAGE = POSTWIM
        CONTENT = Path to the locally-captured WIM file (after it has been copied to the network)
        Do any steps needed after the WIM has been generated
        Example: "C:\Users\DAVIDS~1\AppData\Local\Temp\MDTUpdate.81804\LiteTouchPE_x64.wim"
 
        STAGE = ISO
        CONTENT = Path to the directory that will be used to create the ISO
        Do any desired ISO customizations (right before a new ISO is captured)
        Example: "C:\Users\DAVIDS~1\AppData\Local\Temp\MDTUpdate.81804\ISO"
 
        STAGE = POSTISO
        CONTENT = Path to the locally-captured ISO file (after it has been copied to the network)
        Do any steps needed after the ISO has been generated
        Example: "C:\Users\DAVIDS~1\AppData\Local\Temp\MDTUpdate.81804\LiteTouchPE_x64.iso"
 
    References:
        https://www.deploymentresearch.com/understanding-the-mdt-lite-touch-exits-feature/
    #>

}