From eac8be3d3110fad9f421919844ff76b1c14457ac Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:59:28 -0700 Subject: [PATCH 01/34] Allow custom BYO app list file paths in UI Updates the FFU UI and orchestration scripts to allow users to specify custom file paths for their Bring Your Own (BYO) app lists, rather than forcing the use of `UserAppList.json` in a specific directory. Also modifies the orchestration to sync this custom path via `AppInstallConfig.json` so that the runtime orchestration phase resolves the correct file name and path during installation. Refreshes the Apps ISO if the custom BYO app list is updated. --- .../Apps/Orchestration/Install-Win32Apps.ps1 | 25 +++--- .../Apps/Orchestration/Orchestrator.ps1 | 27 +++++- FFUDevelopment/BuildFFUVM.ps1 | 84 +++++++++++++++++-- FFUDevelopment/BuildFFUVM_UI.xaml | 27 ++++-- .../FFU.Common/FFU.Common.Parallel.psm1 | 1 + .../FFU.Common/FFU.Common.Winget.psm1 | 11 ++- .../FFUUI.Core/FFUUI.Core.Applications.psm1 | 36 ++++++-- .../FFUUI.Core/FFUUI.Core.Config.psm1 | 3 +- .../FFUUI.Core/FFUUI.Core.Handlers.psm1 | 33 ++++++-- .../FFUUI.Core/FFUUI.Core.Initialize.psm1 | 4 + .../FFUUI.Core/FFUUI.Core.Winget.psm1 | 43 ++++++---- FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 | 3 + 12 files changed, 237 insertions(+), 60 deletions(-) diff --git a/FFUDevelopment/Apps/Orchestration/Install-Win32Apps.ps1 b/FFUDevelopment/Apps/Orchestration/Install-Win32Apps.ps1 index 6d4ab63..0316bb0 100644 --- a/FFUDevelopment/Apps/Orchestration/Install-Win32Apps.ps1 +++ b/FFUDevelopment/Apps/Orchestration/Install-Win32Apps.ps1 @@ -1,3 +1,11 @@ +# Allow Orchestrator.ps1 to override the app list file paths while preserving legacy defaults. +param( + [Parameter()] + [string]$wingetAppsJsonFile = (Join-Path -Path $PSScriptRoot -ChildPath "WinGetWin32Apps.json"), + [Parameter()] + [string]$userAppsJsonFile = (Join-Path -Path (Split-Path -Parent $PSScriptRoot) -ChildPath "UserAppList.json") +) + function Invoke-Process { [CmdletBinding(SupportsShouldProcess)] param @@ -247,11 +255,6 @@ function Install-Applications { } } -# Define paths for the JSON files -$wingetAppsJsonFile = "$PSScriptRoot\WinGetWin32Apps.json" -# Look for UserAppList.json one directory level up from the script's location. This keeps the user specific json files (AppList.json and UserAppList.json in the Apps dir) -$userAppsJsonFile = Join-Path -Path (Split-Path -Parent $PSScriptRoot) -ChildPath "UserAppList.json" - # Initialize empty arrays for apps from each source $wingetApps = @() $userApps = @() @@ -286,9 +289,9 @@ if ($wingetApps.Count -gt 0) { Install-Applications -apps $wingetApps } -# Read the UserAppList.json file if it exists +# Read the configured BYO app list file if it exists if (Test-Path -Path $userAppsJsonFile) { - Write-Host "Processing UserAppList.json..." + Write-Host "Processing $(Split-Path -Path $userAppsJsonFile -Leaf)..." try { $userContent = Get-Content -Path $userAppsJsonFile -Raw -ErrorAction Stop | ConvertFrom-Json if ($userContent -is [array]) { @@ -296,19 +299,19 @@ if (Test-Path -Path $userAppsJsonFile) { Write-Host "Found $(($userApps | Measure-Object).Count) user-defined apps." } elseif ($userContent) { - $userApps = @($userContent) # Ensure it's an array + $userApps = @($userContent) Write-Host "Found 1 user-defined app." } else { - Write-Host "UserAppList.json is empty or invalid." + Write-Host "$(Split-Path -Path $userAppsJsonFile -Leaf) is empty or invalid." } } catch { - Write-Error "Failed to read or parse UserAppList.json file: $_" + Write-Error "Failed to read or parse BYO app list file '$userAppsJsonFile': $_" } } else { - Write-Host "UserAppList.json file not found. Skipping." + Write-Host "BYO app list file not found at $userAppsJsonFile. Skipping." } # Install User apps if any were found diff --git a/FFUDevelopment/Apps/Orchestration/Orchestrator.ps1 b/FFUDevelopment/Apps/Orchestration/Orchestrator.ps1 index 9f6bc68..88c23f1 100644 --- a/FFUDevelopment/Apps/Orchestration/Orchestrator.ps1 +++ b/FFUDevelopment/Apps/Orchestration/Orchestrator.ps1 @@ -28,6 +28,23 @@ Write-Host "---------------------------------------------------" -ForegroundColo # Define the path to the scripts $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +# Resolve the configured BYO app list path for runtime orchestration. +$appInstallConfigPath = Join-Path -Path $scriptPath -ChildPath "AppInstallConfig.json" +$userAppsJsonFile = Join-Path -Path (Split-Path -Parent $scriptPath) -ChildPath "UserAppList.json" + +if (Test-Path -Path $appInstallConfigPath) { + try { + $appInstallConfig = Get-Content -Path $appInstallConfigPath -Raw | ConvertFrom-Json + if ($null -ne $appInstallConfig -and $appInstallConfig.PSObject.Properties.Match('UserAppListPath').Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($appInstallConfig.UserAppListPath)) { + $userAppsJsonFile = $appInstallConfig.UserAppListPath + Write-Host "Using BYO app list path from AppInstallConfig.json: $userAppsJsonFile" + } + } + catch { + Write-Host "Failed to parse AppInstallConfig.json. Falling back to default BYO app list path." + } +} + # Define the list of scripts to run $scriptList = @( "Install-LTSCUpdate.ps1", @@ -51,7 +68,6 @@ foreach ($script in $scriptList) { switch ($script) { "Install-Win32Apps.ps1" { $wingetAppsJsonFile = Join-Path -Path $scriptPath -ChildPath "WinGetWin32Apps.json" - $userAppsJsonFile = Join-Path -Path (Split-Path -Parent $scriptPath) -ChildPath "UserAppList.json" if (-not (Test-Path -Path $wingetAppsJsonFile) -and -not (Test-Path -Path $userAppsJsonFile)) { $shouldRun = $false } @@ -69,8 +85,13 @@ foreach ($script in $scriptList) { Write-Host "---------------------------------------------------" -ForegroundColor Yellow Write-Host " Running script: $script " -ForegroundColor Yellow Write-Host "---------------------------------------------------" -ForegroundColor Yellow - # Run script and wait for it to finish - & $scriptFile + # Run script and wait for it to finish. + if ($script -eq "Install-Win32Apps.ps1") { + & $scriptFile -UserAppsJsonFile $userAppsJsonFile + } + else { + & $scriptFile + } } } diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index 6a197fb..1554d87 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -633,6 +633,7 @@ if (-not $AppsPath) { $AppsPath = "$FFUDevelopmentPath\Apps" } if (-not $AppListPath) { $AppListPath = "$AppsPath\AppList.json" } if (-not $UserAppListPath) { $UserAppListPath = "$AppsPath\UserAppList.json" } if (-not $OrchestrationPath) { $OrchestrationPath = "$AppsPath\Orchestration" } +if (-not $appInstallConfigPath) { $appInstallConfigPath = "$OrchestrationPath\AppInstallConfig.json" } if (-not $wingetWin32jsonFile) { $wingetWin32jsonFile = "$OrchestrationPath\WinGetWin32Apps.json" } if (-not $InstallOfficePath) { $InstallOfficePath = "$OrchestrationPath\Install-Office.ps1" } if (-not $InstallDefenderPath) { $InstallDefenderPath = "$OrchestrationPath\Update-Defender.ps1" } @@ -2437,6 +2438,54 @@ function Save-KB { return $fileName } +function Sync-UserAppListForOrchestration { + param( + [Parameter(Mandatory)] + [string]$SourcePath, + [Parameter(Mandatory)] + [string]$AppsPath, + [Parameter(Mandatory)] + [string]$OrchestrationPath, + [Parameter(Mandatory)] + [string]$AppInstallConfigPath + ) + + # Ensure the orchestration folder exists before writing runtime configuration. + if (-not (Test-Path -Path $OrchestrationPath -PathType Container)) { + New-Item -Path $OrchestrationPath -ItemType Directory -Force | Out-Null + } + + # Persist the runtime BYO app list path so Orchestrator can honor custom file names. + $appInstallConfig = [ordered]@{ + UserAppListPath = $null + } + + if (-not [string]::IsNullOrWhiteSpace($SourcePath) -and (Test-Path -Path $SourcePath -PathType Leaf)) { + $stagedUserAppListName = Split-Path -Path $SourcePath -Leaf + $stagedUserAppListPath = Join-Path -Path $AppsPath -ChildPath $stagedUserAppListName + + if (-not [string]::Equals([System.IO.Path]::GetFullPath($SourcePath), [System.IO.Path]::GetFullPath($stagedUserAppListPath), [System.StringComparison]::OrdinalIgnoreCase)) { + Copy-Item -Path $SourcePath -Destination $stagedUserAppListPath -Force | Out-Null + WriteLog "Staged BYO app list for orchestration: $SourcePath -> $stagedUserAppListPath" + } + else { + WriteLog "Using BYO app list already staged at $stagedUserAppListPath" + } + + $appInstallConfig.UserAppListPath = "D:\$stagedUserAppListName" + } + elseif (Test-Path -Path (Join-Path -Path $AppsPath -ChildPath 'UserAppList.json') -PathType Leaf) { + $appInstallConfig.UserAppListPath = "D:\UserAppList.json" + WriteLog "Using default BYO app list path for orchestration." + } + else { + WriteLog "No BYO app list found at configured path '$SourcePath'." + } + + $appInstallConfig | ConvertTo-Json | Set-Content -Path $AppInstallConfigPath -Encoding UTF8 + WriteLog "Wrote app install config to $AppInstallConfigPath" +} + function New-AppsISO { #Create Apps ISO file $OSCDIMG = "$adkpath`Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\oscdimg.exe" @@ -6082,7 +6131,19 @@ if ($InstallApps) { Set-Progress -Percentage 6 -Message "Downloading and preparing applications..." if (Test-Path -Path $AppsISO) { WriteLog "Apps ISO exists at: $AppsISO" - WriteLog "Will use existing ISO" + + # Refresh the Apps ISO when a BYO app list is present so the staged manifest + # and AppInstallConfig.json stay in sync with the current build inputs. + if (Test-Path -Path $UserAppListPath) { + WriteLog "Configured BYO app list detected. Refreshing Apps ISO to include the latest BYO app list data." + Sync-UserAppListForOrchestration -SourcePath $UserAppListPath -AppsPath $AppsPath -OrchestrationPath $OrchestrationPath -AppInstallConfigPath $appInstallConfigPath + Remove-Item -Path $AppsISO -Force -ErrorAction SilentlyContinue + New-AppsISO + WriteLog "Apps ISO refreshed to include the latest BYO app list data." + } + else { + WriteLog "Will use existing ISO" + } } else { try { @@ -6125,7 +6186,7 @@ if ($InstallApps) { # If there are no existing apps, use the original AppList.json directly if (-not $hasExistingApps) { WriteLog "No existing applications found. Using original AppList.json for all apps." - Get-Apps -AppList $AppListPath -AppsPath $AppsPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -LogFilePath $LogFile -ThrottleLimit $Threads + Get-Apps -AppList $AppListPath -AppsPath $AppsPath -UserAppListPath $UserAppListPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -LogFilePath $LogFile -ThrottleLimit $Threads } else { # Compare apps in AppList.json with existing installations @@ -6197,7 +6258,7 @@ if ($InstallApps) { # Download missing apps WriteLog "Downloading missing applications" - Get-Apps -AppList $modifiedAppListPath -AppsPath $AppsPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -LogFilePath $LogFile -ThrottleLimit $Threads + Get-Apps -AppList $modifiedAppListPath -AppsPath $AppsPath -UserAppListPath $UserAppListPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -LogFilePath $LogFile -ThrottleLimit $Threads # Cleanup modified app list Remove-Item -Path $modifiedAppListPath -Force @@ -6207,11 +6268,11 @@ if ($InstallApps) { } } } - # Check is UserAppList.json exists and output to the user which apps will be installed - # It's expected that the user will have already copied the applications and created the UserAppList.json file + # Check if the configured BYO app list exists and output which apps will be installed. + # It is expected that the user will have already copied the applications and created the BYO app list file. if (Test-Path -Path $UserAppListPath) { $userAppList = Get-Content -Path $UserAppListPath -Raw | ConvertFrom-Json - WriteLog "UserAppList.json found, the following apps will be installed:" + WriteLog "$(Split-Path -Path $UserAppListPath -Leaf) found, the following apps will be installed:" foreach ($app in $userAppList) { WriteLog "$($app.name)" } @@ -6525,6 +6586,9 @@ if ($InstallApps) { WriteLog "InjectUnattend is true but source file missing: $unattendSource. Skipping unattend injection." } } + # Stage the configured BYO app list and runtime config before creating the Apps ISO. + Sync-UserAppListForOrchestration -SourcePath $UserAppListPath -AppsPath $AppsPath -OrchestrationPath $OrchestrationPath -AppInstallConfigPath $appInstallConfigPath + Set-Progress -Percentage 10 -Message "Creating Apps ISO..." WriteLog "Creating $AppsISO file" New-AppsISO @@ -7434,6 +7498,7 @@ if ($InstallApps) { Remove-Item -Path $AppsISO -Force -ErrorAction SilentlyContinue WriteLog 'Removal complete' } + Sync-UserAppListForOrchestration -SourcePath $UserAppListPath -AppsPath $AppsPath -OrchestrationPath $OrchestrationPath -AppInstallConfigPath $appInstallConfigPath New-AppsISO WriteLog "Apps ISO refreshed with LTSC CU assets" } @@ -7621,6 +7686,13 @@ if (Test-Path -Path $wingetWin32jsonFile -PathType Leaf) { Remove-Item -Path $wingetWin32jsonFile -Force -ErrorAction SilentlyContinue WriteLog "Removal complete" } + +# Remove AppInstallConfig.json so it is always rebuilt next run +if (Test-Path -Path $appInstallConfigPath -PathType Leaf) { + WriteLog "Removing $appInstallConfigPath" + Remove-Item -Path $appInstallConfigPath -Force -ErrorAction SilentlyContinue + WriteLog "Removal complete" +} #Set $LongPathsEnabled registry value back to original value. $LongPathsEnabled could be $null if the registry value was not found if ($null -eq $LongPathsEnabled) { Remove-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -ErrorAction SilentlyContinue diff --git a/FFUDevelopment/BuildFFUVM_UI.xaml b/FFUDevelopment/BuildFFUVM_UI.xaml index b12a5b6..ae13f82 100644 --- a/FFUDevelopment/BuildFFUVM_UI.xaml +++ b/FFUDevelopment/BuildFFUVM_UI.xaml @@ -270,19 +270,32 @@ - + - + - +