public/Invoke-OSDeployMDT.ps1
|
function Invoke-OSDeployMDT { <# .SYNOPSIS Applies OSDeploy actions at recognized MDT LiteTouchPE exit stages .DESCRIPTION Reads the MDT STAGE environment variable and recognizes WIM, POSTWIM, ISO, and POSTISO. When STAGE is absent, the command writes its own help object and returns. POSTWIM and ISO currently perform no stage-specific work. Unrecognized nonempty values also perform no stage-specific work. WIM creates a process-global build context from MDT environment values, stages current boot files, adds OA3Tool and WinPE tools, applies international settings, saves OSDCloud, automatically applies deployment-share template drivers, optionally presents Boot-Assets drivers in Out-GridView, and writes the applied-driver log. POSTISO patches the temporary ISO tree with the CA 2023 EFI file and uses oscdimg to create a *_uefi2023ca.iso in the deployment-share Boot directory. The command assumes it is called by the MDT LiteTouchPE exit process. It displays ADKPath, INSTALLDIR, DEPLOYROOT, TEMPLATE, STAGE, and CONTENT; uses ARCHITECTURE in the build context; uses TEMP and ProgramData for cache paths; and relies on MDT paths and files represented by those values. WhatIf and Confirm gate WIM and POSTISO stage changes. .PARAMETER SetInputLocale Specifies the WinPE input locale. When omitted or null, the function uses en-us. .PARAMETER SetTimeZone Specifies a WinPE timezone validated against tzutil /l. The default is the current timezone returned by tzutil /g. .EXAMPLE PS> Invoke-OSDeployMDT Writes the command's help object and returns when STAGE is not defined. .EXAMPLE PS> $env:STAGE = 'WIM'; Invoke-OSDeployMDT -SetTimeZone 'Romance Standard Time' Runs WIM-stage customization with the supplied timezone when the remaining MDT environment variables and mounted image are available. .EXAMPLE PS> $env:STAGE = 'POSTISO'; Invoke-OSDeployMDT Builds the CA 2023 ISO variant when MDT has supplied the required POSTISO environment. .INPUTS None. This function does not accept pipeline input. .OUTPUTS System.Management.Automation.HelpInfo. Returns command help when STAGE is not set. Microsoft.Dism.Commands.BasicDriverObject. WIM-stage Add-WindowsDriver calls can emit driver servicing objects. Other delegated commands can emit incidental success-stream output; the process-global build context is not returned intentionally. .NOTES Author: David Segura Company: Recast Software Version: 1.0.0 Date: 2026-08-28 Requires MDT, Windows ADK with the WinPE add-on, DISM, and the MDT exit-process environment described above. The function does not validate every environment value before using it. The function replaces $global:BuildMedia on every invocation with a nonempty STAGE. After stage processing it waits ten seconds. If a truthy PauseOnExit variable exists in caller-visible scope, it also prompts with Read-Host; PauseOnExit is not a parameter. .LINK Install-OSDeployMDT .LINK https://www.deploymentresearch.com/understanding-the-mdt-lite-touch-exits-feature/ #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param ( # Uses en-us when no input locale is supplied. [System.String] $SetInputLocale, # Uses the current system timezone by default. [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-HostOSDeployBanner #================================================= # Display command help instead of modifying content outside an MDT update stage. 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-OSDeployMDTWindowsAdkPaths 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 WinPEApp = $WinPEApp WinPEScript = $WinPEScript MediaScript = $BuildMediaScript 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 #================================================= # Customize only the mounted WinPE image during the WIM stage. if ($Env:STAGE -eq "WIM") { # Honor WhatIf and Confirm before changing the mounted WIM. if (-not $PSCmdlet.ShouldProcess($MountPath, 'Apply MDT WIM stage customizations')) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] WIM stage skipped by WhatIf/Confirm" } else { #================================================= #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 "[$($MyInvocation.MyCommand.Name)] 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)] [INFO] Source: $($EfiFile.Source)" } else { Write-Verbose "[$($MyInvocation.MyCommand.Name)] 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)] [INFO] Source: $SourceFile" } else { Write-Verbose "[$($MyInvocation.MyCommand.Name)] 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)] [INFO] Source: $OA3ToolPath" } #endregion #================================================= Write-Host -ForegroundColor DarkCyan "[$(Get-Date -Format s)] Set WinPE TimeZone and International settings" Step-OSDeployMDTDismSettings # Step-OSDeployMDTAddWallpaper Write-Host -ForegroundColor DarkCyan "[$(Get-Date -Format s)] Adding WinPE PowerShell Gallery support" Step-OSDeployMDTPowerShellUpdate Write-Host -ForegroundColor DarkCyan "[$(Get-Date -Format s)] Adding WinPE Tools" Step-OSDeployMDTInstallAppAzCopy Step-OSDeployMDTInstallAppCurl Step-OSDeployMDTInstallAppZip <# Step-BuildMediaWinPEApp 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)] [INFO] WinPE driver log found: $(@($AppliedDrivers).Count) previously applied driver(s)" } else { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] 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)] [INFO] 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)] [INFO] 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) $ProgramDataDriverPaths = @( "$script:OSDeployBootAssetsPath\winpedrivers-amd64\*" ) $ProgramDataDrivers = Get-ChildItem -Path $ProgramDataDriverPaths -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 } } } # Prompt only when unapplied Boot-Assets drivers are available. if ($AvailableDrivers) { Write-Host -ForegroundColor DarkCyan "[$(Get-Date -Format s)] Select WinPE Drivers to add to this build (Cancel to skip)" $SelectedDrivers = $AvailableDrivers | Out-GridView -Passthru -Title 'Select WinPE Drivers to add to this build (Cancel to skip)' foreach ($Driver in $SelectedDrivers) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Adding $($Driver.FullName)" 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)] [INFO] 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") { } # Build the CA 2023 ISO only during the POSTISO stage. if ($Env:STAGE -eq "POSTISO") { # Honor WhatIf and Confirm before patching the ISO and writing its output. if (-not $PSCmdlet.ShouldProcess($env:DEPLOYROOT, 'Apply MDT POSTISO patch and build CA2023 ISO')) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] POSTISO stage skipped by WhatIf/Confirm" } else { #================================================= #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 "[$($MyInvocation.MyCommand.Name)] ISO folder : $IsoFolder" Write-Verbose "[$($MyInvocation.MyCommand.Name)] 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)] [INFO] [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 $global:BuildMedia $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 "[$($MyInvocation.MyCommand.Name)] 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)] [INFO] [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/ #> } |