private/Step-BuildBootUpdatePowerShell.ps1

#Requires -PSEdition Core

function Step-BuildBootUpdatePowerShell {
    <#
    .SYNOPSIS
        Updates PowerShell components and configuration in the mounted WinPE image
 
    .DESCRIPTION
        Creates or reuses a local package cache, copies default user folders into the
        mounted system profile, and creates required module, script, profile, and
        repository directories. It temporarily registers a trusted OSDeployCore
        repository, downloads configured PackageManagement, PowerShellGet, and NuGet
        packages when absent, installs the modules into the mounted image, and copies
        the cache into the image.
 
        The function also creates a trusted PSGallery configuration and PowerShell
        profile when absent. It loads the mounted DEFAULT hive to set user Path, TEMP,
        and TMP values, then applies unsigned INF files that set system-profile
        environment variables and the PowerShell execution policy to Bypass.
 
    .EXAMPLE
        PS> Step-BuildBootUpdatePowerShell
 
        Updates cached PowerShell packages and configures PowerShell in the mounted image.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.String. Returns unsuppressed native stdout from repository copy and registry commands.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 0.1.0
        Date: 2026-08-28
 
        Requires global BuildMedia.MountPath, global
        OSDeployModule.BootImage.psrepository, and script OSDeployCorePath. Requires
        PowerShellGet repository commands, Save-Module, Add-WindowsDriver, robocopy.exe,
        reg.exe, and network access for cache misses. Modifies the shared cache, current
        session repository registration, mounted files, registry hives, and execution policy.
    #>

    [CmdletBinding()]
    param ()

    $MountPath = $global:BuildMedia.MountPath
    $CachePSRepository = Join-Path $Script:OSDeployCorePath 'cache' 'psrepository'
    $psRepoConfig = $global:OSDeployModule.BootImage.psrepository

    # Ensure cache directory exists
    if (-not (Test-Path -Path $CachePSRepository)) {
        New-Item -Path $CachePSRepository -ItemType Directory -Force | Out-Null
    }

    # Copy User Folders to the System Profile
    Write-HostDateTimeDarkGray 'Copy User Folders to the System Profile'
    $null = robocopy.exe "$MountPath\Users\Default" "$MountPath\Windows\System32\Config\SystemProfile" *.* /e /b /ndl /nfl /np /ts /r:0 /w:0 /xj /xf NTUSER.*

    # Create Required Folders
    $requiredFolders = @(
        "$MountPath\Program Files\WindowsPowerShell\Modules",
        "$MountPath\Program Files\WindowsPowerShell\Scripts",
        "$MountPath\Users\Default\AppData\Local",
        "$MountPath\Users\Default\AppData\Roaming",
        "$MountPath\Users\Default\Desktop",
        "$MountPath\Users\Default\Documents\WindowsPowerShell",
        "$MountPath\Windows\System32\WindowsPowerShell\v1.0\Modules",
        "$MountPath\Windows\System32\WindowsPowerShell\v1.0\Scripts"
    )
    foreach ($item in $requiredFolders) {
        if (-not (Test-Path -Path $item)) {
            New-Item -Path $item -ItemType Directory -Force | Out-Null
        }
    }

    $WinPEPSRepository = "$MountPath\PSRepository"
    if (-not (Test-Path -Path $WinPEPSRepository)) {
        New-Item -Path $WinPEPSRepository -ItemType Directory -Force | Out-Null
    }
    $MountedPSModulesPath = "$MountPath\Program Files\WindowsPowerShell\Modules"

    if (Get-PSRepository -Name OSDeployCore -ErrorAction SilentlyContinue) {
        Unregister-PSRepository -Name OSDeployCore
    }
    Register-PSRepository -Name OSDeployCore -SourceLocation $CachePSRepository -InstallationPolicy Trusted

    #region PackageManagement
    $PackageName = $psRepoConfig.packagemanagement.filename
    $PSRepositoryModule = Join-Path $CachePSRepository $PackageName
    $PSModuleUrl = $psRepoConfig.packagemanagement.url

    if (-not (Test-Path $PSRepositoryModule)) {
        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Downloading $PackageName"
        if (Get-Command 'curl.exe' -ErrorAction SilentlyContinue) {
            & curl.exe --silent --location --retry 5 --continue-at - --output $PSRepositoryModule $PSModuleUrl
        }
        else {
            Invoke-WebRequest -UseBasicParsing -Uri $PSModuleUrl -OutFile $PSRepositoryModule
        }
    }

    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Updating WinPE PowerShell PackageManagement"
    Save-Module -Name PackageManagement -Repository OSDeployCore -Path $MountedPSModulesPath
    #endregion

    #region PowerShellGet
    $PackageName = $psRepoConfig.powershellget.filename
    $PSRepositoryModule = Join-Path $CachePSRepository $PackageName
    $PSModuleUrl = $psRepoConfig.powershellget.url

    if (-not (Test-Path $PSRepositoryModule)) {
        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Downloading $PackageName"
        if (Get-Command 'curl.exe' -ErrorAction SilentlyContinue) {
            & curl.exe --silent --location --retry 5 --continue-at - --output $PSRepositoryModule $PSModuleUrl
        }
        else {
            Invoke-WebRequest -UseBasicParsing -Uri $PSModuleUrl -OutFile $PSRepositoryModule
        }
    }

    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Updating WinPE PowerShell PowerShellGet"
    Save-Module -Name PowerShellGet -Repository OSDeployCore -Path $MountedPSModulesPath
    #endregion

    Unregister-PSRepository -Name OSDeployCore

    #region NuGet
    $PackageName = $psRepoConfig.nuget.filename
    $PSRepositoryModule = Join-Path $CachePSRepository $PackageName
    $PSModuleUrl = $psRepoConfig.nuget.url

    if (-not (Test-Path $PSRepositoryModule)) {
        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Downloading $PackageName"
        if (Get-Command 'curl.exe' -ErrorAction SilentlyContinue) {
            & curl.exe --silent --location --retry 5 --continue-at - --output $PSRepositoryModule $PSModuleUrl
        }
        else {
            Invoke-WebRequest -UseBasicParsing -Uri $PSModuleUrl -OutFile $PSRepositoryModule
        }
    }

    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Updating WinPE NuGet"
    $PSModuleDestination = "$MountPath\Windows\System32\Config\SystemProfile\AppData\Local\Microsoft\Windows\PowerShell\PowerShellGet"
    if (-not (Test-Path $PSModuleDestination)) {
        New-Item -Path $PSModuleDestination -ItemType Directory -Force | Out-Null
    }
    Copy-Item -Path $PSRepositoryModule -Destination $PSModuleDestination -Force
    #endregion

    #region PSRepositories.xml
$PSRepositoriesContent = @'
<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
  <Obj RefId="0">
    <TN RefId="0">
      <T>System.Collections.Hashtable</T>
      <T>System.Object</T>
    </TN>
    <DCT>
      <En>
        <S N="Key">PSGallery</S>
        <Obj N="Value" RefId="1">
          <TN RefId="1">
            <T>Microsoft.PowerShell.Commands.PSRepository</T>
            <T>System.Management.Automation.PSCustomObject</T>
            <T>System.Object</T>
          </TN>
          <MS>
            <S N="Name">PSGallery</S>
            <S N="SourceLocation">https://www.powershellgallery.com/api/v2</S>
            <S N="PublishLocation">https://www.powershellgallery.com/api/v2/package/</S>
            <S N="ScriptSourceLocation">https://www.powershellgallery.com/api/v2/items/psscript</S>
            <S N="ScriptPublishLocation">https://www.powershellgallery.com/api/v2/package/</S>
            <Obj N="Trusted" RefId="2">
              <TN RefId="2">
                <T>System.Management.Automation.SwitchParameter</T>
                <T>System.ValueType</T>
                <T>System.Object</T>
              </TN>
              <ToString>True</ToString>
              <Props>
                <B N="IsPresent">true</B>
              </Props>
            </Obj>
            <B N="Registered">true</B>
            <S N="InstallationPolicy">Trusted</S>
            <S N="PackageManagementProvider">NuGet</S>
            <Obj N="ProviderOptions" RefId="3">
              <TNRef RefId="0" />
              <DCT />
            </Obj>
          </MS>
        </Obj>
      </En>
    </DCT>
  </Obj>
</Objs>
'@


    $PSRepositoriesFile = "$MountPath\Windows\System32\Config\SystemProfile\AppData\Local\Microsoft\Windows\PowerShell\PowerShellGet\PSRepositories.xml"
    if (-not (Test-Path -Path $PSRepositoriesFile)) {
        Write-HostDateTimeDarkGray 'Create PSRepositories.xml and Trust PSGallery'
        $PSRepositoriesContent | Set-Content -Path $PSRepositoriesFile -Encoding utf8 -Force
    }
    #endregion

    #region PowerShell Profile
$PowerShellProfileContent = @'
# OSD PowerShell Profile
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$registryPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment'
$registryPath | ForEach-Object {
    $k = Get-Item $_
    $k.GetValueNames() | ForEach-Object {
        $name = $_
        $value = $k.GetValue($_)
        Set-Item -Path Env:\$name -Value $value
    }
}
'@

    $PowerShellProfileFile = "$MountPath\Windows\System32\WindowsPowerShell\v1.0\profile.ps1"
    if (-not (Test-Path -Path $PowerShellProfileFile)) {
        Write-HostDateTimeDarkGray 'Create OSD PowerShell Profile'
        $PowerShellProfileContent | Set-Content -Path $PowerShellProfileFile -Encoding utf8 -Force
    }
    #endregion

    # Populate WinPE PSRepository
    & robocopy.exe "$CachePSRepository" "$WinPEPSRepository" *.* /e /ndl /nfl /np /njh /njs /r:0 /w:0 /xj

    #region User Environment Variables
    & reg.exe LOAD HKLM\Mount "$MountPath\Windows\System32\Config\DEFAULT"
    Start-Sleep -Seconds 3
    reg.exe add "HKLM\Mount\Environment" /v Path /t REG_SZ /d "X:\Windows\System32\Config\SystemProfile\AppData\Local\Microsoft\WindowsApps" /f
    reg.exe add "HKLM\Mount\Environment" /v TEMP /t REG_SZ /d "X:\Windows\Temp" /f
    reg.exe add "HKLM\Mount\Environment" /v TMP /t REG_SZ /d "X:\Windows\Temp" /f
    Start-Sleep -Seconds 3
    & reg.exe UNLOAD HKLM\Mount
    Start-Sleep -Seconds 3
    #endregion

    #region WinPE Environment Variables INF
$InfEnvironment = @'
[Version]
Signature = "$WINDOWS NT$"
Class = System
ClassGuid = {4D36E97d-E325-11CE-BFC1-08002BE10318}
Provider = OSDeploy
DriverVer = 01/29/2026,2026.01.29.0
 
[DefaultInstall]
AddReg = AddReg
 
[AddReg]
;rootkey,[subkey],[value],[flags],[data]
;0x00000 REG_SZ
;0x00001 REG_BINARY
;0x10000 REG_MULTI_SZ
;0x20000 REG_EXPAND_SZ
;0x10001 REG_DWORD
;0x20001 REG_NONE
HKLM,"SYSTEM\ControlSet001\Control\Session Manager\Environment",APPDATA,0x00000,"X:\Windows\System32\Config\SystemProfile\AppData\Roaming"
HKLM,"SYSTEM\ControlSet001\Control\Session Manager\Environment",HOMEDRIVE,0x00000,"X:"
HKLM,"SYSTEM\ControlSet001\Control\Session Manager\Environment",HOMEPATH,0x00000,"\Windows\System32\Config\SystemProfile"
HKLM,"SYSTEM\ControlSet001\Control\Session Manager\Environment",LOCALAPPDATA,0x00000,"X:\Windows\System32\Config\SystemProfile\AppData\Local"
HKLM,"SYSTEM\ControlSet001\Control\Session Manager\Environment",USERDATA,0x00000,"X:\Windows\System32\Config\SystemProfile"
'@

    Write-HostDateTimeDarkGray 'PowerShell: Set WinPE Environment Variables'
    $InfFile = "$env:TEMP\Set-WinPEEnvironment.inf"
    New-Item -Path $InfFile -Force | Out-Null
    Set-Content -Path $InfFile -Value $InfEnvironment -Encoding Unicode -Force
    $null = Add-WindowsDriver -Path $MountPath -Driver $InfFile -ForceUnsigned
    #endregion

    #region WinPE Execution Policy INF
$InfExecutionPolicy = @'
[Version]
Signature = "$WINDOWS NT$"
Class = System
ClassGuid = {4D36E97d-E325-11CE-BFC1-08002BE10318}
Provider = OSDeploy
DriverVer = 01/29/2026,2026.01.29.0
 
[DefaultInstall]
AddReg = AddReg
 
[AddReg]
;rootkey,[subkey],[value],[flags],[data]
;0x00000 REG_SZ
;0x00001 REG_BINARY
;0x10000 REG_MULTI_SZ
;0x20000 REG_EXPAND_SZ
;0x10001 REG_DWORD
;0x20001 REG_NONE
HKLM,SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell,ExecutionPolicy,0x00000,"Bypass"
'@

    Write-HostDateTimeDarkGray 'PowerShell: Set WinPE PowerShell Execution Policy'
    $InfFile = "$env:TEMP\Set-WinPEExecutionPolicy.inf"
    New-Item -Path $InfFile -Force | Out-Null
    Set-Content -Path $InfFile -Value $InfExecutionPolicy -Encoding Unicode -Force
    $null = Add-WindowsDriver -Path $MountPath -Driver $InfFile -ForceUnsigned
    #endregion
}