private/Initialize-OSDeployCorePaths.ps1

#Requires -PSEdition Core

function Initialize-OSDeployCorePaths {
    <#
    .SYNOPSIS
        Initializes OSDeployCore cache and Boot-Assets paths
 
    .DESCRIPTION
        Ensures the standard OSDeployCore boot, cache, download, hash, software, Windows
        image, shared build-content, architecture, and build-profile directories exist.
 
        Existing root-level software cache content is moved to cache\software. Content is
        merged recursively without replacing destination conflicts. Conflicting legacy items
        remain in the root-level software directory with a warning.
 
        Module-managed cache and root-level winpedrivers-amd64 and winpedrivers-arm64
        directories are moved to Boot-Assets. Legacy cache and Boot-Assets build-winpedrivers
        architecture directories are migrated to their matching Boot-Assets destinations.
        Root-level managed content replaces matching Boot-Assets content during migration.
        Profile-local build-winpedrivers content moves to the directory matching the profile
        architecture.
 
        The function migrates legacy Boot-Assets folders and build profiles into flat
        osdeployboot-profiles\<Name>-<Architecture> directories. Canonical destination
        conflicts are preserved in their source location with a warning. The reserved
        recent-amd64.json and recent-arm64.json snapshots remain at the profile root. The
        function creates profile-local content directories, synchronizes each migrated profile's
        Name property, removes retired wallpaper and WinPE app properties, renames the legacy
        media property, and rewrites persisted paths that refer to legacy locations. Empty
        retired WinPE app-script and architecture directories are removed after migration.
 
    .EXAMPLE
        PS> Initialize-OSDeployCorePaths
 
        Creates missing OSDeployCore paths and migrates supported legacy Boot-Assets content.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        None.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 1.0.0
        Date: 2026-08-28
 
        This function reads $Script:OSDeployCorePath and $Script:OSDeployBootAssetsPath and
        modifies content below those locations without WhatIf or Confirm support. The paths
        must be writable.
 
        Change Summary:
            - Moves the root-level software cache to cache\software without replacing conflicts.
            - Consolidates shared WinPE drivers under Boot-Assets and migrates root-level content with replacement.
            - Migrates build profiles to osdeployboot-profiles\<Name>-<Architecture>\osdeployboot.json.
            - Synchronizes the profile Name property with its canonical folder name.
            - Removes WinPECustomWallpaper, WinPEApp, and WinPEAppScript from build profile copies.
            - Removes empty retired WinPE app-script directories without deleting their content.
            - Creates profile-local content directories for every saved build profile.
    #>

    [CmdletBinding()]
    param ()

    $functionName = $MyInvocation.MyCommand.Name
    $legacyRepositoryPath = Join-Path $Script:OSDeployCorePath 'repository'
    $legacyOSDRepoPath = Join-Path $Script:OSDeployCorePath 'OSDRepo'
    Initialize-OSDeployCoreBootAssets -LegacyPath $legacyRepositoryPath, $legacyOSDRepoPath -BootAssetsPath $Script:OSDeployBootAssetsPath -Create
    Initialize-OSDeployBootAssetPath

    $legacyBuildProfilesPath = Join-Path $script:OSDeployBootAssetsPath 'build-profiles'
    $buildProfilesPath = Join-Path $script:OSDeployBootAssetsPath 'osdeployboot-profiles'
    $legacySoftwarePath = Join-Path $script:OSDeployCorePath 'software'
    $legacyBootAssetsWinPEDriversPath = Join-Path $script:OSDeployBootAssetsPath 'build-winpedrivers'
    $legacyCacheWinPEDriversPath = Join-Path $script:OSDeployCoreCachePath 'build-winpedrivers'
    $cachedWinPEDriversPaths = [ordered]@{
        'amd64' = Join-Path $script:OSDeployCoreCachePath 'winpedrivers-amd64'
        'arm64' = Join-Path $script:OSDeployCoreCachePath 'winpedrivers-arm64'
    }
    $managedWinPEDriversPaths = [ordered]@{
        'amd64' = Join-Path $script:OSDeployCorePath 'winpedrivers-amd64'
        'arm64' = Join-Path $script:OSDeployCorePath 'winpedrivers-arm64'
    }
    $bootAssetsWinPEDriversPaths = [ordered]@{
        'amd64' = Join-Path $script:OSDeployBootAssetsPath 'winpedrivers-amd64'
        'arm64' = Join-Path $script:OSDeployBootAssetsPath 'winpedrivers-arm64'
    }
    $buildContentFolderMap = [ordered]@{
        'media-script'          = 'boot-mediascript'
        'build-mediascript'     = 'boot-mediascript'
        'winpe-drivers'         = 'build-winpedrivers'
        'winpe-script'          = 'boot-winpescript'
        'build-winpescript'     = 'boot-winpescript'
        'winpe-wallpaper'       = 'boot-wallpaper'
        'build-winpewallpaper'  = 'boot-wallpaper'
    }
    $profilePathMoves = [System.Collections.Generic.List[System.Object]]::new()
    $getCanonicalProfileName = {
        param (
            [System.String]$ProfileName,
            [System.String]$Architecture
        )

        $baseName = $ProfileName -replace '(?i)-(?:amd64|arm64)$', ''
        "$baseName-$($Architecture.ToLowerInvariant())"
    }

    # Merge legacy directories recursively while preserving any destination-side conflicts.
    $moveDirectoryContent = {
        param (
            [System.String]$SourcePath,
            [System.String]$DestinationPath,
            [System.Boolean]$ArchiveProfileConflicts = $false
        )

        if (-not (Test-Path -LiteralPath $SourcePath -PathType Container)) { return }

        if (-not (Test-Path -LiteralPath $DestinationPath -PathType Container)) {
            New-Item -ItemType Directory -Path $DestinationPath -Force | Out-Null
        }

        foreach ($item in @(Get-ChildItem -LiteralPath $SourcePath -Force -ErrorAction SilentlyContinue)) {
            $destinationItem = Join-Path $DestinationPath $item.Name
            if (Test-Path -LiteralPath $destinationItem) {
                # Two matching directories can be merged safely; conflicting files remain untouched.
                if ($item.PSIsContainer -and (Test-Path -LiteralPath $destinationItem -PathType Container)) {
                    & $moveDirectoryContent $item.FullName $destinationItem $ArchiveProfileConflicts
                    continue
                }

                if ($ArchiveProfileConflicts -and -not $item.PSIsContainer -and $item.Name -in @('buildprofile.json', 'osdeployboot.json')) {
                    $archivePath = Join-Path $DestinationPath 'profile.legacy.json'
                    $suffix = 1
                    while (Test-Path -LiteralPath $archivePath) {
                        $archivePath = Join-Path $DestinationPath ('profile.legacy-{0:d3}.json' -f $suffix)
                        $suffix++
                    }

                    try {
                        Move-Item -LiteralPath $item.FullName -Destination $archivePath -ErrorAction Stop
                        Write-Warning "[$functionName] Profile already exists at '$destinationItem'. Archived legacy profile to '$archivePath'."
                    }
                    catch {
                        Write-Warning "[$functionName] Could not archive legacy profile '$($item.FullName)' to '$archivePath': $($_.Exception.Message)"
                    }
                    continue
                }

                Write-Warning "[$functionName] Could not migrate '$($item.FullName)' because '$destinationItem' already exists."
                continue
            }

            try {
                Move-Item -LiteralPath $item.FullName -Destination $DestinationPath -ErrorAction Stop
                Write-Verbose "[$($MyInvocation.MyCommand.Name)] [$functionName] Migrated '$($item.FullName)' to '$DestinationPath'"
            }
            catch {
                Write-Warning "[$functionName] Could not migrate '$($item.FullName)' to '$DestinationPath': $($_.Exception.Message)"
            }
        }

        # Remove the legacy directory only after all movable content has left it.
        if (-not (Get-ChildItem -LiteralPath $SourcePath -Force -ErrorAction SilentlyContinue | Select-Object -First 1)) {
            Remove-Item -LiteralPath $SourcePath -Force -ErrorAction SilentlyContinue
        }
    }

    # Move source content recursively and replace matching destination content.
    $moveDirectoryContentOverwrite = {
        param (
            [System.String]$SourcePath,
            [System.String]$DestinationPath
        )

        if (-not (Test-Path -LiteralPath $SourcePath -PathType Container)) { return }

        if (-not (Test-Path -LiteralPath $DestinationPath -PathType Container)) {
            New-Item -ItemType Directory -Path $DestinationPath -Force | Out-Null
        }

        foreach ($item in @(Get-ChildItem -LiteralPath $SourcePath -Force -ErrorAction SilentlyContinue)) {
            $destinationItem = Join-Path $DestinationPath $item.Name
            if ($item.PSIsContainer -and (Test-Path -LiteralPath $destinationItem -PathType Container)) {
                & $moveDirectoryContentOverwrite $item.FullName $destinationItem
                continue
            }

            try {
                if (Test-Path -LiteralPath $destinationItem) {
                    Remove-Item -LiteralPath $destinationItem -Recurse -Force -ErrorAction Stop
                }
                Move-Item -LiteralPath $item.FullName -Destination $DestinationPath -ErrorAction Stop
                Write-Verbose "[$($MyInvocation.MyCommand.Name)] [$functionName] Migrated '$($item.FullName)' to '$DestinationPath'"
            }
            catch {
                Write-Warning "[$functionName] Could not replace '$destinationItem' with '$($item.FullName)': $($_.Exception.Message)"
            }
        }

        if (-not (Get-ChildItem -LiteralPath $SourcePath -Force -ErrorAction SilentlyContinue | Select-Object -First 1)) {
            Remove-Item -LiteralPath $SourcePath -Force -ErrorAction SilentlyContinue
        }
    }

    $moveWinPEDriverDirectory = {
        param (
            [System.String]$SourcePath,
            [System.String]$DestinationPath
        )

        if (-not (Test-Path -LiteralPath $SourcePath -PathType Container)) { return }
        if (Test-Path -LiteralPath $DestinationPath) {
            Write-Warning "[$functionName] Could not migrate WinPE drivers '$SourcePath' because '$DestinationPath' already exists. Both directories were preserved."
            return
        }

        try {
            Move-Item -LiteralPath $SourcePath -Destination $DestinationPath -ErrorAction Stop
            Write-HostDateTimeDarkGray "Migrated WinPE drivers '$SourcePath' to '$DestinationPath'"
        }
        catch {
            Write-Warning "[$functionName] Could not migrate WinPE drivers '$SourcePath' to '$DestinationPath': $($_.Exception.Message)"
        }
    }

    if (Test-Path -LiteralPath $legacyBuildProfilesPath -PathType Container) {
        if (-not (Test-Path -LiteralPath $buildProfilesPath -PathType Container)) {
            try {
                Move-Item -LiteralPath $legacyBuildProfilesPath -Destination $buildProfilesPath -ErrorAction Stop
                Write-HostDateTimeDarkGray "Migrated build profiles '$legacyBuildProfilesPath' to '$buildProfilesPath'"
            }
            catch {
                Write-Warning "[$functionName] Could not migrate '$legacyBuildProfilesPath' to '$buildProfilesPath': $($_.Exception.Message)"
            }
        }

        if (Test-Path -LiteralPath $legacyBuildProfilesPath -PathType Container) {
            & $moveDirectoryContent $legacyBuildProfilesPath $buildProfilesPath $true
        }
    }

    foreach ($legacyFolderName in $buildContentFolderMap.Keys) {
        $legacyContentPath = Join-Path $Script:OSDeployBootAssetsPath $legacyFolderName
        $contentPath = Join-Path $Script:OSDeployBootAssetsPath $buildContentFolderMap[$legacyFolderName]
        & $moveDirectoryContent $legacyContentPath $contentPath
    }

    if (-not (Test-Path -LiteralPath $script:OSDeployCoreCachePath -PathType Container)) {
        New-Item -ItemType Directory -Path $script:OSDeployCoreCachePath -Force | Out-Null
    }

    if ((Test-Path -LiteralPath $legacyBootAssetsWinPEDriversPath -PathType Container) -and
        -not (Test-Path -LiteralPath $legacyCacheWinPEDriversPath)) {
        try {
            Move-Item -LiteralPath $legacyBootAssetsWinPEDriversPath -Destination $legacyCacheWinPEDriversPath -ErrorAction Stop
            Write-HostDateTimeDarkGray "Migrated WinPE drivers '$legacyBootAssetsWinPEDriversPath' to '$legacyCacheWinPEDriversPath'"
        }
        catch {
            Write-Warning "[$functionName] Could not migrate '$legacyBootAssetsWinPEDriversPath' to '$legacyCacheWinPEDriversPath': $($_.Exception.Message)"
        }
    }

    foreach ($architecture in @('amd64', 'arm64')) {
        & $moveDirectoryContent (Join-Path $legacyCacheWinPEDriversPath $architecture) $managedWinPEDriversPaths[$architecture]
        & $moveDirectoryContent $cachedWinPEDriversPaths[$architecture] $managedWinPEDriversPaths[$architecture]
        & $moveWinPEDriverDirectory (Join-Path $legacyBootAssetsWinPEDriversPath $architecture) $bootAssetsWinPEDriversPaths[$architecture]
        & $moveDirectoryContentOverwrite $managedWinPEDriversPaths[$architecture] $bootAssetsWinPEDriversPaths[$architecture]
    }

    foreach ($legacyWinPEDriversPath in @($legacyCacheWinPEDriversPath, $legacyBootAssetsWinPEDriversPath)) {
        if ((Test-Path -LiteralPath $legacyWinPEDriversPath -PathType Container) -and
            -not (Get-ChildItem -LiteralPath $legacyWinPEDriversPath -Force -ErrorAction SilentlyContinue | Select-Object -First 1)) {
            Remove-Item -LiteralPath $legacyWinPEDriversPath -Force -ErrorAction SilentlyContinue
        }
    }

    # Establish the shared cache and Boot-Assets roots before migration.
    $paths = @(
        (Join-Path $Script:OSDeployCorePath 'boot'),
        (Join-Path $Script:OSDeployCorePath 'cache'),
        (Join-Path $Script:OSDeployCorePath 'cache' 'config'),
        (Join-Path $Script:OSDeployCorePath 'cache' 'downloads'),
        (Join-Path $Script:OSDeployCorePath 'cache' 'hashes'),
        (Join-Path $Script:OSDeployCorePath 'cache' 'psrepository'),
        $Script:OSDeployCoreSoftwarePath,
        (Join-Path $Script:OSDeployCorePath 'cache' 'windows-os'),
        (Join-Path $Script:OSDeployCorePath 'cache' 'windows-re'),
        (Join-Path $Script:OSDeployCorePath 'cache' 'winpe-apps'),
        (Join-Path $Script:OSDeployBootAssetsPath 'boot-mediascript'),
        $bootAssetsWinPEDriversPaths['amd64'],
        $bootAssetsWinPEDriversPaths['arm64'],
        (Join-Path $Script:OSDeployBootAssetsPath 'boot-winpescript'),
        (Join-Path $Script:OSDeployBootAssetsPath 'boot-wallpaper'),
        $buildProfilesPath,
        (Join-Path $Script:OSDeployBootAssetsPath 'winpestartup-profiles')
    )

    foreach ($path in $paths) {
        if (-not (Test-Path -Path $path)) {
            New-Item -ItemType Directory -Path $path -Force | Out-Null
            Write-Verbose "[$($MyInvocation.MyCommand.Name)] Created directory: $path"
        }
    }

    & $moveDirectoryContent $legacySoftwarePath $Script:OSDeployCoreSoftwarePath

    # Move a complete profile directory so all profile-local content remains together.
    $moveProfileDirectory = {
        param (
            [System.IO.DirectoryInfo]$ProfileDirectory,
            [System.String]$FallbackArchitecture
        )

        $profileFile = @('osdeployboot.json', 'buildprofile.json') |
            ForEach-Object { Join-Path $ProfileDirectory.FullName $_ } |
            Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
            Select-Object -First 1
        if (-not $profileFile) {
            Write-Warning "[$functionName] Could not migrate '$($ProfileDirectory.FullName)' because it does not contain a build profile."
            return
        }

        try {
            $profileContent = Get-Content -LiteralPath $profileFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
        }
        catch {
            Write-Warning "[$functionName] Could not migrate build profile '$profileFile': $($_.Exception.Message)"
            return
        }

        $architecture = if ($profileContent.Architecture -in @('amd64', 'arm64')) {
            [System.String]$profileContent.Architecture
        }
        elseif ($FallbackArchitecture -in @('amd64', 'arm64')) {
            $FallbackArchitecture
        }
        else {
            Write-Warning "[$functionName] Could not migrate build profile '$profileFile' because Architecture is missing or unsupported."
            return
        }

        $canonicalName = & $getCanonicalProfileName $ProfileDirectory.Name $architecture
        $destinationPath = Join-Path $buildProfilesPath $canonicalName
        if ($ProfileDirectory.FullName.Equals($destinationPath, [System.StringComparison]::OrdinalIgnoreCase)) {
            return
        }
        if (Test-Path -LiteralPath $destinationPath) {
            Write-Warning "[$functionName] Could not migrate '$($ProfileDirectory.FullName)' because canonical profile path '$destinationPath' already exists."
            return
        }

        try {
            $sourcePath = $ProfileDirectory.FullName
            Move-Item -LiteralPath $sourcePath -Destination $destinationPath -ErrorAction Stop
            $profilePathMoves.Add([PSCustomObject]@{ Source = $sourcePath; Destination = $destinationPath })
            Write-HostDateTimeDarkGray "Migrated build profile '$sourcePath' to '$destinationPath'"
        }
        catch {
            Write-Warning "[$functionName] Could not migrate build profile '$($ProfileDirectory.FullName)': $($_.Exception.Message)"
        }
    }

    # Root-level profile files predate profile directories; default unknown architectures to amd64.
    foreach ($file in @(Get-ChildItem -LiteralPath $buildProfilesPath -Filter '*.json' -File -ErrorAction SilentlyContinue |
        Where-Object { $_.Name -notin @('recent-amd64.json', 'recent-arm64.json') })) {
        try {
            $profileContent = Get-Content -LiteralPath $file.FullName -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
            $architecture = if ($profileContent.Architecture -in @('amd64', 'arm64')) { [System.String]$profileContent.Architecture } else { 'amd64' }
            $canonicalName = & $getCanonicalProfileName $file.BaseName $architecture
            $profileDirectory = Join-Path $buildProfilesPath $canonicalName
            $profilePath = Join-Path $profileDirectory 'osdeployboot.json'
            if (Test-Path -LiteralPath $profileDirectory) {
                Write-Warning "[$functionName] Could not migrate '$($file.FullName)' because canonical profile path '$profileDirectory' already exists."
                continue
            }

            New-Item -ItemType Directory -Path $profileDirectory -ErrorAction Stop | Out-Null
            Move-Item -LiteralPath $file.FullName -Destination $profilePath -ErrorAction Stop
            Write-HostDateTimeDarkGray "Migrated build profile '$($file.FullName)' to '$profilePath'"
        }
        catch {
            Write-Warning "[$functionName] Could not migrate build profile '$($file.FullName)': $($_.Exception.Message)"
        }
    }

    # Architecture-level files and directories are moved to canonical flat profile folders.
    foreach ($architecture in @('amd64', 'arm64')) {
        $architecturePath = Join-Path $buildProfilesPath $architecture
        if (-not (Test-Path -LiteralPath $architecturePath -PathType Container)) { continue }

        foreach ($file in @(Get-ChildItem -LiteralPath $architecturePath -Filter '*.json' -File -ErrorAction SilentlyContinue)) {
            try {
                $profileContent = Get-Content -LiteralPath $file.FullName -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
                $profileArchitecture = if ($profileContent.Architecture -in @('amd64', 'arm64')) { [System.String]$profileContent.Architecture } else { $architecture }
                $canonicalName = & $getCanonicalProfileName $file.BaseName $profileArchitecture
                $profileDirectory = Join-Path $buildProfilesPath $canonicalName
                $profilePath = Join-Path $profileDirectory 'osdeployboot.json'
                if (Test-Path -LiteralPath $profileDirectory) {
                    Write-Warning "[$functionName] Could not migrate '$($file.FullName)' because canonical profile path '$profileDirectory' already exists."
                    continue
                }

                New-Item -ItemType Directory -Path $profileDirectory -ErrorAction Stop | Out-Null
                Move-Item -LiteralPath $file.FullName -Destination $profilePath -ErrorAction Stop
                Write-HostDateTimeDarkGray "Migrated build profile '$($file.FullName)' to '$profilePath'"
            }
            catch {
                Write-Warning "[$functionName] Could not migrate build profile '$($file.FullName)': $($_.Exception.Message)"
            }
        }

        foreach ($profileDirectory in @(Get-ChildItem -LiteralPath $architecturePath -Directory -ErrorAction SilentlyContinue)) {
            & $moveProfileDirectory $profileDirectory $architecture
        }

        if (-not (Get-ChildItem -LiteralPath $architecturePath -Force -ErrorAction SilentlyContinue | Select-Object -First 1)) {
            Remove-Item -LiteralPath $architecturePath -Force -ErrorAction SilentlyContinue
        }
    }

    # Canonicalize any profiles that were already stored directly below the profile root.
    foreach ($profileDirectory in @(Get-ChildItem -LiteralPath $buildProfilesPath -Directory -ErrorAction SilentlyContinue |
        Where-Object { $_.Name -notin @('amd64', 'arm64') })) {
        & $moveProfileDirectory $profileDirectory
    }

    $legacySharedStartupProfilesPath = Join-Path $Script:OSDeployBootAssetsPath 'winpe-profiles'
    $sharedStartupProfilesPath = Join-Path $Script:OSDeployBootAssetsPath 'winpestartup-profiles'
    & $moveDirectoryContent $legacySharedStartupProfilesPath $sharedStartupProfilesPath

    foreach ($profileDirectory in @(Get-ChildItem -LiteralPath $buildProfilesPath -Directory -ErrorAction SilentlyContinue |
        Where-Object { $_.Name -notin @('amd64', 'arm64') })) {
            foreach ($legacyFolderName in $buildContentFolderMap.Keys) {
                $legacyContentPath = Join-Path $profileDirectory.FullName $legacyFolderName
                $contentPath = Join-Path $profileDirectory.FullName $buildContentFolderMap[$legacyFolderName]
                & $moveDirectoryContent $legacyContentPath $contentPath
            }

            $legacyProfilePath = Join-Path $profileDirectory.FullName 'buildprofile.json'
            $profilePath = Join-Path $profileDirectory.FullName 'osdeployboot.json'

            if (Test-Path -LiteralPath $legacyProfilePath -PathType Leaf) {
                try {
                    if (Test-Path -LiteralPath $profilePath -PathType Leaf) {
                        $archivePath = Join-Path $profileDirectory.FullName 'profile.legacy.json'
                        $suffix = 1
                        while (Test-Path -LiteralPath $archivePath) {
                            $archivePath = Join-Path $profileDirectory.FullName ('profile.legacy-{0:d3}.json' -f $suffix)
                            $suffix++
                        }

                        Move-Item -LiteralPath $legacyProfilePath -Destination $archivePath -ErrorAction Stop
                        Write-Warning "[$(Get-Date -Format s)] Build profile already exists at '$profilePath'. Archived legacy profile to '$archivePath'."
                    }
                    else {
                        Move-Item -LiteralPath $legacyProfilePath -Destination $profilePath -ErrorAction Stop
                        Write-HostDateTimeDarkGray "Migrated build profile '$legacyProfilePath' to '$profilePath'"
                    }
                }
                catch {
                    Write-Warning "[$(Get-Date -Format s)] Could not migrate build profile '$legacyProfilePath': $($_.Exception.Message)"
                }
            }

            if (Test-Path -LiteralPath $profilePath -PathType Leaf) {
                try {
                    $profileContent = Get-Content -LiteralPath $profilePath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
                    if ($profileContent.Architecture -in @('amd64', 'arm64')) {
                        $legacyProfileDriverPath = Join-Path $profileDirectory.FullName 'build-winpedrivers'
                        $profileDriverPath = Join-Path $profileDirectory.FullName "winpedrivers-$($profileContent.Architecture)"
                        & $moveWinPEDriverDirectory $legacyProfileDriverPath $profileDriverPath
                    }
                    elseif (Test-Path -LiteralPath (Join-Path $profileDirectory.FullName 'build-winpedrivers') -PathType Container) {
                        Write-Warning "[$functionName] Could not migrate profile-local WinPE drivers in '$($profileDirectory.FullName)' because Architecture is missing or unsupported."
                    }
                }
                catch {
                    Write-Warning "[$functionName] Could not inspect build profile '$profilePath' for WinPE driver migration: $($_.Exception.Message)"
                }

                $legacyProfileStartupPath = Join-Path $profileDirectory.FullName 'winpe-profiles'
                $profileStartupPath = Join-Path $profileDirectory.FullName 'WinPEStartup' 'profiles'
                & $moveDirectoryContent $legacyProfileStartupPath $profileStartupPath
                Initialize-OSDeployCoreBuildProfilePaths -Path $profileDirectory.FullName
            }
    }

    $legacyOSDRepoBuildProfilesPath = Join-Path $legacyOSDRepoPath 'build-profiles'
    $legacyRepositoryTokenPath = '${{ OSDeployCore }}\repository'
    $bootAssetsTokenPath = '${{ OSDeployCore }}\boot-assets'
    $pathProperties = @('WinPEDriver', 'WinPEScript', 'MediaScript', 'WinPEStartupProfile')

    # Remove retired app properties and rename the media-script property in every profile copy.
    foreach ($profileFile in @(Get-ChildItem -LiteralPath $Script:OSDeployCorePath -Filter 'osdeployboot.json' -File -Recurse -ErrorAction SilentlyContinue)) {
        try {
            $profileContent = Get-Content -LiteralPath $profileFile.FullName -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
            $profileChanged = $false

            foreach ($retiredPropertyName in @('WinPEApp', 'WinPEAppScript')) {
                if ($profileContent.PSObject.Properties[$retiredPropertyName]) {
                    $profileContent.PSObject.Properties.Remove($retiredPropertyName)
                    $profileChanged = $true
                    Write-Verbose "[$($MyInvocation.MyCommand.Name)] Removed $retiredPropertyName from build profile: $($profileFile.FullName)"
                }
            }

            $legacyMediaScriptProperty = $profileContent.PSObject.Properties['WinPEMediaScript']
            if ($legacyMediaScriptProperty) {
                $mediaScriptProperty = $profileContent.PSObject.Properties['MediaScript']
                $mediaScriptValues = @(
                    if ($mediaScriptProperty) { @($mediaScriptProperty.Value) }
                    @($legacyMediaScriptProperty.Value)
                ) | Where-Object { $null -ne $_ }

                $seenMediaScriptValues = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
                $mediaScriptValues = @($mediaScriptValues | Where-Object { $seenMediaScriptValues.Add([System.String]$_) })
                $legacyWasArray = $legacyMediaScriptProperty.Value -is [System.Array]

                if ($mediaScriptProperty) {
                    $mediaScriptProperty.Value = if ($mediaScriptValues.Count -eq 1 -and -not ($mediaScriptProperty.Value -is [System.Array]) -and -not $legacyWasArray) {
                        $mediaScriptValues[0]
                    }
                    else {
                        [System.Object[]]$mediaScriptValues
                    }
                }
                else {
                    $mediaScriptValue = if ($legacyWasArray) { [System.Object[]]$mediaScriptValues } else { $mediaScriptValues[0] }
                    $profileContent | Add-Member -NotePropertyName 'MediaScript' -NotePropertyValue $mediaScriptValue
                }

                $profileContent.PSObject.Properties.Remove('WinPEMediaScript')
                $profileChanged = $true
                Write-Verbose "[$($MyInvocation.MyCommand.Name)] Renamed WinPEMediaScript to MediaScript in build profile: $($profileFile.FullName)"
            }

            if ($profileChanged) {
                $temporaryPath = Join-Path $profileFile.DirectoryName ".$(New-Guid).tmp"
                try {
                    $profileContent | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue |
                        Out-File -LiteralPath $temporaryPath -Encoding utf8 -ErrorAction Stop
                    [System.IO.File]::Move($temporaryPath, $profileFile.FullName, $true)
                }
                finally {
                    if (Test-Path -LiteralPath $temporaryPath) {
                        Remove-Item -LiteralPath $temporaryPath -Force -ErrorAction SilentlyContinue
                    }
                }
            }
        }
        catch {
            Write-Warning "[$(Get-Date -Format s)] Could not update build profile '$($profileFile.FullName)': $($_.Exception.Message)"
        }
    }

    # Rewrite and tokenize persisted paths so migrated profiles continue to resolve content.
    foreach ($profileFile in @(Get-ChildItem -LiteralPath $Script:OSDeployCorePath -Filter 'osdeployboot.json' -File -Recurse -ErrorAction SilentlyContinue)) {
        try {
            $profileContent = Get-Content -LiteralPath $profileFile.FullName -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
            $profileChanged = $false

            $isFlatProfile = $profileFile.Directory.Parent.FullName.Equals($buildProfilesPath, [System.StringComparison]::OrdinalIgnoreCase)
            $canonicalProfileName = if ($profileContent.Architecture -in @('amd64', 'arm64')) {
                & $getCanonicalProfileName $profileFile.Directory.Name ([System.String]$profileContent.Architecture)
            }
            if ($isFlatProfile -and $profileFile.Directory.Name.Equals($canonicalProfileName, [System.StringComparison]::OrdinalIgnoreCase)) {
                $profileName = $profileFile.Directory.Name
                $nameProperty = $profileContent.PSObject.Properties['Name']
                if ($nameProperty) {
                    if ($nameProperty.Value -cne $profileName) {
                        $nameProperty.Value = $profileName
                        $profileChanged = $true
                    }
                }
                else {
                    $profileContent | Add-Member -NotePropertyName 'Name' -NotePropertyValue $profileName
                    $profileChanged = $true
                }
            }

            if ($profileContent.PSObject.Properties['WinPECustomWallpaper']) {
                $profileContent.PSObject.Properties.Remove('WinPECustomWallpaper')
                $profileChanged = $true
                Write-Verbose "[$($MyInvocation.MyCommand.Name)] Removed WinPECustomWallpaper from build profile: $($profileFile.FullName)"
            }

            foreach ($propertyName in $pathProperties) {
                $property = $profileContent.PSObject.Properties[$propertyName]
                if (-not $property -or $null -eq $property.Value) {
                    continue
                }

                # Preserve the original scalar-or-array shape while updating each stored path.
                $isArray = $property.Value -is [System.Array]
                $updatedValues = @(
                    foreach ($entry in @($property.Value)) {
                        $updatedEntry = $entry
                        if ($entry -is [System.String]) {
                            if ($propertyName -eq 'WinPEDriver') {
                                foreach ($architecture in @('amd64', 'arm64')) {
                                    $legacyCacheArchitecturePath = Join-Path $legacyCacheWinPEDriversPath $architecture
                                    $legacyBootAssetsArchitecturePath = Join-Path $legacyBootAssetsWinPEDriversPath $architecture
                                    $legacyManagedTokenPath = '${{ OSDeployCore }}' + "\winpedrivers-$architecture"
                                    $driverPathCandidates = @()
                                    $legacyDriverPath = $null

                                    if ($updatedEntry.StartsWith($legacyManagedTokenPath, [System.StringComparison]::OrdinalIgnoreCase) -and
                                        ($updatedEntry.Length -eq $legacyManagedTokenPath.Length -or $updatedEntry[$legacyManagedTokenPath.Length] -in @('\', '/'))) {
                                        $updatedEntry = $bootAssetsWinPEDriversPaths[$architecture] + $updatedEntry.Substring($legacyManagedTokenPath.Length)
                                        $profileChanged = $true
                                        break
                                    }
                                    elseif ($updatedEntry.StartsWith($legacyCacheArchitecturePath, [System.StringComparison]::OrdinalIgnoreCase) -and
                                        ($updatedEntry.Length -eq $legacyCacheArchitecturePath.Length -or $updatedEntry[$legacyCacheArchitecturePath.Length] -in @('\', '/'))) {
                                        $legacyDriverPath = $legacyCacheArchitecturePath
                                        $driverPathCandidates = @($bootAssetsWinPEDriversPaths[$architecture])
                                    }
                                    elseif ($updatedEntry.StartsWith($cachedWinPEDriversPaths[$architecture], [System.StringComparison]::OrdinalIgnoreCase) -and
                                        ($updatedEntry.Length -eq $cachedWinPEDriversPaths[$architecture].Length -or $updatedEntry[$cachedWinPEDriversPaths[$architecture].Length] -in @('\', '/'))) {
                                        $legacyDriverPath = $cachedWinPEDriversPaths[$architecture]
                                        $driverPathCandidates = @($bootAssetsWinPEDriversPaths[$architecture])
                                    }
                                    elseif ($updatedEntry.StartsWith($managedWinPEDriversPaths[$architecture], [System.StringComparison]::OrdinalIgnoreCase) -and
                                        ($updatedEntry.Length -eq $managedWinPEDriversPaths[$architecture].Length -or $updatedEntry[$managedWinPEDriversPaths[$architecture].Length] -in @('\', '/'))) {
                                        $legacyDriverPath = $managedWinPEDriversPaths[$architecture]
                                        $driverPathCandidates = @($bootAssetsWinPEDriversPaths[$architecture])
                                    }
                                    elseif ($updatedEntry.StartsWith($legacyBootAssetsArchitecturePath, [System.StringComparison]::OrdinalIgnoreCase) -and
                                        ($updatedEntry.Length -eq $legacyBootAssetsArchitecturePath.Length -or $updatedEntry[$legacyBootAssetsArchitecturePath.Length] -in @('\', '/'))) {
                                        $legacyDriverPath = $legacyBootAssetsArchitecturePath
                                        $driverPathCandidates = @($bootAssetsWinPEDriversPaths[$architecture])
                                    }

                                    if ($legacyDriverPath) {
                                        $driverPathSuffix = $updatedEntry.Substring($legacyDriverPath.Length)
                                        foreach ($driverPathCandidate in $driverPathCandidates) {
                                            $migratedDriverEntry = $driverPathCandidate + $driverPathSuffix
                                            if (Test-Path -LiteralPath $migratedDriverEntry) {
                                                $updatedEntry = $migratedDriverEntry
                                                $profileChanged = $true
                                                break
                                            }
                                        }
                                        break
                                    }
                                }
                            }

                            if ($updatedEntry.StartsWith($legacyBuildProfilesPath, [System.StringComparison]::OrdinalIgnoreCase) -and
                                ($updatedEntry.Length -eq $legacyBuildProfilesPath.Length -or $updatedEntry[$legacyBuildProfilesPath.Length] -in @('\', '/'))) {
                                $profileChanged = $true
                                $updatedEntry = $buildProfilesPath + $updatedEntry.Substring($legacyBuildProfilesPath.Length)
                            }
                            elseif ($updatedEntry.StartsWith($legacyOSDRepoBuildProfilesPath, [System.StringComparison]::OrdinalIgnoreCase) -and
                                ($updatedEntry.Length -eq $legacyOSDRepoBuildProfilesPath.Length -or $updatedEntry[$legacyOSDRepoBuildProfilesPath.Length] -in @('\', '/'))) {
                                $profileChanged = $true
                                $updatedEntry = $buildProfilesPath + $updatedEntry.Substring($legacyOSDRepoBuildProfilesPath.Length)
                            }
                            elseif ($updatedEntry.StartsWith($legacyRepositoryTokenPath, [System.StringComparison]::OrdinalIgnoreCase) -and
                                ($updatedEntry.Length -eq $legacyRepositoryTokenPath.Length -or $updatedEntry[$legacyRepositoryTokenPath.Length] -in @('\', '/'))) {
                                $profileChanged = $true
                                $updatedEntry = $Script:OSDeployBootAssetsPath + $updatedEntry.Substring($legacyRepositoryTokenPath.Length)
                            }
                            elseif ($updatedEntry.StartsWith($bootAssetsTokenPath, [System.StringComparison]::OrdinalIgnoreCase) -and
                                ($updatedEntry.Length -eq $bootAssetsTokenPath.Length -or $updatedEntry[$bootAssetsTokenPath.Length] -in @('\', '/'))) {
                                $updatedEntry = $Script:OSDeployBootAssetsPath + $updatedEntry.Substring($bootAssetsTokenPath.Length)
                            }
                            elseif ($updatedEntry.StartsWith($legacyRepositoryPath, [System.StringComparison]::OrdinalIgnoreCase) -and
                                ($updatedEntry.Length -eq $legacyRepositoryPath.Length -or $updatedEntry[$legacyRepositoryPath.Length] -in @('\', '/'))) {
                                $profileChanged = $true
                                $updatedEntry = $Script:OSDeployBootAssetsPath + $updatedEntry.Substring($legacyRepositoryPath.Length)
                            }
                            elseif ($updatedEntry.StartsWith($legacyOSDRepoPath, [System.StringComparison]::OrdinalIgnoreCase) -and
                                ($updatedEntry.Length -eq $legacyOSDRepoPath.Length -or $updatedEntry[$legacyOSDRepoPath.Length] -in @('\', '/'))) {
                                $profileChanged = $true
                                $updatedEntry = $Script:OSDeployBootAssetsPath + $updatedEntry.Substring($legacyOSDRepoPath.Length)
                            }

                            foreach ($profilePathMove in $profilePathMoves) {
                                if ($updatedEntry.StartsWith($profilePathMove.Source, [System.StringComparison]::OrdinalIgnoreCase) -and
                                    ($updatedEntry.Length -eq $profilePathMove.Source.Length -or $updatedEntry[$profilePathMove.Source.Length] -in @('\', '/'))) {
                                    $updatedEntry = $profilePathMove.Destination + $updatedEntry.Substring($profilePathMove.Source.Length)
                                    $profileChanged = $true
                                    break
                                }
                            }

                            if ($updatedEntry.StartsWith($Script:OSDeployBootAssetsPath, [System.StringComparison]::OrdinalIgnoreCase) -and
                                ($updatedEntry.Length -eq $Script:OSDeployBootAssetsPath.Length -or $updatedEntry[$Script:OSDeployBootAssetsPath.Length] -in @('\', '/'))) {
                                $bootAssetsRelativePath = $updatedEntry.Substring($Script:OSDeployBootAssetsPath.Length)
                                foreach ($legacyFolderName in $buildContentFolderMap.Keys) {
                                    $escapedFolderName = [System.Text.RegularExpressions.Regex]::Escape($legacyFolderName)
                                    $sharedFolderPattern = "^([\\/])$escapedFolderName(?=[\\/]|$)"
                                    $profileFolderPattern = "^([\\/]osdeployboot-profiles[\\/](?:(?:amd64|arm64)[\\/])?[^\\/]+[\\/])$escapedFolderName(?=[\\/]|$)"
                                    $updatedRelativePath = $bootAssetsRelativePath -ireplace $sharedFolderPattern, "`${1}$($buildContentFolderMap[$legacyFolderName])"
                                    $updatedRelativePath = $updatedRelativePath -ireplace $profileFolderPattern, "`${1}$($buildContentFolderMap[$legacyFolderName])"
                                    if ($updatedRelativePath -cne $bootAssetsRelativePath) {
                                        $bootAssetsRelativePath = $updatedRelativePath
                                        $profileChanged = $true
                                    }
                                }
                                $updatedEntry = $Script:OSDeployBootAssetsPath + $bootAssetsRelativePath
                            }

                            if ($propertyName -eq 'MediaScript') {
                                $updatedModuleEntry = $updatedEntry -ireplace '[\\/]core[\\/](?:OSDRepo[\\/])?(?:media-script|build-mediascript)(?=[\\/]|$)', '\core\boot-mediascript'
                                if ($updatedModuleEntry -cne $updatedEntry) {
                                    $updatedEntry = $updatedModuleEntry
                                    $profileChanged = $true
                                }
                            }
                            elseif ($propertyName -eq 'WinPEScript') {
                                $updatedModuleEntry = $updatedEntry -ireplace '[\\/]core[\\/](?:OSDRepo[\\/])?(?:winpe-script|build-winpescript)(?=[\\/]|$)', '\core\boot-winpescript'
                                if ($updatedModuleEntry -cne $updatedEntry) {
                                    $updatedEntry = $updatedModuleEntry
                                    $profileChanged = $true
                                }
                            }

                        }

                        if ($propertyName -eq 'WinPEStartupProfile' -and $updatedEntry -is [System.String]) {
                            $legacyProfileStartupPath = Join-Path $profileFile.DirectoryName 'winpe-profiles'
                            $profileStartupPath = Join-Path $profileFile.DirectoryName 'WinPEStartup' 'profiles'

                            if ($updatedEntry.StartsWith($legacySharedStartupProfilesPath, [System.StringComparison]::OrdinalIgnoreCase) -and
                                ($updatedEntry.Length -eq $legacySharedStartupProfilesPath.Length -or $updatedEntry[$legacySharedStartupProfilesPath.Length] -in @('\', '/'))) {
                                $updatedEntry = $sharedStartupProfilesPath + $updatedEntry.Substring($legacySharedStartupProfilesPath.Length)
                                $profileChanged = $true
                            }
                            elseif ($updatedEntry.StartsWith($legacyProfileStartupPath, [System.StringComparison]::OrdinalIgnoreCase) -and
                                ($updatedEntry.Length -eq $legacyProfileStartupPath.Length -or $updatedEntry[$legacyProfileStartupPath.Length] -in @('\', '/'))) {
                                $updatedEntry = $profileStartupPath + $updatedEntry.Substring($legacyProfileStartupPath.Length)
                                $profileChanged = $true
                            }

                            $updatedModuleEntry = $updatedEntry -ireplace '[\\/]core[\\/]OSDRepo[\\/]winpe-profiles(?=[\\/]|$)', '\core\winpestartup-profiles'
                            if ($updatedModuleEntry -cne $updatedEntry) {
                                $updatedEntry = $updatedModuleEntry
                                $profileChanged = $true
                            }
                        }

                        if ($updatedEntry -is [System.String]) {
                            $tokenizedEntry = ConvertTo-OSDeployBuildProfileToken -Path $updatedEntry
                            if ($tokenizedEntry -cne $updatedEntry) {
                                $updatedEntry = $tokenizedEntry
                                $profileChanged = $true
                            }
                        }

                        $updatedEntry
                    }
                )

                $property.Value = if ($isArray) { [System.Object[]]$updatedValues } else { $updatedValues[0] }
            }

            if ($profileChanged) {
                # Replace through a temporary file so a serialization failure cannot corrupt the profile.
                $temporaryPath = Join-Path $profileFile.DirectoryName ".$(New-Guid).tmp"
                try {
                    $profileContent | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue |
                        Out-File -LiteralPath $temporaryPath -Encoding utf8 -ErrorAction Stop
                    [System.IO.File]::Move($temporaryPath, $profileFile.FullName, $true)
                    Write-Verbose "[$($MyInvocation.MyCommand.Name)] Updated build profile: $($profileFile.FullName)"
                }
                finally {
                    if (Test-Path -LiteralPath $temporaryPath) {
                        Remove-Item -LiteralPath $temporaryPath -Force -ErrorAction SilentlyContinue
                    }
                }
            }
        }
        catch {
            Write-Warning "[$(Get-Date -Format s)] Could not update build profile '$($profileFile.FullName)': $($_.Exception.Message)"
        }
    }

    $retiredAppFolderNames = @('winpe-appscript', 'build-winpeappscript', 'build-winpeapp')
    $cleanupRoots = @($legacyRepositoryPath, $legacyOSDRepoPath, $Script:OSDeployBootAssetsPath) |
        Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Container) } |
        Select-Object -Unique

    foreach ($cleanupRoot in $cleanupRoots) {
        $retiredAppFolders = @(Get-ChildItem -LiteralPath $cleanupRoot -Directory -Recurse -Force -ErrorAction SilentlyContinue |
            Where-Object { $_.Name -in $retiredAppFolderNames } |
            Sort-Object { $_.FullName.Length } -Descending)

        foreach ($retiredAppFolder in $retiredAppFolders) {
            if (-not (Get-ChildItem -LiteralPath $retiredAppFolder.FullName -Force -ErrorAction SilentlyContinue | Select-Object -First 1)) {
                Remove-Item -LiteralPath $retiredAppFolder.FullName -Force -ErrorAction SilentlyContinue
                Write-Verbose "[$($MyInvocation.MyCommand.Name)] Removed empty retired WinPE app-script directory: $($retiredAppFolder.FullName)"
            }
        }
    }
}