From c32cb934349cb0d218698d7ead9f6c950e7d47a4 Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:54:16 -0700 Subject: [PATCH] Fix stale WinGetWin32Apps.json handling Treat WinGetWin32Apps.json as generated app orchestration output so failed or cancelled builds do not restore stale install metadata. Reuse Apps.iso when app inputs have not changed, and refresh it only when current app content or orchestration inputs are newer. --- FFUDevelopment/BuildFFUVM.ps1 | 96 +++++++++++++++---- .../FFU.Common/FFU.Common.Cleanup.psm1 | 5 + docs/build.md | 4 +- 3 files changed, 83 insertions(+), 22 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index 48acdad..2d9267d 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -2744,6 +2744,42 @@ function New-AppsISO { $AppsPath = '\\?\' + $AppsPath Invoke-Process $OSCDIMG "-n -m -d $Appspath $AppsISO" | Out-Null } +function Test-AppsIsoRefreshRequired { + param( + [string]$AppsISOPath, + [string]$AppsPath, + [string[]]$InputPaths = @() + ) + + if ([string]::IsNullOrWhiteSpace($AppsISOPath) -or -not (Test-Path -Path $AppsISOPath -PathType Leaf)) { + return $true + } + + $appsIsoLastWriteUtc = (Get-Item -Path $AppsISOPath).LastWriteTimeUtc + + foreach ($inputPath in $InputPaths) { + if ([string]::IsNullOrWhiteSpace($inputPath) -or -not (Test-Path -Path $inputPath)) { continue } + + $inputItem = Get-Item -Path $inputPath -Force + if ($inputItem.LastWriteTimeUtc -gt $appsIsoLastWriteUtc) { + WriteLog "Apps ISO refresh required because $($inputItem.FullName) is newer than $AppsISOPath" + return $true + } + } + + if (-not [string]::IsNullOrWhiteSpace($AppsPath) -and (Test-Path -Path $AppsPath -PathType Container)) { + $newerAppInput = Get-ChildItem -Path $AppsPath -File -Force -Recurse -ErrorAction SilentlyContinue | Where-Object { + $_.LastWriteTimeUtc -gt $appsIsoLastWriteUtc + } | Select-Object -First 1 + + if ($null -ne $newerAppInput) { + WriteLog "Apps ISO refresh required because $($newerAppInput.FullName) is newer than $AppsISOPath" + return $true + } + } + + return $false +} function Get-WimFromISO { #Mount ISO, get Wim file $mountResult = Mount-DiskImage -ImagePath $isoPath -PassThru @@ -5068,15 +5104,6 @@ function New-RunSession { WriteLog "Backed up DriverMapping.json to $backup" } } - if ($OrchestrationPath) { - $wgPath = Join-Path $OrchestrationPath 'WinGetWin32Apps.json' - if (Test-Path $wgPath) { - $backup2 = Join-Path $backupDir 'WinGetWin32Apps.json' - Copy-Item -Path $wgPath -Destination $backup2 -Force - $manifest.JsonBackups += @{ Path = $wgPath; Backup = $backup2 } - WriteLog "Backed up WinGetWin32Apps.json to $backup2" - } - } # Backup Office XMLs (DeployFFU.xml, DownloadFFU.xml) if present so we can restore them after cleanup if ($OfficePath) { foreach ($n in @('DeployFFU.xml', 'DownloadFFU.xml')) { @@ -5675,6 +5702,7 @@ function Cleanup-CurrentRunDownloads { 'Update-Edge.ps1', 'Install-Office.ps1', 'Install-LTSCUpdate.ps1', + 'WinGetWin32Apps.json', 'AppsScriptVariables.json' ) @@ -6461,24 +6489,47 @@ catch { #Create apps ISO for Office and/or 3rd party apps if ($InstallApps) { Set-Progress -Percentage 6 -Message "Downloading and preparing applications..." - if (Test-Path -Path $AppsISO) { + $appsIsoRefreshRequired = -not (Test-Path -Path $AppsISO -PathType Leaf) + if (-not $appsIsoRefreshRequired) { WriteLog "Apps ISO exists at: $AppsISO" - # 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." + $appIsoInputPaths = @( + $AppListPath, + $UserAppListPath, + $appInstallConfigPath, + $wingetWin32jsonFile, + $appsScriptVarsJsonPath, + $OfficeDownloadXML, + $OfficeConfigXMLFile + ) + + if ($InjectUnattend) { + $appIsoInputPaths += Get-UnattendSourcePath -UnattendFolder $UnattendFolder -WindowsArch $WindowsArch -UnattendX64FilePath $UnattendX64FilePath -UnattendArm64FilePath $UnattendArm64FilePath } - else { - WriteLog "Will use existing ISO" + + $appsIsoRefreshRequired = Test-AppsIsoRefreshRequired -AppsISOPath $AppsISO -AppsPath $AppsPath -InputPaths $appIsoInputPaths + } + + if (Test-Path -Path $wingetWin32jsonFile -PathType Leaf) { + try { + WriteLog "Removing generated Winget Win32 app manifest before app preparation: $wingetWin32jsonFile" + Remove-Item -Path $wingetWin32jsonFile -Force -ErrorAction Stop + WriteLog 'Removal complete' + } + catch { + WriteLog "Failed removing generated Winget Win32 app manifest: $($_.Exception.Message)" + if ($appsIsoRefreshRequired) { throw $_ } } } - else { + + if ($appsIsoRefreshRequired) { try { + if (Test-Path -Path $AppsISO -PathType Leaf) { + WriteLog "Refreshing Apps ISO because app inputs changed: $AppsISO" + Remove-Item -Path $AppsISO -Force -ErrorAction SilentlyContinue + WriteLog 'Removal complete' + } + #Check for and download WinGet applications if (Test-Path -Path $AppListPath) { $appList = Get-Content -Path $AppListPath -Raw | ConvertFrom-Json @@ -6930,6 +6981,9 @@ if ($InstallApps) { throw $_ } } + else { + WriteLog "Will use existing ISO" + } } #Create VHDX diff --git a/FFUDevelopment/FFU.Common/FFU.Common.Cleanup.psm1 b/FFUDevelopment/FFU.Common/FFU.Common.Cleanup.psm1 index 9afea0b..fdb1f78 100644 --- a/FFUDevelopment/FFU.Common/FFU.Common.Cleanup.psm1 +++ b/FFUDevelopment/FFU.Common/FFU.Common.Cleanup.psm1 @@ -69,6 +69,11 @@ function Invoke-FFUPostBuildCleanup { WriteLog "CommonCleanup: Removing $store" try { Remove-Item -LiteralPath $store -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $store : $($_.Exception.Message)" } } + $wingetWin32AppsJson = Join-Path (Join-Path $AppsPath 'Orchestration') 'WinGetWin32Apps.json' + if (Test-Path -LiteralPath $wingetWin32AppsJson) { + WriteLog "CommonCleanup: Removing $wingetWin32AppsJson" + try { Remove-Item -LiteralPath $wingetWin32AppsJson -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $wingetWin32AppsJson : $($_.Exception.Message)" } + } $office = Join-Path $AppsPath 'Office' if ((Test-Path -LiteralPath $office) -and $InstallOffice) { WriteLog "CommonCleanup: Checking for Office artifacts in $office" diff --git a/docs/build.md b/docs/build.md index 2784f30..daa9789 100644 --- a/docs/build.md +++ b/docs/build.md @@ -659,6 +659,8 @@ Controls the `-CleanupAppsISO` parameter. When checked, the Apps ISO file is aut During the build process, when apps are being installed, the script creates an `Apps.iso` file in the FFU Development Path (for example, `.\FFUDevelopment\Apps.iso`). This ISO contains the contents of the `.\FFUDevelopment\Apps` folder, including application installers, Office deployment files, and orchestration scripts, and is mounted to the VM during the build to install applications. +If an existing `Apps.iso` is present, FFU Builder reuses it when the app inputs have not changed. If app content, app configuration, BYO app data, or the generated `WinGetWin32Apps.json` orchestration file is newer than the ISO, the ISO is refreshed before the VM starts so the VM receives current install commands. + #### When to Disable You may want to disable Cleanup Apps ISO in the following scenarios: @@ -755,7 +757,7 @@ During the build process, application content accumulates in several subfolders | `MSStore` | Microsoft Store applications downloaded via Winget | | `Office` | Microsoft 365 Apps installer files downloaded by the Office Deployment Tool | -Additionally, the `WinGetWin32Apps.json` orchestration file in `.\FFUDevelopment\Apps\Orchestration` is removed. This file is automatically regenerated at build time based on downloaded applications. +Additionally, the `WinGetWin32Apps.json` orchestration file in `.\FFUDevelopment\Apps\Orchestration` is removed. This file is generated at build time based on the current downloaded Winget applications and is refreshed when app inputs change. When this option is enabled, the cleanup process removes: