public/New-OSDeployBootProfilePreview.ps1

#Requires -PSEdition Core
#Requires -Version 7.4

function New-OSDeployBootProfilePreview {
    <#
    .SYNOPSIS
        Creates an OSDeploy Boot profile
 
    .DESCRIPTION
        Creates a persistent architecture-specific profile under OSDeploy Boot-Assets.
        The command prompts for a cached Windows Recovery Image and derives the profile
        architecture from that selection. It then selects shared WinPE drivers, WinPE scripts,
        media scripts, WinPEStartup profiles, and wallpaper, and writes portable content paths
        to osdeployboot.json.
 
        A base profile name receives the selected architecture suffix. A supplied amd64 or
        arm64 suffix must match the selected image architecture. Existing profile directories
        are not overwritten. If no recovery image is selected, the command creates nothing.
        This command creates configuration only; use Build-OSDeployBoot -ProfileName to build media.
 
    .PARAMETER ProfileName
        Specifies the base or canonical profile name. A base name receives the Architecture
        suffix, such as Contoso-amd64.
 
    .PARAMETER Languages
        Specifies zero or more Windows ADK language identifiers to save. Specify * to request
        all additional language directories during a later build.
 
    .PARAMETER SetAllIntl
        Specifies the international-settings value to save.
 
    .PARAMETER SetInputLocale
        Specifies the WinPE input locale to save.
 
    .PARAMETER SetTimeZone
        Specifies a timezone validated against tzutil /l. The default is the current system
        timezone returned by tzutil /g.
 
    .PARAMETER Options
        Specifies optional WinPE features to save. Valid values are pwsh and dart.
 
    .EXAMPLE
        PS> New-OSDeployBootProfilePreview -ProfileName 'Contoso'
 
        Prompts for a Windows Recovery Image, selects shared content, and creates a profile
        with the selected architecture suffix.
 
    .EXAMPLE
        PS> New-OSDeployBootProfilePreview -ProfileName 'Lab-arm64' -Options pwsh -WhatIf
 
        Shows the profile creation action without displaying selectors or writing 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-09-03
 
        At least one cached Windows Recovery Image and write access to the OSDeployCore
        Boot-Assets directory under ProgramData are required. An interactive, nonredirected console
        is required when multiple recovery images are available.
 
    .LINK
        Build-OSDeployBoot
 
    .LINK
        Update-OSDeployBootProfilePreview
 
    .LINK
        Delete-OSDeployBootProfilePreview
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    param (
        [Parameter(Mandatory)]
        [System.String]
        $ProfileName,

        [ValidateSet(
            '*', 'ar-sa', 'bg-bg', 'cs-cz', 'da-dk', 'de-de', 'el-gr',
            'en-gb', 'en-us', 'es-es', 'es-mx', 'et-ee', 'fi-fi',
            'fr-ca', 'fr-fr', 'he-il', 'hr-hr', 'hu-hu', 'it-it',
            'ja-jp', 'ko-kr', 'lt-lt', 'lv-lv', 'nb-no', 'nl-nl',
            'pl-pl', 'pt-br', 'pt-pt', 'ro-ro', 'ru-ru', 'sk-sk',
            'sl-si', 'sr-latn-rs', 'sv-se', 'th-th', 'tr-tr',
            'uk-ua', 'zh-cn', 'zh-tw'
        )]
        [System.String[]]
        $Languages,

        [System.String]
        $SetAllIntl,

        [System.String]
        $SetInputLocale,

        [ValidateScript({
            $TimeZones = tzutil /l
            $ValidOptions = foreach ($TimeZone in $TimeZones) {
                if (($TimeZones.IndexOf($TimeZone) - 1) % 3 -eq 0) {
                    $TimeZone.Trim()
                }
            }
            $ValidOptions -contains $_
        })]
        [System.String]
        $SetTimeZone = (tzutil /g),

        [ValidateSet('pwsh', 'dart')]
        [System.String[]]
        $Options
    )

    Write-Verbose "[$($MyInvocation.MyCommand.Name)] Start"

    if ([System.String]::IsNullOrWhiteSpace($ProfileName)) {
        throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] ProfileName cannot be empty."
    }

    $RequestedProfilePath = Join-Path $Script:OSDeployBootAssetsPath 'osdeployboot-profiles' $ProfileName
    if (-not $PSCmdlet.ShouldProcess($RequestedProfilePath, 'Create OSDeploy Boot profile')) {
        return
    }

    $SelectedWindowsRE = Select-OSDeployCoreCacheWindowsRE
    if (-not $SelectedWindowsRE) {
        Write-Warning "[$(Get-Date -Format s)] No Windows Recovery Image was selected. The profile was not created."
        return
    }

    $Architecture = [System.String]$SelectedWindowsRE.Architecture
    if ($Architecture -notin @('amd64', 'arm64')) {
        throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Selected Windows Recovery Image has an unsupported architecture: $Architecture"
    }

    $ProfileArchitecture = if ($ProfileName -match '(?i)-(amd64|arm64)$') {
        $Matches[1].ToLowerInvariant()
    }
    if ($ProfileArchitecture -and $ProfileArchitecture -ne $Architecture) {
        throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Architecture '$Architecture' does not match profile architecture '$ProfileArchitecture'."
    }

    $CanonicalProfileName = if ($ProfileArchitecture) { $ProfileName } else { "$ProfileName-$($Architecture.ToLowerInvariant())" }
    $ProfilePath = Join-Path $Script:OSDeployBootAssetsPath 'osdeployboot-profiles' $CanonicalProfileName
    if (Test-Path -LiteralPath $ProfilePath) {
        throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Build profile path already exists: $ProfilePath"
    }

    Initialize-OSDeployCorePaths
    $SelectedContent = Select-OSDeployBootProfileContent -Architecture $Architecture

    New-Item -Path $ProfilePath -ItemType Directory -ErrorAction Stop | Out-Null
    Initialize-OSDeployCoreBuildProfilePaths -Path $ProfilePath
    $ProfileFile = Join-Path $ProfilePath 'osdeployboot.json'
    $BuildProfile = [ordered]@{
        Name                 = [System.String]$CanonicalProfileName
        Architecture         = [System.String]$Architecture
        Languages            = [System.String[]]$Languages
        SetAllIntl           = [System.String]$SetAllIntl
        SetInputLocale       = [System.String]$SetInputLocale
        SetTimeZone          = [System.String]$SetTimeZone
        Options              = [System.String[]]$Options
        WinPEStartupProfile  = ConvertTo-OSDeployBuildProfileToken $SelectedContent.WinPEStartupProfile
        WinPEDriver          = ConvertTo-OSDeployBuildProfileToken $SelectedContent.WinPEDriver
        WinPEScript          = ConvertTo-OSDeployBuildProfileToken $SelectedContent.WinPEScript
        MediaScript          = ConvertTo-OSDeployBuildProfileToken $SelectedContent.MediaScript
    }

    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Exporting Build Profile to $ProfileFile"
    $BuildProfile | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue |
        Out-File -LiteralPath $ProfileFile -Encoding utf8 -Force
    Set-OSDeployBootProfileWallpaper -ProfilePath $ProfilePath

    Write-Verbose "[$($MyInvocation.MyCommand.Name)] End"
}