public/Install-OSDeployMDT.ps1
|
function Install-OSDeployMDT { <# .SYNOPSIS Audits or initializes an MDT deployment share for OSDeployMDT .DESCRIPTION Resolves the MDT installation directory, interactively selects a deployment share, and audits the OSDeployMDT template and Settings.xml configuration. Without Force, missing or differing items are reported as skipped and no files are written. With Force, the function creates Templates, Templates\winpe-drivers, and Templates\winpe-extrafiles; overwrites winpeshl.ini, wimscript.ini, Unattend_PE_x64.xml, LiteTouchPE.xml, and background.bmp from the MDT installation; changes the Unattend and winpeshl copy sources in LiteTouchPE.xml from INSTALLDIR to DEPLOYROOT; and adds the Invoke-OSDeployMDT exit command. When Boot\LiteTouchPE_x64.xml exists, its /Definition/WindowsPE/Components node is imported into Templates\LiteTouchPE.xml. When it does not exist, the template receives the function's default component list. Control\Settings.xml is backed up once as Settings.xml.backup, then exact matching values are changed to use the copied background, 512 MB scratch space, the empty x64 extra-directory value is set to Templates\winpe-extrafiles, and enabled x86 settings are changed to False. WhatIf or a declined Confirm stops processing before either audit reporting or changes. .PARAMETER Force Writes the audited deployment-share changes. When omitted, the command only reports existing, missing, and differing items. This switch does not bypass WhatIf or Confirm. .EXAMPLE PS> Install-OSDeployMDT Prompts for a deployment share and audits its OSDeployMDT configuration. .EXAMPLE PS> Install-OSDeployMDT -Force Prompts for a deployment share and writes the required template and Settings.xml changes. .EXAMPLE PS> Install-OSDeployMDT -WhatIf Selects a deployment share and displays the top-level initialization operation without running the audit or changing files. .INPUTS None. This function does not accept pipeline input. .OUTPUTS None. .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, curl.exe, MDT, and Administrator rights. Source files that are absent from the MDT installation produce warnings. XML updates are applied only when the expected nodes or exact source values are present. .LINK Invoke-OSDeployMDT #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [OutputType([void])] param ( [Parameter()] [switch] $Force ) #================================================= # Stop before inspecting or modifying MDT 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 MDT installation paths cannot be resolved. #region Resolve MDT installation directory $installDir = Get-OSDeployMDTInstallDir if ($null -eq $installDir) { Write-Warning "[$(Get-Date -Format s)] MDT installation directory (INSTALLDIR) could not be resolved. Aborting." return } Write-Host "[$(Get-Date -Format s)] INSTALLDIR '$installDir'" -ForegroundColor DarkGray #endregion # Stop when the user cancels deployment-share selection. #region Select the active deployment share $share = Select-OSDeployMDTDeploymentShare if ($null -eq $share) { Write-Warning "[$(Get-Date -Format s)] No deployment share selected. Aborting." return } #endregion $deployRoot = $share.Path # Honor WhatIf and Confirm before changing the selected deployment share. if (-not $PSCmdlet.ShouldProcess($deployRoot, 'Initialize MDT Deployment Share')) { return } Write-Host "[$(Get-Date -Format s)] Initializing MDT DeployRoot $deployRoot" -ForegroundColor DarkCyan #region Ensure Templates folder structure exists $templateFolders = @( (Join-Path -Path $deployRoot -ChildPath 'Templates') (Join-Path -Path $deployRoot -ChildPath 'Templates\winpe-drivers') (Join-Path -Path $deployRoot -ChildPath 'Templates\winpe-extrafiles') ) foreach ($folder in $templateFolders) { if (-not (Test-Path -LiteralPath $folder)) { if ($Force) { New-Item -Path $folder -ItemType Directory -Force | Out-Null Write-Host "[$(Get-Date -Format s)] [NEW] $folder" -ForegroundColor DarkGreen } else { Write-Host "[$(Get-Date -Format s)] [SKIP] $folder (use -Force to create)" -ForegroundColor DarkYellow } } else { Write-Host "[$(Get-Date -Format s)] [OK] $folder" -ForegroundColor DarkGray } } $templatesFolder = $templateFolders[0] #endregion #region Copy template files from INSTALLDIR\Templates to DEPLOYROOT\Templates Write-Host "[$(Get-Date -Format s)] Copy files from $installDir\Templates" -ForegroundColor DarkCyan $templateCopyMap = @( [ordered]@{ SourceName = 'winpeshl.ini'; DestinationName = 'winpeshl.ini' } [ordered]@{ SourceName = 'Wimscript.ini'; DestinationName = 'wimscript.ini' } [ordered]@{ SourceName = 'Unattend_PE_x64.xml'; DestinationName = 'Unattend_PE_x64.xml' } [ordered]@{ SourceName = 'LiteTouchPE.xml'; DestinationName = 'LiteTouchPE.xml' } ) foreach ($copyItem in $templateCopyMap) { $sourcePath = Join-Path -Path $installDir -ChildPath (Join-Path -Path 'Templates' -ChildPath $copyItem.SourceName) $destPath = Join-Path -Path $templatesFolder -ChildPath $copyItem.DestinationName if (Test-Path -LiteralPath $sourcePath) { if ($Force) { Copy-Item -LiteralPath $sourcePath -Destination $destPath -Force Write-Host "[$(Get-Date -Format s)] [COPY] $destPath" -ForegroundColor DarkGreen } elseif (-not (Test-Path -LiteralPath $destPath)) { Write-Host "[$(Get-Date -Format s)] [SKIP] $destPath (use -Force to copy)" -ForegroundColor DarkYellow } else { Write-Host "[$(Get-Date -Format s)] [OK] $destPath" -ForegroundColor DarkGray } } else { Write-Warning "[$(Get-Date -Format s)] Source file not found: '$sourcePath'" } } $destXml = Join-Path -Path $templatesFolder -ChildPath 'LiteTouchPE.xml' #endregion #region Copy Background.bmp from INSTALLDIR\Samples to DEPLOYROOT\Templates (no overwrite) Write-Host "[$(Get-Date -Format s)] Copy files from $installDir\Samples" -ForegroundColor DarkCyan $sourceBackground = Join-Path -Path $installDir -ChildPath 'Samples\Background.bmp' $destBackground = Join-Path -Path $templatesFolder -ChildPath 'background.bmp' if (Test-Path -Path $sourceBackground) { if ($Force) { Copy-Item -Path $sourceBackground -Destination $destBackground -Force Write-Host "[$(Get-Date -Format s)] [COPY] $destBackground" -ForegroundColor DarkGreen } elseif (-not (Test-Path -Path $destBackground)) { Write-Host "[$(Get-Date -Format s)] [SKIP] $destBackground (use -Force to copy)" -ForegroundColor DarkYellow } else { Write-Host "[$(Get-Date -Format s)] [OK] $destBackground" -ForegroundColor DarkGray } } else { Write-Warning "[$(Get-Date -Format s)] Source file not found: '$sourceBackground'" } #endregion Write-Host "[$(Get-Date -Format s)] Update $destXml" -ForegroundColor DarkCyan #region Rewrite INSTALLDIR -> DEPLOYROOT for Unattend and winpeshl Copy sources in LiteTouchPE.xml # Write-Host "[$(Get-Date -Format s)] Update INSTALLDIR to DEPLOYROOT in $destXml" -ForegroundColor DarkCyan if (Test-Path -Path $destXml) { try { $xmlContent = Get-Content -Path $destXml -Raw $changed = $false $replacements = @( @{ Find = '%INSTALLDIR%\Templates\Unattend_PE_%PLATFORM%.xml'; Replace = '%DEPLOYROOT%\Templates\Unattend_PE_%PLATFORM%.xml' } @{ Find = '%INSTALLDIR%\Templates\winpeshl.ini'; Replace = '%DEPLOYROOT%\Templates\winpeshl.ini' } ) foreach ($r in $replacements) { if ($xmlContent -match [regex]::Escape($r.Find)) { if ($Force) { $xmlContent = $xmlContent -replace [regex]::Escape($r.Find), $r.Replace Write-Host "[$(Get-Date -Format s)] [UPDATE] $($r.Replace)" -ForegroundColor DarkGreen $changed = $true } else { Write-Host "[$(Get-Date -Format s)] [SKIP] $($r.Replace) (use -Force to update)" -ForegroundColor DarkYellow } } else { Write-Host "[$(Get-Date -Format s)] [OK] $($r.Replace)" -ForegroundColor DarkGray } } if ($changed) { Set-Content -Path $destXml -Value $xmlContent -Encoding UTF8 -NoNewline } } catch { Write-Warning "[$(Get-Date -Format s)] Failed to update Copy sources in '$destXml': $_" } } #endregion #region Mirror Components from LiteTouchPE_x64.xml into Templates\LiteTouchPE.xml # Write-Host "[$(Get-Date -Format s)] Update WindowsPE Components in $destXml" -ForegroundColor DarkCyan $bootXmlPath = Join-Path -Path $deployRoot -ChildPath 'Boot\LiteTouchPE_x64.xml' if ((Test-Path -Path $bootXmlPath) -and (Test-Path -Path $destXml)) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Merging Components from $bootXmlPath" try { $bootXml = New-Object -TypeName System.Xml.XmlDocument $bootXml.Load($bootXmlPath) $templateXml = New-Object -TypeName System.Xml.XmlDocument $templateXml.Load($destXml) $bootComponents = $bootXml.SelectSingleNode('/Definition/WindowsPE/Components') if ($null -ne $bootComponents) { $templateComponents = $templateXml.SelectSingleNode('/Definition/WindowsPE/Components') # Import the node from the boot XML document into the template document $importedNode = $templateXml.ImportNode($bootComponents, $true) if ($null -ne $templateComponents) { # Replace existing Components node $templateComponents.ParentNode.ReplaceChild($importedNode, $templateComponents) | Out-Null } else { # Append Components under WindowsPE if not present $windowsPE = $templateXml.SelectSingleNode('/Definition/WindowsPE') if ($null -ne $windowsPE) { $windowsPE.AppendChild($importedNode) | Out-Null } } if ($Force) { $templateXml.Save($destXml) Write-Host "[$(Get-Date -Format s)] [MERGE] Existing WindowsPE Components from Boot\LiteTouchPE_x64.xml" -ForegroundColor DarkGreen } else { Write-Host "[$(Get-Date -Format s)] [SKIP] WindowsPE Components from Boot\LiteTouchPE_x64.xml (use -Force to merge)" -ForegroundColor DarkYellow } } else { Write-Host "[$(Get-Date -Format s)] [OK] No Components element found in Boot\LiteTouchPE_x64.xml" -ForegroundColor DarkGray } } catch { Write-Warning "[$(Get-Date -Format s)] Failed to merge Components from LiteTouchPE_x64.xml: $_" } } elseif (-not (Test-Path -Path $bootXmlPath)) { # Write-Host "[$(Get-Date -Format s)] '$bootXmlPath' not found - writing default Components" -ForegroundColor DarkGray if (Test-Path -Path $destXml) { try { $defaultComponents = @( 'winpe-scripting' 'winpe-hta' 'winpe-wmi' 'winpe-securestartup' 'winpe-fmapi' 'winpe-dismcmdlets' 'winpe-dot3svc' 'winpe-enhancedstorage' 'winpe-hsp-driver' 'winpe-netfx' 'winpe-platformid' 'winpe-pmemcmdlets' 'winpe-powershell' 'winpe-pppoe' 'winpe-rndis' 'winpe-securebootcmdlets' 'winpe-storagewmi' 'winpe-wds-tools' ) $templateXml = New-Object -TypeName System.Xml.XmlDocument $templateXml.Load($destXml) # Locate or create the Components node under /Definition/WindowsPE $templateComponents = $templateXml.SelectSingleNode('/Definition/WindowsPE/Components') if ($null -eq $templateComponents) { $windowsPE = $templateXml.SelectSingleNode('/Definition/WindowsPE') if ($null -ne $windowsPE) { $templateComponents = $templateXml.CreateElement('Components') $windowsPE.AppendChild($templateComponents) | Out-Null } } if ($null -ne $templateComponents) { # Remove any existing Component children before writing defaults $templateComponents.RemoveAll() foreach ($name in $defaultComponents) { $componentNode = $templateXml.CreateElement('Component') $componentNode.InnerText = $name $templateComponents.AppendChild($componentNode) | Out-Null } if ($Force) { $templateXml.Save($destXml) Write-Host "[$(Get-Date -Format s)] [UPDATE] WindowsPE Components updated in $destXml" -ForegroundColor DarkGreen } else { Write-Host "[$(Get-Date -Format s)] [SKIP] WindowsPE default Components (use -Force to update)" -ForegroundColor DarkYellow } } } catch { Write-Warning "[$(Get-Date -Format s)] Failed to update default Components to template: $_" } } } #endregion #region Ensure Invoke-OSDeployMDT Exit entry exists in LiteTouchPE.xml # Write-Host "[$(Get-Date -Format s)] Update Exits in $destXml" -ForegroundColor DarkCyan $buildMdtExit = 'start /wait pwsh.exe -ExecutionPolicy Bypass -Command "Invoke-OSDeployMDT"' if (Test-Path -Path $destXml) { try { $exitXml = New-Object -TypeName System.Xml.XmlDocument $exitXml.Load($destXml) $exitsNode = $exitXml.SelectSingleNode('/Definition/WindowsPE/Exits') if ($null -eq $exitsNode) { $windowsPE = $exitXml.SelectSingleNode('/Definition/WindowsPE') if ($null -ne $windowsPE) { $exitsNode = $exitXml.CreateElement('Exits') $windowsPE.AppendChild($exitsNode) | Out-Null } } if ($null -ne $exitsNode) { # Check if the entry already exists $existing = $exitsNode.SelectSingleNode("Exit[. = '$buildMdtExit']") if ($null -eq $existing) { if ($Force) { $exitNode = $exitXml.CreateElement('Exit') $exitNode.InnerText = $buildMdtExit $exitsNode.AppendChild($exitNode) | Out-Null $exitXml.Save($destXml) Write-Host "[$(Get-Date -Format s)] [ADD] start /wait pwsh.exe -ExecutionPolicy Bypass -Command `"Invoke-OSDeployMDT`"" -ForegroundColor DarkGreen } else { Write-Host "[$(Get-Date -Format s)] [SKIP] Invoke-OSDeployMDT Exit entry (use -Force to add)" -ForegroundColor DarkYellow } } else { Write-Host "[$(Get-Date -Format s)] [OK] start /wait pwsh.exe -ExecutionPolicy Bypass -Command `"Invoke-OSDeployMDT`"" -ForegroundColor DarkGray } } } catch { Write-Warning "[$(Get-Date -Format s)] Failed to update Exits in '$destXml': $_" } } #endregion #region Update DEPLOYROOT\Control\Settings.xml $settingsXmlPath = Join-Path -Path $deployRoot -ChildPath 'Control\Settings.xml' if (Test-Path -Path $settingsXmlPath) { #region Backup Settings.xml (no overwrite) $settingsBackupPath = Join-Path -Path $deployRoot -ChildPath 'Control\Settings.xml.backup' if (-not (Test-Path -Path $settingsBackupPath)) { if ($Force) { Copy-Item -Path $settingsXmlPath -Destination $settingsBackupPath Write-Host "[$(Get-Date -Format s)] [BACKUP] $settingsBackupPath" -ForegroundColor DarkGreen } else { Write-Host "[$(Get-Date -Format s)] [SKIP] $settingsBackupPath (use -Force to create)" -ForegroundColor DarkYellow } } else { Write-Host "[$(Get-Date -Format s)] [OK] $settingsBackupPath" -ForegroundColor DarkGray } #endregion Write-Host "[$(Get-Date -Format s)] Update x64 Settings in $settingsXmlPath" -ForegroundColor DarkCyan try { $settingsContent = Get-Content -Path $settingsXmlPath -Raw $changed = $false $replacements = @( @{ Find = '%INSTALLDIR%\Samples\Background.bmp'; Replace = '%DEPLOYROOT%\Templates\background.bmp'; IsRegex = $false } @{ Find = '(?s)<Boot\.x64\.ExtraDirectory>\s*</Boot\.x64\.ExtraDirectory>'; Replace = '<Boot.x64.ExtraDirectory>%DEPLOYROOT%\Templates\winpe-extrafiles</Boot.x64.ExtraDirectory>'; IsRegex = $true; ExistsPattern = '<Boot\.x64\.ExtraDirectory>' } @{ Find = '<Boot.x64.ScratchSpace>32</Boot.x64.ScratchSpace>'; Replace = '<Boot.x64.ScratchSpace>512</Boot.x64.ScratchSpace>'; IsRegex = $false } ) foreach ($r in $replacements) { $pattern = if ($r.IsRegex) { $r.Find } else { [regex]::Escape($r.Find) } if ($settingsContent -match $pattern) { if ($Force) { $settingsContent = $settingsContent -replace $pattern, $r.Replace Write-Host "[$(Get-Date -Format s)] [UPDATE] $($r.Replace)" -ForegroundColor DarkGreen $changed = $true } else { Write-Host "[$(Get-Date -Format s)] [SKIP] $($r.Replace) (use -Force to update)" -ForegroundColor DarkYellow } } else { if ($r.ExistsPattern -and ($settingsContent -match $r.ExistsPattern)) { Write-Host "[$(Get-Date -Format s)] Not modifying '$($r.Find)' - already has a value" -ForegroundColor DarkGray } else { Write-Host "[$(Get-Date -Format s)] [OK] $($r.Replace)" -ForegroundColor DarkGray } } } if ($changed) { Set-Content -Path $settingsXmlPath -Value $settingsContent -Encoding UTF8 -NoNewline } } catch { Write-Warning "[$(Get-Date -Format s)] Failed to update '$settingsXmlPath': $_" } Write-Host "[$(Get-Date -Format s)] Disable x86 Settings in $settingsXmlPath" -ForegroundColor DarkCyan try { $settingsContent = Get-Content -Path $settingsXmlPath -Raw $changed = $false $replacements = @( @{ Find = '<SupportX86>True</SupportX86>'; Replace = '<SupportX86>False</SupportX86>'; IsRegex = $false } @{ Find = '<Boot.x86.UseBootWim>True</Boot.x86.UseBootWim>'; Replace = '<Boot.x86.UseBootWim>False</Boot.x86.UseBootWim>'; IsRegex = $false } @{ Find = '<Boot.x86.IncludeAllDrivers>True</Boot.x86.IncludeAllDrivers>'; Replace = '<Boot.x86.IncludeAllDrivers>False</Boot.x86.IncludeAllDrivers>'; IsRegex = $false } @{ Find = '<Boot.x86.IncludeNetworkDrivers>True</Boot.x86.IncludeNetworkDrivers>'; Replace = '<Boot.x86.IncludeNetworkDrivers>False</Boot.x86.IncludeNetworkDrivers>';IsRegex = $false } @{ Find = '<Boot.x86.IncludeMassStorageDrivers>True</Boot.x86.IncludeMassStorageDrivers>';Replace = '<Boot.x86.IncludeMassStorageDrivers>False</Boot.x86.IncludeMassStorageDrivers>';IsRegex = $false } @{ Find = '<Boot.x86.IncludeVideoDrivers>True</Boot.x86.IncludeVideoDrivers>'; Replace = '<Boot.x86.IncludeVideoDrivers>False</Boot.x86.IncludeVideoDrivers>'; IsRegex = $false } @{ Find = '<Boot.x86.IncludeSystemDrivers>True</Boot.x86.IncludeSystemDrivers>'; Replace = '<Boot.x86.IncludeSystemDrivers>False</Boot.x86.IncludeSystemDrivers>'; IsRegex = $false } @{ Find = '<Boot.x86.GenerateLiteTouchISO>True</Boot.x86.GenerateLiteTouchISO>'; Replace = '<Boot.x86.GenerateLiteTouchISO>False</Boot.x86.GenerateLiteTouchISO>'; IsRegex = $false } ) foreach ($r in $replacements) { $pattern = if ($r.IsRegex) { $r.Find } else { [regex]::Escape($r.Find) } if ($settingsContent -match $pattern) { if ($Force) { $settingsContent = $settingsContent -replace $pattern, $r.Replace Write-Host "[$(Get-Date -Format s)] [DISABLE] $($r.Replace)" -ForegroundColor DarkGray $changed = $true } else { Write-Host "[$(Get-Date -Format s)] [SKIP] $($r.Replace) (use -Force to disable)" -ForegroundColor DarkGray } } else { if ($r.ExistsPattern -and ($settingsContent -match $r.ExistsPattern)) { Write-Host "[$(Get-Date -Format s)] Not modifying '$($r.Find)' - already has a value" -ForegroundColor DarkGray } else { Write-Host "[$(Get-Date -Format s)] [OK] $($r.Replace)" -ForegroundColor DarkGray } } } if ($changed) { Set-Content -Path $settingsXmlPath -Value $settingsContent -Encoding UTF8 -NoNewline } } catch { Write-Warning "[$(Get-Date -Format s)] Failed to update '$settingsXmlPath': $_" } } else { Write-Host "[$(Get-Date -Format s)] Settings.xml not found" -ForegroundColor DarkGray } #endregion if ($Force) { Write-Host Write-Host "🚗 WinPE Drivers can be added to $templatesFolder\winpe-drivers" -ForegroundColor DarkCyan Write-Host "➕ WinPE ExtraFiles can be added to $templatesFolder\winpe-extrafiles" -ForegroundColor DarkCyan Write-Host "🧱 Wallpaper can be customized by replacing $destBackground" -ForegroundColor DarkCyan Write-Host "⚙️ Invoke-OSDeployMDT will run when running Update Deployment Share" -ForegroundColor DarkCyan Write-Host "🎉 PowerShell Gallery will now work in your bootimage" -ForegroundColor DarkCyan Write-Host "☁️ OSDCloud will now work in your bootimage by running Deploy-OSDCloud in PowerShell" -ForegroundColor DarkCyan } } |