public/Install-OSDeployMDT.ps1

function Install-OSDeployMDT {
    <#
    .SYNOPSIS
        Initializes an MDT Deployment Share for use with OSDeployMDT.
 
    .DESCRIPTION
        Configures an MDT Deployment Share so that Invoke-OSDeployMDT runs automatically
        on every Update Deployment Share operation.
 
        Without -Force, the function runs in audit mode only — it reports the current state of
        the deployment share without making any changes.
 
        With -Force, the function performs the following initialization steps:
          1. Resolves the MDT installation directory (INSTALLDIR) and active Deployment Share (DEPLOYROOT).
          2. Creates Templates\, Templates\winpe-drivers\, and Templates\winpe-extrafiles\ under DEPLOYROOT.
          3. Copies winpeshl.ini, Wimscript.ini, Unattend_PE_x64.xml, and LiteTouchPE.xml
             from INSTALLDIR\Templates\ to DEPLOYROOT\Templates\.
          4. Copies Background.bmp from INSTALLDIR\Samples\ to DEPLOYROOT\Templates\background.bmp.
          5. Rewrites %INSTALLDIR% -> %DEPLOYROOT% references in LiteTouchPE.xml.
          6. Mirrors the <Components> element from DEPLOYROOT\Boot\LiteTouchPE_x64.xml into
             the template (or writes a default component list if the Boot file does not exist).
          7. Registers the Invoke-OSDeployMDT exit command in LiteTouchPE.xml.
          8. Updates Control\Settings.xml: disables x86, sets scratch space to 512 MB, sets
             wallpaper path.
 
        Supports -WhatIf and -Confirm.
 
    .PARAMETER Force
        Performs all initialization changes (creates directories, copies files,
        modifies XML and Settings.xml). Without this switch the function runs
        in audit mode and only reports the current state of the deployment share.
 
    .EXAMPLE
        Install-OSDeployMDT
 
        Selects the active deployment share, resolves INSTALLDIR, then
        creates DEPLOYROOT\Templates and copies LiteTouchPE.xml.
 
    .EXAMPLE
        Install-OSDeployMDT -Force
 
        Same as above, but overwrites any template and background files that
        already exist in DEPLOYROOT\Templates.
 
    .EXAMPLE
        Install-OSDeployMDT -WhatIf
 
        Shows what initialization actions would be performed without making
        any changes.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        None.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([void])]
    param (
        [Parameter()]
        [switch] $Force
    )

    Write-OSDeployBanner
    Write-Host "[$(Get-Date -format s)] Install-OSDeployMDT" -ForegroundColor DarkCyan

    #region Require Administrator rights
    $currentPrincipal = [Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()
    if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        throw "[$(Get-Date -format s)] requires Administrator rights. Please run as Administrator."
    }
    #endregion

    #region Resolve MDT installation directory
    $installDir = Get-MDTInstallDir
    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

    #region Select the active deployment share
    $share = Select-MdtDeploymentShare
    if ($null -eq $share) {
        Write-Warning "[$(Get-Date -format s)] No deployment share selected. Aborting."
        return
    }
    #endregion

    $deployRoot = $share.Path

    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 "[$(Get-Date -format s)] 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
    }
}