private/Select-BuildOSDeployBootProfile.ps1

#Requires -PSEdition Core

function Select-BuildOSDeployBootProfile {
    <#
    .SYNOPSIS
        Selects an OSDeploy Boot build profile
 
    .DESCRIPTION
        Scans profile directories beneath the OSDeploy Boot-Assets
        osdeployboot-profiles directory for osdeployboot.json files. Profile folder names
        use the <Name>-<Architecture> format. The function displays profile settings in a
        single-selection Out-GridView picker when Name is omitted. An exact canonical Name
        returns the matching profile without displaying the picker.
 
        After selection, the function expands portable path tokens and verifies every configured
        WinPE driver, script, media script, and startup profile path. Profiles with
        missing paths are rejected and the picker is displayed again. Valid profiles are
        automatically rewritten when absolute OSDeployCore or module paths can be converted to portable
        tokens. Canceling the picker returns no object.
 
    .PARAMETER Name
        Specifies the exact canonical profile folder name to return without displaying the
        profile picker. Named profiles are still validated before they are returned.
 
    .PARAMETER Architecture
        Limits discovery to profiles whose JSON Architecture property is amd64 or arm64.
        When omitted, profiles for both architectures are displayed.
 
    .PARAMETER SkipPathValidation
        Returns an exact named profile without validating or migrating its configured content
        paths. This allows Build-OSDeployBoot to repair stale paths during a profile update.
 
    .PARAMETER SkipTokenMigration
        Validates configured content paths without rewriting absolute paths as portable tokens.
        This supports read-only profile builds.
 
    .EXAMPLE
        PS> Select-BuildOSDeployBootProfile -Architecture amd64
 
        Displays valid amd64 build profiles for single selection.
 
    .EXAMPLE
        PS> Select-BuildOSDeployBootProfile -Name 'Contoso-amd64'
 
        Returns the validated Contoso-amd64 profile without displaying the picker.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.Management.Automation.PSCustomObject. Returns the selected profile summary,
        including its FullName. Returns no object when no profile exists or selection is canceled.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 1.0.0
        Date: 2026-08-28
 
        Change Summary:
            - Initial version.
            - Updated output path from builds to boot; moved profiles under Boot-Assets.
            - Added profile property display, architecture validation, and token migration.
            - Uses flat canonical profile folders and filters by the JSON Architecture property.
 
        Selecting a valid profile may overwrite its JSON file with tokenized module paths.
 
    .LINK
        Build-OSDeployBoot
    #>

    [CmdletBinding()]
    param (
        [System.String]
        $Name,

        [ValidateSet('amd64', 'arm64')]
        [System.String]
        $Architecture,

        [System.Management.Automation.SwitchParameter]
        $SkipPathValidation,

        [System.Management.Automation.SwitchParameter]
        $SkipTokenMigration
    )

    if ($SkipPathValidation -and -not $Name) {
        throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] SkipPathValidation requires an exact profile name."
    }

    $profilePath = Join-Path $script:OSDeployBootAssetsPath 'osdeployboot-profiles'
    if (-not (Test-Path -LiteralPath $profilePath -PathType Container)) {
        return $null
    }

    if ($Name) {
        Initialize-OSDeployBootAssetPath -ProfilePath (Join-Path $profilePath $Name)
    }
    else {
        Initialize-OSDeployBootAssetPath
    }

    $results = @(Get-ChildItem -LiteralPath $profilePath -Directory -ErrorAction SilentlyContinue | ForEach-Object {
        if ($Name -and $_.Name -ine $Name) { return }

        $profileFile = Join-Path $_.FullName 'osdeployboot.json'
        if (-not (Test-Path -LiteralPath $profileFile -PathType Leaf)) { return }

        try {
            $profileJson = Get-Content -LiteralPath $profileFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
            if (-not $Architecture -or $profileJson.Architecture -eq $Architecture) {
                Get-Item -LiteralPath $profileFile
            }
        }
        catch {
            Write-Warning "[$(Get-Date -Format s)] Could not read build profile: $profileFile"
        }
    })

    if ($results.Count -gt 0) {
        if (-not $Name) {
            Write-Host -ForegroundColor DarkGreen "[$(Get-Date -format s)] Build Profiles are saved in $profilePath"
            Write-HostDateTimeDarkGray 'Select an OSDeploy Build Profile (Cancel to create a new one)'
        }

        $profileObjects = foreach ($file in $results) {
            try {
                $json = Get-Content -LiteralPath $file.FullName -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
            }
            catch {
                $json = $null
            }
            $driverCount = if ($json.WinPEDriver) { @($json.WinPEDriver).Count } else { 0 }
            [PSCustomObject]@{
                Name                 = $file.Directory.Name
                LastModified         = $file.LastWriteTime
                Architecture         = $json.Architecture
                Options              = ($json.Options -join ', ')
                Languages            = ($json.Languages -join ', ')
                SetInputLocale       = $json.SetInputLocale
                SetAllIntl           = $json.SetAllIntl
                SetTimeZone          = $json.SetTimeZone
                WinPEStartupProfile  = $json.WinPEStartupProfile
                WinPEScript          = ($json.WinPEScript -join ', ')
                MediaScript          = ($json.MediaScript -join ', ')
                WinPEDrivers         = $driverCount
                FullName             = $file.FullName
            }
        }

        while ($true) {
            $selected = if ($Name) {
                $profileObjects | Where-Object Name -IEQ $Name | Select-Object -First 1
            }
            else {
                $profileObjects |
                    Out-GridView -OutputMode Single -Title 'Select a Build Profile (Cancel to create a new one)'
            }

            if (-not $selected) {
                return $null
            }

            # Validate all path-bearing properties in the selected profile
            try {
                $profileJson = Get-Content -LiteralPath $selected.FullName -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
            }
            catch {
                Write-Warning "[$(Get-Date -Format s)] Could not read build profile: $($selected.FullName)"
                Write-Warning "[$(Get-Date -Format s)] $($_.Exception.Message)"
                if ($Name) {
                    throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Named build profile is invalid: $Name"
                }
                continue
            }

            if ($SkipPathValidation) {
                return $selected
            }

            $pathProperties = [ordered]@{
                WinPEDriver          = $profileJson.WinPEDriver
                WinPEScript          = $profileJson.WinPEScript
                MediaScript          = $profileJson.MediaScript
                WinPEStartupProfile  = $profileJson.WinPEStartupProfile
            }

            # Expand tokens to absolute paths before Test-Path validation
            $expandedProperties = [ordered]@{}
            foreach ($prop in $pathProperties.Keys) {
                $expandedProperties[$prop] = Expand-OSDeployBuildProfileToken $pathProperties[$prop]
            }

            $invalidPaths = foreach ($prop in $expandedProperties.Keys) {
                foreach ($entry in @($expandedProperties[$prop])) {
                    if ($entry -and -not (Test-Path -LiteralPath $entry)) {
                        [PSCustomObject]@{ Property = $prop; Path = $entry }
                    }
                }
            }

            if ($invalidPaths) {
                Write-Warning "[$(Get-Date -Format s)] Build profile has path errors: $($selected.FullName)"
                foreach ($bad in $invalidPaths) {
                    Write-Warning "[$(Get-Date -Format s)] [$($bad.Property)] Path not found: $($bad.Path)"
                }
                if ($Name) {
                    throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Named build profile has path errors: $Name"
                }
                Write-Warning "[$(Get-Date -Format s)] Fix the build profile and try again, or cancel to create a new one."
                continue
            }

            # Auto-migrate: re-tokenize paths and rewrite the file if anything changed
            $tokenizedProperties = [ordered]@{}
            foreach ($prop in $pathProperties.Keys) {
                $tokenizedProperties[$prop] = ConvertTo-OSDeployBuildProfileToken $pathProperties[$prop]
            }

            $needsMigration = $false
            foreach ($prop in $pathProperties.Keys) {
                $original = @($pathProperties[$prop]) | Where-Object { $_ }
                $tokenized = @($tokenizedProperties[$prop]) | Where-Object { $_ }
                if (($original -join '|') -ine ($tokenized -join '|')) {
                    $needsMigration = $true
                    break
                }
            }

            if ($needsMigration -and -not $SkipTokenMigration) {
                Write-Verbose "[$($MyInvocation.MyCommand.Name)] Migrating build profile to use portable path tokens: $($selected.FullName)"
                $updatedProfile = [ordered]@{
                    Name                = $selected.Name
                    Architecture        = $profileJson.Architecture
                    Options             = $profileJson.Options
                    WinPEDriver         = $tokenizedProperties['WinPEDriver']
                    WinPEScript         = $tokenizedProperties['WinPEScript']
                    MediaScript         = $tokenizedProperties['MediaScript']
                    WinPEStartupProfile  = if ($tokenizedProperties['WinPEStartupProfile']) { $tokenizedProperties['WinPEStartupProfile'][0] } else { $null }
                    Languages            = $profileJson.Languages
                    SetAllIntl          = $profileJson.SetAllIntl
                    SetInputLocale      = $profileJson.SetInputLocale
                    SetTimeZone         = $profileJson.SetTimeZone
                }
                try {
                    $updatedProfile | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue | Out-File -LiteralPath $selected.FullName -Encoding utf8 -Force
                }
                catch {
                    Write-Warning "[$(Get-Date -Format s)] Could not migrate build profile tokens: $($_.Exception.Message)"
                }
            }

            return $selected
        }
    }

    return $null
}