private/WinpeStartup/Initialize-WinpeStartupEnvironment.ps1

#requires -Version 5.1

function Initialize-WinpeStartupEnvironment {
    <#
    .SYNOPSIS
        Initializes the WinPE shell environment for OS deployment
 
    .DESCRIPTION
        Creates required shell profile folders, sets process-level environment
        variables, and writes registry keys needed by Windows PE before any
        PowerShell modules or deployment scripts run.
 
        This function is the PowerShell equivalent of the environment-setup
        section in startnet.cmd / ReStartnet.cmd and should be called early
        in the WinPE boot sequence.
 
        Actions performed:
          - Imports X:\WinpeStartup\core\*\*.env into the process environment
          - Creates shell profile directories under SystemDrive
          - Sets APPDATA, HOMEDRIVE, HOMEPATH, LOCALAPPDATA, USERPROFILE
          - Writes the USERDATA registry value to the Session Manager Environment key
          - Sets the PowerShell execution policy to Bypass via registry
          - Imports X:\WinpeStartup\core\*\winpe-reg\*.reg using reg.exe import
          - Adds X:\WinpeStartup\core\*\winpe-root-cer\*.cer to the machine Root store
 
        Core files, including hidden files named .env, are loaded from immediate
        Core subfolders in sorted full-path order. Later assignments overwrite
        earlier values. Shell profile variables and the selected startup profile
        are applied afterward and take precedence over Core values.
 
        Entries use literal, single-line NAME=VALUE syntax. Blank lines and
        full-line # comments are ignored. Surrounding whitespace and matching
        outer quotes are removed; embedded equals signs are preserved. Expressions,
        variable references, escape sequences, and inline comments are not expanded.
        Multiline values and export syntax are not supported. Empty values use
        the current runtime's process environment semantics (removal on PS 5.1).
        Invalid entries and file failures warn and continue without logging values.
        Missing Core content is allowed. Imported values are not persisted.
        Environment import output is summarized by one pass or fail message when
        files are discovered or discovery fails.
 
        After built-in registry setup, Core registry files are imported, followed
        by root certificates using certutil.exe -addstore root. Each file type is
        processed in sorted full-path order, including hidden files, without
        searching deeper folders. Missing import folders are allowed. Discovery,
        process launch, and nonzero exit-code failures warn and continue with
        remaining files. Registry imports may overwrite existing keys; certificates
        are trusted in the local machine Root store for the current WinPE instance.
        Registry and certificate tool output is suppressed and summarized by one
        pass or fail message for each file type.
 
    .EXAMPLE
        PS> Initialize-WinpeStartupEnvironment
 
        Imports Core environment files, creates shell folders, sets environment
        variables, and writes registry keys for the current WinPE session.
 
    .EXAMPLE
        PS> Initialize-WinpeStartupEnvironment -Verbose
 
        Runs the full environment initialization with detailed progress output.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        None.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 1.0.0
        Date: 2026-10-06
        Module: OSDCloud
        Runs only when SystemDrive is X:.
        Core imports require reg.exe and certutil.exe and permission to modify
        the WinPE registry and local machine certificate store.
 
    .LINK
        https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/reg-import
 
    .LINK
        https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/certutil
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param ()

    begin {
        $skipExecution = $false
        if ($env:SystemDrive -ne 'X:') {
            Write-Warning 'Initialize-WinpeStartupEnvironment: Not running in WinPE (SystemDrive is not X:). Exiting.'
            $skipExecution = $true
            return
        }

        Write-Verbose 'Initialize-WinpeStartupEnvironment: Starting WinPE environment initialization'

        $systemDrive = $env:SystemDrive
        $profileRoot = Join-Path -Path $systemDrive -ChildPath 'windows\system32\config\systemprofile'
    }

    process {
        if ($skipExecution) { return }
        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Initialize WinpeStartup"

        $corePath = Join-Path -Path $systemDrive -ChildPath 'WinpeStartup\core'
        $coreFolders = @()
        $coreDiscoverySucceeded = $true
        try {
            if (Test-Path -LiteralPath $corePath -PathType Container -ErrorAction Stop) {
                $coreFolders = @(Get-ChildItem -LiteralPath $corePath -Directory -Force -ErrorAction Stop)
            }
        }
        catch {
            $coreDiscoverySucceeded = $false
            Write-Warning "Initialize-WinpeStartupEnvironment: Failed to discover Core folders in '$corePath'."
        }

        $allEnvironmentImportsSucceeded = $coreDiscoverySucceeded
        $environmentFiles = @(
            foreach ($coreFolder in $coreFolders) {
                try {
                    Get-ChildItem -LiteralPath $coreFolder.FullName -File -Filter '*.env' -Force -ErrorAction Stop |
                        Where-Object { $_.Name -like '*.env' }
                }
                catch {
                    $allEnvironmentImportsSucceeded = $false
                    Write-Warning "Initialize-WinpeStartupEnvironment: Failed to discover environment files in '$($coreFolder.FullName)'."
                }
            }
        )

        foreach ($environmentFile in ($environmentFiles | Sort-Object FullName)) {
            try {
                $environmentLines = @(Get-Content -LiteralPath $environmentFile.FullName -ErrorAction Stop)
            }
            catch {
                $allEnvironmentImportsSucceeded = $false
                Write-Warning "Initialize-WinpeStartupEnvironment: Failed to read environment file '$($environmentFile.FullName)'."
                continue
            }

            $lineNumber = 0
            foreach ($environmentLine in $environmentLines) {
                $lineNumber++
                $line = $environmentLine.Trim()
                if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith('#')) {
                    continue
                }

                $separatorIndex = $line.IndexOf('=')
                if ($separatorIndex -le 0) {
                    $allEnvironmentImportsSucceeded = $false
                    Write-Warning "Initialize-WinpeStartupEnvironment: Invalid environment entry in '$($environmentFile.FullName)' at line $lineNumber. Expected NAME=VALUE."
                    continue
                }

                $name = $line.Substring(0, $separatorIndex).Trim()
                $value = $line.Substring($separatorIndex + 1).Trim()
                if ($name -match '[\s=\x00]' -or [string]::IsNullOrWhiteSpace($name) -or $value.Contains([char]0)) {
                    $allEnvironmentImportsSucceeded = $false
                    Write-Warning "Initialize-WinpeStartupEnvironment: Invalid environment name or value in '$($environmentFile.FullName)' at line $lineNumber."
                    continue
                }

                if ($value.StartsWith('"') -or $value.StartsWith("'")) {
                    if ($value.Length -lt 2 -or $value[$value.Length - 1] -ne $value[0]) {
                        $allEnvironmentImportsSucceeded = $false
                        Write-Warning "Initialize-WinpeStartupEnvironment: Unmatched environment value quotes in '$($environmentFile.FullName)' at line $lineNumber."
                        continue
                    }
                    $value = $value.Substring(1, $value.Length - 2)
                }

                try {
                    [System.Environment]::SetEnvironmentVariable($name, $value, [System.EnvironmentVariableTarget]::Process)
                    Write-Verbose "Initialize-WinpeStartupEnvironment: Loaded environment entry from '$($environmentFile.FullName)' at line $lineNumber."
                }
                catch {
                    $allEnvironmentImportsSucceeded = $false
                    Write-Warning "Initialize-WinpeStartupEnvironment: Failed to set environment entry from '$($environmentFile.FullName)' at line $lineNumber."
                }
            }
        }

        if ($environmentFiles.Count -gt 0 -or -not $allEnvironmentImportsSucceeded) {
            $environmentStatus = if ($allEnvironmentImportsSucceeded) { 'PASS' } else { 'FAIL' }
            Write-Host "[$(Get-Date -format s)] [$environmentStatus] Initialize env"
        }

        # ── Shell Folders ───────────────────────────────────────────────
        $shellFolders = @(
            Join-Path -Path $systemDrive -ChildPath 'Program Files\WindowsPowerShell\Scripts'
            Join-Path -Path $profileRoot -ChildPath 'AppData\Local'
            Join-Path -Path $profileRoot -ChildPath 'AppData\Roaming'
            Join-Path -Path $profileRoot -ChildPath 'Desktop'
            Join-Path -Path $profileRoot -ChildPath 'Documents\WindowsPowerShell'
            Join-Path -Path $systemDrive -ChildPath 'windows\system32\WindowsPowerShell\v1.0\Scripts'
        )

        foreach ($folder in $shellFolders) {
            New-Item -Path $folder -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
            # Write-Verbose "Created folder: $folder"
        }

        # ── Environment Variables (process scope) ───────────────────────
        $env:APPDATA      = Join-Path -Path $profileRoot -ChildPath 'AppData\Roaming'
        $env:HOMEDRIVE    = $systemDrive
        $env:HOMEPATH     = '\windows\system32\config\systemprofile'
        $env:LOCALAPPDATA = Join-Path -Path $profileRoot -ChildPath 'AppData\Local'
        $env:USERPROFILE  = $profileRoot

        Write-Verbose "Set APPDATA = $env:APPDATA"
        Write-Verbose "Set HOMEDRIVE = $env:HOMEDRIVE"
        Write-Verbose "Set HOMEPATH = $env:HOMEPATH"
        Write-Verbose "Set LOCALAPPDATA = $env:LOCALAPPDATA"
        Write-Verbose "Set USERPROFILE = $env:USERPROFILE"

        # ── Registry: USERDATA environment variable ─────────────────────
        $userDataPath  = Join-Path -Path $systemDrive -ChildPath 'Windows\System32\Config\SystemProfile'
        $envRegKeyPath = 'HKLM:\SYSTEM\ControlSet001\Control\Session Manager\Environment'

        Set-ItemProperty -Path $envRegKeyPath -Name 'USERDATA' -Value $userDataPath -Type String -Force
        Write-Verbose "Set registry USERDATA = $userDataPath"

        # ── Registry: PowerShell execution policy ───────────────────────
        $psRegKeyPath = 'HKLM:\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell'

        Set-ItemProperty -Path $psRegKeyPath -Name 'ExecutionPolicy' -Value 'Bypass' -Type String -Force
        Write-Verbose 'Set registry ExecutionPolicy = Bypass'

        $coreImports = @(
            @{
                Folder     = 'winpe-reg'
                Filter     = '*.reg'
                Executable = 'reg.exe'
                Arguments  = @('import')
                Status     = 'Initialize registry'
            }
            @{
                Folder     = 'winpe-root-cer'
                Filter     = '*.cer'
                Executable = 'certutil.exe'
                Arguments  = @('-addstore', 'root')
                Status     = 'Initialize certificates'
            }
        )

        foreach ($coreImport in $coreImports) {
            $allImportsSucceeded = $coreDiscoverySucceeded
            $importFiles = @(
                foreach ($coreFolder in $coreFolders) {
                    $importPath = Join-Path -Path $coreFolder.FullName -ChildPath $coreImport.Folder
                    try {
                        if (Test-Path -LiteralPath $importPath -PathType Container -ErrorAction Stop) {
                            Get-ChildItem -LiteralPath $importPath -File -Filter $coreImport.Filter -Force -ErrorAction Stop |
                                Where-Object { $_.Name -like $coreImport.Filter }
                        }
                    }
                    catch {
                        $allImportsSucceeded = $false
                        Write-Warning "Initialize-WinpeStartupEnvironment: Failed to discover '$($coreImport.Filter)' files in '$importPath'."
                    }
                }
            )

            foreach ($importFile in ($importFiles | Sort-Object FullName)) {
                $standardOutputPath = $null
                $standardErrorPath = $null
                try {
                    # Start-Process joins arguments, so retain quotes around paths with spaces.
                    $importArguments = @($coreImport.Arguments) + @("`"$($importFile.FullName)`"")
                    $startProcessParameters = @{
                        FilePath     = $coreImport.Executable
                        ArgumentList = $importArguments
                        NoNewWindow  = $true
                        Wait         = $true
                        PassThru     = $true
                        ErrorAction  = 'Stop'
                    }
                    $standardOutputPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "$([System.IO.Path]::GetRandomFileName()).stdout"
                    $standardErrorPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "$([System.IO.Path]::GetRandomFileName()).stderr"
                    $startProcessParameters.RedirectStandardOutput = $standardOutputPath
                    $startProcessParameters.RedirectStandardError = $standardErrorPath

                    $importProcess = Start-Process @startProcessParameters
                    if ($importProcess.ExitCode -ne 0) {
                        $allImportsSucceeded = $false
                        Write-Warning "Initialize-WinpeStartupEnvironment: '$($coreImport.Executable)' failed for '$($importFile.FullName)' with exit code $($importProcess.ExitCode)."
                        continue
                    }
                    Write-Verbose "Initialize-WinpeStartupEnvironment: Imported '$($importFile.FullName)' using '$($coreImport.Executable)'."
                }
                catch {
                    $allImportsSucceeded = $false
                    Write-Warning "Initialize-WinpeStartupEnvironment: Failed to import '$($importFile.FullName)' using '$($coreImport.Executable)': $($_.Exception.Message)"
                }
                finally {
                    @($standardOutputPath, $standardErrorPath) |
                        Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
                        ForEach-Object { Remove-Item -LiteralPath $_ -Force -ErrorAction SilentlyContinue }
                }
            }

            if ($importFiles.Count -gt 0 -or -not $allImportsSucceeded) {
                $importStatus = if ($allImportsSucceeded) { 'PASS' } else { 'FAIL' }
                Write-Host "[$(Get-Date -format s)] [$importStatus] $($coreImport.Status)"
            }
        }
    }

    end {
        if ($skipExecution) { return }
        Write-Verbose 'Initialize-WinpeStartupEnvironment: Complete'
    }
}