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/ #> } |