mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d39ec8802 | |||
| e3a4634d3c | |||
| ad35a0b7f9 | |||
| b2352e338b | |||
| 53741632a4 | |||
| e9652daba9 | |||
| ed5b7f669f | |||
| ceeabd1ebc | |||
| 15149ffa0b | |||
| 2f180747b7 | |||
| 25fe90253c | |||
| 86d122aacf | |||
| 9737d5c930 | |||
| c6088d91fa | |||
| 15fdf77ce4 | |||
| f7f001ac2e |
+121
@@ -1,5 +1,126 @@
|
||||
# Change Log
|
||||
|
||||
# 2601.1 UI Preview
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Improved WinPE driver copy reliability and logging
|
||||
|
||||
Fixed a bug where some drivers weren't being copied into WinPE media when using **Use Drivers Folder as PE Drivers Source** option (`UseDriversAsPEDrivers` parameter)
|
||||
|
||||
### Improved driver injection for long driver folder paths
|
||||
|
||||
In some cases some drivers weren't being copied to the FFU or WinPE deployment media due to long paths. This required some significant refactoring. [See this post](https://github.com/rbalsleyMSFT/FFU/discussions/375) for more details on the changes that were made and the reasoning behind them.
|
||||
|
||||
### Fixed an issue with WingetWin32Apps.Json file corruption during parallel app updates
|
||||
|
||||
A code refactor that was done to consolidate some of the winget application download work that both the UI and BuildFFUVM.ps1 script caused an issue where parallel writes to the WingetWin32Apps.json file was causing the file to corrupt, resulting in apps not installing as expected.
|
||||
|
||||
### Winget App installs now follow Applist.json order
|
||||
|
||||
Winget application installs were installing in an indeterministic way when the WingetWin32Apps.json file was created. The order will now follow the order listed in the AppList.json file.
|
||||
|
||||
### Support added for Winget Win32 app dependency handling
|
||||
|
||||
Some apps (Camtasia) require dependency apps to be installed first. Winget will download said dependency apps. Dependent applications will now install before the calling application. There is also deduplication support added in the event multiple applications have the same dependencies.
|
||||
|
||||
**Full Changelog**: [https://github.com/rbalsleyMSFT/FFU/compare/v2512.1Preview...v2601.1Preview](https://github.com/rbalsleyMSFT/FFU/compare/v2512.1Preview...v2601.1Preview)
|
||||
|
||||
# 2512.1 UI Preview
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Refactored Cleanup logic into a shared module
|
||||
|
||||
Consolidates duplicated cleanup code by moving logic into a shared function, eliminating redundant implementations across multiple locations.
|
||||
|
||||
Removes standalone cleanup functions (Remove-FFU, Remove-Apps, Remove-Updates) and replaces scattered cleanup calls with a single invocation of Invoke-FFUPostBuildCleanup.
|
||||
|
||||
### Add 30 second delay to allow for Windows Security Platform to install
|
||||
|
||||
There was an issue where the Windows Security Platform would attempt to install in the VM during the build via `Update-Defender.ps1` however the install didn't always happen and on deployment of the FFU, Windows Update would show that the Windows Security Platform needed an update. I suspect this is related to the AppxSVC not being ready during Audit Mode. Adding a 30 second delay appears to work more reliably.
|
||||
|
||||
### Windows and .NET CU's now persist across builds
|
||||
|
||||
Content in the FFUDevelopment\KB folder was always deleted once it was used. Since the Windows CU is so large now, it doesn't make sense to delete it if a user wants it again and may not be using cached VHDX files.
|
||||
|
||||
Deletion of the KB folder is now correctly handled via the **Remove Downloaded Update Files** option on the Build tab.
|
||||
|
||||
### Skip CU downloads if the Windows ESD version is current or newer
|
||||
|
||||
Now that the Windows ESD media is kept up to date, there rarely will be a need to download the latest CU. There will always be a slight gap when the latest CU comes out and the updated media is available, but that's generally just a few days to a week.
|
||||
|
||||
The script will now do some parsing of the windows version of the ESD file and the latest CU and if the ESD is newer, the CU will not be downloaded.
|
||||
|
||||
### Fixes an issue with WingetWin32Apps.json file not being created if applications were pre-downloaded via the UI
|
||||
|
||||
Fixed a bug due to some code consolidation that broke scenarios where applications that were downloaded via the UI, but were not installing in the VM.
|
||||
|
||||
**Full Changelog**: https://github.com/rbalsleyMSFT/FFU/compare/v2511.1preview...v2511.2
|
||||
|
||||
# 2511.1 UI Preview
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Major changes to drivers
|
||||
|
||||
A few weeks ago I wrote a [lengthy post](https://github.com/rbalsleyMSFT/FFU/discussions/350) asking for some help testing some changes that were added.
|
||||
|
||||
The summary of that post is that there have been significant changes for both Dell and HP driver downloads to leverage the SystemID for each model. This increases the total number of driver models that are exposed in the UI. This also requires the `DriverMapping.json` to be modified to require the SystemID and query the SystemID from WMI when doing automatic matching.
|
||||
|
||||
#### Driver folder structure changes on the USB drive - breaking change
|
||||
|
||||
Driver folder structure on the USB drive has also changed. The new structure is `Drivers\Make\Model` (e.g. `D:\Drivers\Lenovo\Lenovo 300w`). This structure is consistent with how the UI and `BuildFFUVM.ps1` script download and store drivers and automatically copy them. So if you've been following that, then no changes are required.
|
||||
|
||||
Please read [the post](https://github.com/rbalsleyMSFT/FFU/discussions/350) for more details on these changes to drivers.
|
||||
|
||||
### Windows 11 25H2 is now the default option for MCT/ESD downloads
|
||||
|
||||
For MCT/ESD downloads: Adds dynamic products.cab download functionality for Windows 11 using Windows Update service API instead of static MCT links. This is due to a change in how the MCT pulls the products.cab file. In other words, the Windows 11 25H2 ESD media is now updated each month (usually shortly after patch Tuesday)
|
||||
|
||||
### Added 8 new hardware manufactures for automatic driver matching during deployment
|
||||
|
||||
Extends hardware detection and driver mapping capabilities to support Panasonic, Viglen, AZW, Fujitsu, Getac, ByteSpeed, and Intel devices when applying the FFU to a device. This does not mean FFU Builder supports downloading drivers from these manufacturers. You'll still need to download the drivers for them manually. You can now create your own `DriverMapping.json` file to include these manufacturers.
|
||||
|
||||
Thanks to @arwidmark and the [Modern Driver Management](https://msendpointmgr.com/modern-driver-management/) team for the WMI queries.
|
||||
|
||||
### Fixed an issue with long paths when applying drivers from USB
|
||||
|
||||
Implemented SUBST drive mappings to shorten driver file paths within WinPE as some paths were causing dism to error when servicing drivers. You should see a Z:\ drive when applying drivers from the USB drive.
|
||||
|
||||
### Added an option to skip driver selection when multiple driver models are detected during deployment
|
||||
|
||||
Allows users to bypass driver installation by entering 0 at the selection prompt, providing flexibility for deployments that don't require driver updates.
|
||||
|
||||
### Add HTTP fallback for BITS transfer network authentication errors
|
||||
|
||||
Fixes an issue with standard users elevating PowerShell as Admin and getting BITS errors when trying to download content.
|
||||
|
||||
### Add -BitsPriority script parameter
|
||||
|
||||
Introduces a new parameter `-BitsPriority` with options `(Foreground, High, Normal, Low)` to control BITS download priority across the build system and UI, allowing users to optimize transfer speeds when needed.
|
||||
|
||||
The feature adds a priority selector to the UI with four options (Foreground, High, Normal, Low) and propagates the selection through the build script and common modules. Priority can be set via UI or command-line parameter with Normal as the default.
|
||||
|
||||
### BYO Apps: Add MSI path quoting to handle spaces in msiexec arguments
|
||||
|
||||
When specifying Build Your Own Apps msiexec arguments, if there were spaces in the argument list that weren't quoted properly, you'd get an error. This should now automatically add missing spaces in case you forget to add them or there are spaces in your application name.
|
||||
|
||||
### Misc Fixes
|
||||
|
||||
* Fixed some reliability issues when trying to download Lenovo drivers
|
||||
* Fixed an issue with PPKG files with spaces
|
||||
* Replaced SerialNumber with UniqueID for USB drive identification when building USB drives. USB drive manufacturers may use the same serial number for different drives, potentially causing data loss if the wrong drive is chosen.
|
||||
* `-Threads` parameter has been added to `BuildFFUVM.ps1` which defaults to 5, matching the UI behavior. This value can be 1-64.
|
||||
* ESD media downloads now use BITS by default
|
||||
* Fixed an issue with multi-disk devices. Prior, if multiple disks were detected, ApplyFFU.ps1 would fail. Now a menu pops up asking the end user to select the disk they want to deploy the FFU to
|
||||
|
||||
## New Contributors
|
||||
|
||||
* @arwidmark made their first contribution in https://github.com/rbalsleyMSFT/FFU/pull/325
|
||||
|
||||
**Full Changelog**: https://github.com/rbalsleyMSFT/FFU/compare/v2509.1preview...v2511.1preview
|
||||
|
||||
# 2509.1 UI Preview
|
||||
|
||||
## What's Changed
|
||||
|
||||
+605
-239
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ function Invoke-FFUPostBuildCleanup {
|
||||
[string]$CaptureISOPath,
|
||||
[string]$DeployISOPath,
|
||||
[string]$AppsISOPath,
|
||||
[string]$KBPath,
|
||||
[bool]$RemoveCaptureISO = $false,
|
||||
[bool]$RemoveDeployISO = $false,
|
||||
[bool]$RemoveAppsISO = $false,
|
||||
@@ -20,7 +21,7 @@ function Invoke-FFUPostBuildCleanup {
|
||||
$originalProgressPreference = $ProgressPreference
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
try {
|
||||
WriteLog "CommonCleanup: Starting cleanup (CaptureISO=$RemoveCaptureISO DeployISO=$RemoveDeployISO AppsISO=$RemoveAppsISO Drivers=$RemoveDrivers FFU=$RemoveFFU Apps=$RemoveApps Updates=$RemoveUpdates)."
|
||||
WriteLog "CommonCleanup: Starting cleanup (CaptureISO=$RemoveCaptureISO DeployISO=$RemoveDeployISO AppsISO=$RemoveAppsISO Drivers=$RemoveDrivers FFU=$RemoveFFU Apps=$RemoveApps Updates=$RemoveUpdates KBPath=$KBPath)."
|
||||
|
||||
# Primary ISO paths (new naming/location)
|
||||
if ($RemoveCaptureISO -and -not [string]::IsNullOrWhiteSpace($CaptureISOPath) -and (Test-Path -LiteralPath $CaptureISOPath)) {
|
||||
@@ -49,8 +50,15 @@ function Invoke-FFUPostBuildCleanup {
|
||||
}
|
||||
|
||||
if ($RemoveDrivers -and -not [string]::IsNullOrWhiteSpace($DriversPath) -and (Test-Path -LiteralPath $DriversPath -PathType Container)) {
|
||||
WriteLog "CommonCleanup: Removing contents of $DriversPath"
|
||||
try { Get-ChildItem -LiteralPath $DriversPath -Force -ErrorAction SilentlyContinue | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue } catch { WriteLog "CommonCleanup: Driver content cleanup issue: $($_.Exception.Message)" }
|
||||
WriteLog "CommonCleanup: Removing contents of $DriversPath (preserving Drivers.json and DriverMapping.json)"
|
||||
try {
|
||||
# Preserve drivers json files
|
||||
$driverItems = Get-ChildItem -LiteralPath $DriversPath -Force -ErrorAction SilentlyContinue | Where-Object { @('Drivers.json', 'DriverMapping.json') -notcontains $_.Name }
|
||||
if ($driverItems) {
|
||||
$driverItems | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
catch { WriteLog "CommonCleanup: Driver content cleanup issue: $($_.Exception.Message)" }
|
||||
}
|
||||
|
||||
if ($RemoveFFU -and -not [string]::IsNullOrWhiteSpace($FFUCapturePath) -and (Test-Path -LiteralPath $FFUCapturePath -PathType Container)) {
|
||||
@@ -72,22 +80,26 @@ function Invoke-FFUPostBuildCleanup {
|
||||
try { Remove-Item -LiteralPath $store -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $store : $($_.Exception.Message)" }
|
||||
}
|
||||
$office = Join-Path $AppsPath 'Office'
|
||||
if (Test-Path -LiteralPath $office) {
|
||||
WriteLog "CommonCleanup: Cleaning Office artifacts"
|
||||
if ((Test-Path -LiteralPath $office) -and $InstallOffice) {
|
||||
WriteLog "CommonCleanup: Checking for Office artifacts in $office"
|
||||
$officeSub = Join-Path $office 'Office'
|
||||
if (Test-Path -LiteralPath $officeSub) {
|
||||
WriteLog "CommonCleanup: Removing $officeSub"
|
||||
try { Remove-Item -LiteralPath $officeSub -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $officeSub : $($_.Exception.Message)" }
|
||||
}
|
||||
$setupExe = Join-Path $office 'setup.exe'
|
||||
if (Test-Path -LiteralPath $setupExe) {
|
||||
WriteLog "CommonCleanup: Removing $setupExe"
|
||||
try { Remove-Item -LiteralPath $setupExe -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $setupExe : $($_.Exception.Message)" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($RemoveUpdates -and -not [string]::IsNullOrWhiteSpace($AppsPath) -and (Test-Path -LiteralPath $AppsPath)) {
|
||||
$updateDirs = @('Defender', 'Edge', 'MSRT', 'OneDrive', '.NET', 'CU', 'Microcode')
|
||||
foreach ($d in $updateDirs) {
|
||||
if ($RemoveUpdates) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($AppsPath) -and (Test-Path -LiteralPath $AppsPath)) {
|
||||
# Remove per-run app update payloads stored under Apps
|
||||
$appUpdateDirs = @('Defender', 'Edge', 'MSRT', 'OneDrive')
|
||||
foreach ($d in $appUpdateDirs) {
|
||||
$target = Join-Path $AppsPath $d
|
||||
if (Test-Path -LiteralPath $target) {
|
||||
WriteLog "CommonCleanup: Removing update folder $target"
|
||||
@@ -95,6 +107,12 @@ function Invoke-FFUPostBuildCleanup {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($KBPath) -and (Test-Path -LiteralPath $KBPath)) {
|
||||
# Remove Windows/.NET CU downloads stored under KB
|
||||
WriteLog "CommonCleanup: Removing downloaded updates in $KBPath"
|
||||
try { Remove-Item -LiteralPath $KBPath -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $KBPath : $($_.Exception.Message)" }
|
||||
}
|
||||
}
|
||||
|
||||
WriteLog "CommonCleanup: Completed."
|
||||
}
|
||||
|
||||
@@ -221,12 +221,20 @@ function Get-Application {
|
||||
WriteLog "$AppName moved to $NewAppPath"
|
||||
$result = 0 # Success for UWP app
|
||||
}
|
||||
# If app is in Win32 folder, add the silent install command to the WinGetWin32Apps.json file
|
||||
# If app is in Win32 folder, add dependency entries (if any) and then add the parent silent install command
|
||||
elseif ($appFolderPath -match 'Win32') {
|
||||
if (-not $SkipWin32Json) {
|
||||
# Add dependency install commands first (de-duped). Fail if any dependency cannot be processed.
|
||||
$depResult = Add-Win32DependencySilentInstallCommands -ParentAppName $AppName -ParentAppFolderPath $appFolderPath -OrchestrationPath $OrchestrationPath -SubFolder $subFolderForCommand
|
||||
if ($depResult -ne 0) {
|
||||
WriteLog "Dependency processing failed for '$AppName'. The app will not be added to WinGetWin32Apps.json."
|
||||
$result = 5
|
||||
}
|
||||
else {
|
||||
WriteLog "$AppName is a Win32 app. Adding silent install command to $OrchestrationPath\WinGetWin32Apps.json"
|
||||
$result = Add-Win32SilentInstallCommand -AppFolder $AppName -AppFolderPath $appFolderPath -OrchestrationPath $OrchestrationPath -SubFolder $subFolderForCommand
|
||||
}
|
||||
}
|
||||
else {
|
||||
WriteLog "$AppName is a Win32 app. Skipping WinGetWin32Apps.json generation (UI mode)."
|
||||
$result = 0
|
||||
@@ -441,6 +449,56 @@ function Start-WingetAppDownloadTask {
|
||||
$status = "Not Downloaded: Existing content found in $appFolder"
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
WriteLog "Found existing content for '$appName' in '$appFolder'. Skipping download to prevent duplicate entry."
|
||||
|
||||
# Regenerate WinGetWin32Apps.json for CLI builds when content already exists
|
||||
# UI mode pre-downloads should not generate this file (SkipWin32Json)
|
||||
if (-not $SkipWin32Json) {
|
||||
$archFolders = Get-ChildItem -Path $appFolder -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -in @('x86', 'x64', 'arm64') }
|
||||
if ($archFolders) {
|
||||
foreach ($archFolder in $archFolders) {
|
||||
# Add dependencies first (fail if dependencies cannot be processed)
|
||||
$depResult = Add-Win32DependencySilentInstallCommands -ParentAppName $sanitizedAppName -ParentAppFolderPath $archFolder.FullName -OrchestrationPath $OrchestrationPath -SubFolder $archFolder.Name
|
||||
if ($depResult -ne 0) {
|
||||
$status = "Error: Dependency manifests could not be processed for $sanitizedAppName ($($archFolder.Name))"
|
||||
WriteLog $status
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 5 }
|
||||
}
|
||||
|
||||
WriteLog "Adding silent install command for pre-downloaded $sanitizedAppName ($($archFolder.Name)) to $OrchestrationPath\WinGetWin32Apps.json"
|
||||
$addResult = Add-Win32SilentInstallCommand -AppFolder $sanitizedAppName -AppFolderPath $archFolder.FullName -OrchestrationPath $OrchestrationPath -SubFolder $archFolder.Name -SkipRemoveOnFailure
|
||||
if ($addResult -ne 0) {
|
||||
$status = "Error: Failed to generate silent install command for $sanitizedAppName ($($archFolder.Name))"
|
||||
WriteLog $status
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = $addResult }
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
# Add dependencies first (fail if dependencies cannot be processed)
|
||||
$depResult = Add-Win32DependencySilentInstallCommands -ParentAppName $sanitizedAppName -ParentAppFolderPath $appFolder -OrchestrationPath $OrchestrationPath
|
||||
if ($depResult -ne 0) {
|
||||
$status = "Error: Dependency manifests could not be processed for $sanitizedAppName"
|
||||
WriteLog $status
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 5 }
|
||||
}
|
||||
|
||||
WriteLog "Adding silent install command for pre-downloaded $sanitizedAppName to $OrchestrationPath\WinGetWin32Apps.json"
|
||||
$addResult = Add-Win32SilentInstallCommand -AppFolder $sanitizedAppName -AppFolderPath $appFolder -OrchestrationPath $OrchestrationPath -SkipRemoveOnFailure
|
||||
if ($addResult -ne 0) {
|
||||
$status = "Error: Failed to generate silent install command for $sanitizedAppName"
|
||||
WriteLog $status
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = $addResult }
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
WriteLog "Skipping WinGetWin32Apps.json regeneration for pre-downloaded $sanitizedAppName (UI mode)."
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
|
||||
}
|
||||
}
|
||||
@@ -582,6 +640,8 @@ function Start-WingetAppDownloadTask {
|
||||
2 { $status = "Silent install switch could not be found. Did not download." }
|
||||
3 { $status = "Error: Publisher does not support download" }
|
||||
4 { $status = "Skipped: Use 'msstore' source instead." }
|
||||
5 { $status = "Error: Dependency manifest processing failed. Remove app or use BYO." }
|
||||
6 { $status = "Error: Could not resolve installer from YAML. Remove app or use BYO." }
|
||||
default { $status = "Downloaded with status: $resultCode" }
|
||||
}
|
||||
|
||||
@@ -772,6 +832,9 @@ function Get-Apps {
|
||||
if ($overrideMap.Count -gt 0) {
|
||||
$winGetWin32Path = Join-Path -Path $OrchestrationPath -ChildPath 'WinGetWin32Apps.json'
|
||||
if (Test-Path -Path $winGetWin32Path) {
|
||||
# Lock WinGetWin32Apps.json during override writes to avoid any unexpected concurrent access
|
||||
$mutexName = Get-WinGetWin32AppsJsonMutexName -WinGetWin32AppsJsonPath $winGetWin32Path
|
||||
Invoke-WithNamedMutex -MutexName $mutexName -TimeoutSeconds 60 -ScriptBlock {
|
||||
[array]$appsDataUpdated = Get-Content -Path $winGetWin32Path -Raw | ConvertFrom-Json
|
||||
$changed = $false
|
||||
foreach ($entry in $appsDataUpdated) {
|
||||
@@ -800,13 +863,15 @@ function Get-Apps {
|
||||
}
|
||||
}
|
||||
if ($changed) {
|
||||
$appsDataUpdated | ConvertTo-Json -Depth 10 | Set-Content -Path $winGetWin32Path
|
||||
$jsonText = $appsDataUpdated | ConvertTo-Json -Depth 10
|
||||
Set-FileContentAtomic -Path $winGetWin32Path -Content $jsonText
|
||||
WriteLog "Applied AppList.json command overrides to WinGetWin32Apps.json"
|
||||
}
|
||||
else {
|
||||
WriteLog "No matching apps required command overrides."
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
WriteLog "WinGetWin32Apps.json not found; no overrides applied."
|
||||
}
|
||||
@@ -815,6 +880,119 @@ function Get-Apps {
|
||||
catch {
|
||||
WriteLog "Failed to apply AppList.json command overrides: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Post-processing: Ensure WinGetWin32Apps.json ordering matches AppList.json
|
||||
# Parallel downloads can append entries in completion order. We reorder and re-prioritize
|
||||
# so install order matches the ordering specified in AppList.json.
|
||||
try {
|
||||
$winGetWin32Path = Join-Path -Path $OrchestrationPath -ChildPath 'WinGetWin32Apps.json'
|
||||
if (Test-Path -Path $winGetWin32Path) {
|
||||
# Build desired order map from AppList.json (winget entries only)
|
||||
$desiredOrderMap = @{}
|
||||
$orderIndex = 0
|
||||
foreach ($app in ($apps.apps | Where-Object { $_.source -eq 'winget' })) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($app.name) -and -not $desiredOrderMap.ContainsKey($app.name)) {
|
||||
$desiredOrderMap[$app.name] = $orderIndex
|
||||
$orderIndex++
|
||||
}
|
||||
}
|
||||
|
||||
# Only attempt reordering when we have a meaningful order map
|
||||
if ($desiredOrderMap.Count -gt 0) {
|
||||
# Lock WinGetWin32Apps.json to serialize reads/writes
|
||||
$mutexName = Get-WinGetWin32AppsJsonMutexName -WinGetWin32AppsJsonPath $winGetWin32Path
|
||||
Invoke-WithNamedMutex -MutexName $mutexName -TimeoutSeconds 60 -ScriptBlock {
|
||||
# Load existing WinGetWin32Apps.json content
|
||||
[array]$currentAppsData = Get-Content -Path $winGetWin32Path -Raw | ConvertFrom-Json
|
||||
if ($null -eq $currentAppsData) {
|
||||
$currentAppsData = @()
|
||||
}
|
||||
|
||||
# Only reorder when there is more than one entry
|
||||
if ($currentAppsData.Count -gt 1) {
|
||||
# Capture original order for change detection
|
||||
$originalNames = @($currentAppsData | ForEach-Object { $_.Name })
|
||||
|
||||
# Build sortable records that preserve stable ordering for ties
|
||||
$indexed = @()
|
||||
for ($i = 0; $i -lt $currentAppsData.Count; $i++) {
|
||||
$entry = $currentAppsData[$i]
|
||||
|
||||
# If this is a dependency entry, order it with (and before) its parent app
|
||||
$dependencyFor = $null
|
||||
if ($entry.PSObject.Properties['DependencyFor']) {
|
||||
$dependencyFor = $entry.DependencyFor
|
||||
}
|
||||
|
||||
# Normalize entry names like "Foo (x64)" back to "Foo" for ordering
|
||||
$baseName = $entry.Name
|
||||
if (-not [string]::IsNullOrWhiteSpace($dependencyFor)) {
|
||||
$baseName = $dependencyFor
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($baseName)) {
|
||||
$baseName = ($baseName -replace '\s+\((x86|x64|arm64)\)$', '')
|
||||
}
|
||||
|
||||
# Determine desired order; unknown entries are pushed to the end
|
||||
$orderKey = [int]::MaxValue
|
||||
if (-not [string]::IsNullOrWhiteSpace($baseName) -and $desiredOrderMap.ContainsKey($baseName)) {
|
||||
$orderKey = [int]$desiredOrderMap[$baseName]
|
||||
}
|
||||
|
||||
# Dependencies must install before the parent app within the same OrderKey
|
||||
$isDependency = 1
|
||||
if (-not [string]::IsNullOrWhiteSpace($dependencyFor)) {
|
||||
$isDependency = 0
|
||||
}
|
||||
|
||||
$indexed += [PSCustomObject]@{
|
||||
OrderKey = $orderKey
|
||||
IsDependency = $isDependency
|
||||
OriginalIndex = $i
|
||||
App = $entry
|
||||
}
|
||||
}
|
||||
|
||||
# Sort by desired AppList.json order, dependencies first, stable within same group using OriginalIndex
|
||||
$sorted = $indexed | Sort-Object -Property OrderKey, IsDependency, OriginalIndex
|
||||
$reorderedApps = @($sorted | ForEach-Object { $_.App })
|
||||
|
||||
# Detect whether priority needs to be rewritten (even if order is unchanged)
|
||||
$priorityNeedsUpdate = $false
|
||||
for ($p = 0; $p -lt $reorderedApps.Count; $p++) {
|
||||
if ($reorderedApps[$p].PSObject.Properties['Priority'] -and $reorderedApps[$p].Priority -eq ($p + 1)) {
|
||||
continue
|
||||
}
|
||||
$priorityNeedsUpdate = $true
|
||||
break
|
||||
}
|
||||
|
||||
# Detect whether the array order actually changed
|
||||
$sortedNames = @($reorderedApps | ForEach-Object { $_.Name })
|
||||
$orderNeedsUpdate = (($originalNames -join "`n") -ne ($sortedNames -join "`n"))
|
||||
|
||||
if ($orderNeedsUpdate -or $priorityNeedsUpdate) {
|
||||
# Re-assign priority sequentially to match the ordering
|
||||
for ($p = 0; $p -lt $reorderedApps.Count; $p++) {
|
||||
$reorderedApps[$p].Priority = $p + 1
|
||||
}
|
||||
|
||||
# Write updated JSON content atomically
|
||||
$jsonText = $reorderedApps | ConvertTo-Json -Depth 10
|
||||
Set-FileContentAtomic -Path $winGetWin32Path -Content $jsonText
|
||||
WriteLog "Reordered and re-prioritized WinGetWin32Apps.json to match AppList.json ordering."
|
||||
}
|
||||
else {
|
||||
WriteLog "WinGetWin32Apps.json is already ordered to match AppList.json; no reorder needed."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Failed to reorder WinGetWin32Apps.json: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
function Install-WinGet {
|
||||
param (
|
||||
@@ -889,27 +1067,244 @@ function Confirm-WinGetInstallation {
|
||||
WriteLog "Installed WinGet version: $wingetVersion"
|
||||
}
|
||||
}
|
||||
function Add-Win32SilentInstallCommand {
|
||||
# --------------------------------------------------------------------------
|
||||
# SECTION: WinGetWin32Apps.json File Locking Helpers
|
||||
# --------------------------------------------------------------------------
|
||||
function Get-WinGetWin32AppsJsonMutexName {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$AppFolder,
|
||||
[string]$AppFolderPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$WinGetWin32AppsJsonPath
|
||||
)
|
||||
|
||||
# Create a stable, safe mutex name based on the full file path
|
||||
# This prevents cross-runspace/cross-process corruption when multiple apps write the same JSON.
|
||||
$normalizedPath = $WinGetWin32AppsJsonPath.ToLowerInvariant()
|
||||
$sha256 = [System.Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($normalizedPath)
|
||||
$hashBytes = $sha256.ComputeHash($bytes)
|
||||
}
|
||||
finally {
|
||||
$sha256.Dispose()
|
||||
}
|
||||
|
||||
$hash = -join ($hashBytes | ForEach-Object { $_.ToString('x2') })
|
||||
return "WinGetWin32AppsJsonLock_$hash"
|
||||
}
|
||||
|
||||
function Invoke-WithNamedMutex {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$MutexName,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[scriptblock]$ScriptBlock,
|
||||
[int]$TimeoutSeconds = 60
|
||||
)
|
||||
|
||||
# Use a named mutex so all parallel runspaces serialize file access
|
||||
$mutex = New-Object System.Threading.Mutex($false, $MutexName)
|
||||
$lockTaken = $false
|
||||
|
||||
try {
|
||||
$lockTaken = $mutex.WaitOne([TimeSpan]::FromSeconds($TimeoutSeconds))
|
||||
if (-not $lockTaken) {
|
||||
throw "Timed out waiting for mutex '$MutexName' after $TimeoutSeconds seconds."
|
||||
}
|
||||
|
||||
& $ScriptBlock
|
||||
}
|
||||
finally {
|
||||
if ($lockTaken) {
|
||||
try {
|
||||
$mutex.ReleaseMutex() | Out-Null
|
||||
}
|
||||
catch {
|
||||
# Best-effort release; ignore release failures
|
||||
}
|
||||
}
|
||||
$mutex.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Set-FileContentAtomic {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Content
|
||||
)
|
||||
|
||||
# Write to a unique temp file in the same directory and then rename into place
|
||||
# to reduce the chance of partial writes.
|
||||
$parentPath = Split-Path -Path $Path -Parent
|
||||
if (-not (Test-Path -Path $parentPath -PathType Container)) {
|
||||
New-Item -Path $parentPath -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$tempPath = "$Path.$([guid]::NewGuid().ToString('N')).tmp"
|
||||
Set-Content -Path $tempPath -Value $Content -Encoding UTF8
|
||||
|
||||
try {
|
||||
# PowerShell 7+ (.NET) supports overwrite via File.Move overload
|
||||
[System.IO.File]::Move($tempPath, $Path, $true)
|
||||
}
|
||||
catch {
|
||||
# Fallback for environments where overwrite overload is unavailable
|
||||
Move-Item -Path $tempPath -Destination $Path -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Get-WinGetYamlScalarValue {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$YamlText,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Key
|
||||
)
|
||||
|
||||
# Extract a simple "Key: Value" scalar from a Winget YAML file
|
||||
$regexOptions = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Multiline
|
||||
$pattern = "^\s*$Key\s*:\s*(?<val>.+?)\s*$"
|
||||
$m = [regex]::Match($YamlText, $pattern, $regexOptions)
|
||||
if (-not $m.Success) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$value = $m.Groups['val'].Value.Trim()
|
||||
$value = $value.Trim("'").Trim('"')
|
||||
return $value
|
||||
}
|
||||
|
||||
function Add-Win32DependencySilentInstallCommands {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ParentAppName,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ParentAppFolderPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OrchestrationPath,
|
||||
[string]$SubFolder
|
||||
)
|
||||
$appName = $AppFolder
|
||||
|
||||
# Discover installer candidates (top-level files as before)
|
||||
# Discover WinGet dependency manifests under the downloaded Win32 app folder
|
||||
$dependenciesFolderPath = Join-Path -Path $ParentAppFolderPath -ChildPath 'Dependencies'
|
||||
if (-not (Test-Path -Path $dependenciesFolderPath -PathType Container)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
WriteLog "Dependencies folder detected for '$ParentAppName': $dependenciesFolderPath"
|
||||
|
||||
# Require YAML manifests to generate silent install commands
|
||||
$dependencyYamlFiles = Get-ChildItem -Path $dependenciesFolderPath -Filter "*.yaml" -File -ErrorAction SilentlyContinue
|
||||
if (-not $dependencyYamlFiles -or $dependencyYamlFiles.Count -eq 0) {
|
||||
WriteLog "Dependencies folder exists for '$ParentAppName' but no .yaml files were found. Cannot generate dependency install commands."
|
||||
return 5
|
||||
}
|
||||
|
||||
# Build the VM install base path for dependency payloads (matches D:\win32 layout)
|
||||
$vmBasePath = "D:\win32\$ParentAppName"
|
||||
if (-not [string]::IsNullOrEmpty($SubFolder)) {
|
||||
$vmBasePath = "$vmBasePath\$SubFolder"
|
||||
}
|
||||
$vmDependenciesBasePath = "$vmBasePath\Dependencies"
|
||||
|
||||
# Process each dependency manifest and add it to WinGetWin32Apps.json
|
||||
foreach ($yamlFile in $dependencyYamlFiles) {
|
||||
WriteLog "Processing dependency manifest '$($yamlFile.Name)' for '$ParentAppName'"
|
||||
try {
|
||||
$yamlText = Get-Content -Path $yamlFile.FullName -Raw -ErrorAction Stop
|
||||
|
||||
$packageIdentifier = Get-WinGetYamlScalarValue -YamlText $yamlText -Key 'PackageIdentifier'
|
||||
$packageName = Get-WinGetYamlScalarValue -YamlText $yamlText -Key 'PackageName'
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($packageIdentifier)) {
|
||||
$packageIdentifier = $yamlFile.BaseName
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($packageName)) {
|
||||
$packageName = $yamlFile.BaseName
|
||||
}
|
||||
|
||||
# Add dependency entry (de-duped) and ensure it sorts before the parent app
|
||||
$depResult = Add-Win32SilentInstallCommand -AppFolder $packageName -AppFolderPath $dependenciesFolderPath -OrchestrationPath $OrchestrationPath -YamlFilePath $yamlFile.FullName -BasePathOverride $vmDependenciesBasePath -PackageIdentifier $packageIdentifier -DependencyFor $ParentAppName -SkipRemoveOnFailure
|
||||
if ($depResult -ne 0) {
|
||||
WriteLog "Failed to generate dependency install command for '$packageName' (PackageIdentifier='$packageIdentifier') under '$ParentAppName'."
|
||||
return 5
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Failed to process dependency YAML '$($yamlFile.FullName)': $($_.Exception.Message)"
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function Add-Win32SilentInstallCommand {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$AppFolder,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$AppFolderPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OrchestrationPath,
|
||||
[string]$SubFolder,
|
||||
[string]$YamlFilePath,
|
||||
[string]$BasePathOverride,
|
||||
[string]$PackageIdentifier,
|
||||
[string]$DependencyFor,
|
||||
[switch]$SkipRemoveOnFailure
|
||||
)
|
||||
|
||||
$appName = $AppFolder
|
||||
$appFolderPath = $AppFolderPath
|
||||
|
||||
# Discover installer candidates (top-level files only)
|
||||
$installerCandidates = Get-ChildItem -Path "$appFolderPath\*" -Include "*.exe", "*.msi" -File -ErrorAction SilentlyContinue
|
||||
if (-not $installerCandidates) {
|
||||
WriteLog "No win32 app installers were found. Skipping the inclusion of $AppFolder"
|
||||
|
||||
# Avoid removing shared folders (ex: Dependencies) or pre-downloaded content when requested
|
||||
if (-not $SkipRemoveOnFailure) {
|
||||
Remove-Item -Path $AppFolderPath -Recurse -Force
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Read the exported WinGet YAML
|
||||
$yamlFile = Get-ChildItem -Path "$appFolderPath\*" -Include "*.yaml" -File -ErrorAction Stop
|
||||
$yamlText = Get-Content -Path $yamlFile -Raw
|
||||
# Read the exported WinGet YAML (explicit file if provided; otherwise pick the first YAML found)
|
||||
$yamlFile = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace($YamlFilePath)) {
|
||||
$yamlFile = Get-Item -LiteralPath $YamlFilePath -ErrorAction Stop
|
||||
}
|
||||
else {
|
||||
$yamlFile = Get-ChildItem -Path "$appFolderPath\*" -Include "*.yaml" -File -ErrorAction Stop | Select-Object -First 1
|
||||
}
|
||||
$yamlText = Get-Content -Path $yamlFile.FullName -Raw
|
||||
|
||||
# When multiple installers exist in the folder (common for Dependencies), do NOT guess.
|
||||
# WinGet exports use the same basename for installer and YAML, so select the installer by YAML basename.
|
||||
if ($installerCandidates.Count -gt 1) {
|
||||
$expectedInstallerBaseName = $yamlFile.BaseName
|
||||
$matchedInstallers = $installerCandidates | Where-Object { $_.BaseName -ieq $expectedInstallerBaseName }
|
||||
|
||||
if ($matchedInstallers -and $matchedInstallers.Count -gt 0) {
|
||||
$installerCandidates = $matchedInstallers
|
||||
}
|
||||
else {
|
||||
WriteLog "Multiple installers found but none matched YAML basename '$expectedInstallerBaseName' in '$appFolderPath'."
|
||||
if (-not $SkipRemoveOnFailure) {
|
||||
Remove-Item -Path $appFolderPath -Recurse -Force
|
||||
}
|
||||
return 6
|
||||
}
|
||||
}
|
||||
|
||||
# Attempt to resolve the correct installer from YAML NestedInstallerFiles within the matching Architecture block
|
||||
$desiredArch = if (-not [string]::IsNullOrEmpty($SubFolder)) { $SubFolder } else { $null }
|
||||
@@ -967,7 +1362,12 @@ function Add-Win32SilentInstallCommand {
|
||||
}
|
||||
if (-not $silentInstallSwitch) {
|
||||
WriteLog "Silent install switch for $appName could not be found. Skipping the inclusion of $appName."
|
||||
|
||||
# Avoid removing shared folders (ex: Dependencies) or pre-downloaded content when requested
|
||||
if (-not $SkipRemoveOnFailure) {
|
||||
Remove-Item -Path $appFolderPath -Recurse -Force
|
||||
}
|
||||
|
||||
return 2
|
||||
}
|
||||
|
||||
@@ -1011,19 +1411,27 @@ function Add-Win32SilentInstallCommand {
|
||||
WriteLog "Multiple installers found. YAML not used. Falling back to single EXE: $resolvedRelativePath"
|
||||
}
|
||||
else {
|
||||
$first = $installerCandidates | Select-Object -First 1
|
||||
$resolvedRelativePath = $first.Name
|
||||
$installerExt = $first.Extension
|
||||
WriteLog "Multiple installers found and ambiguous. Selecting the first candidate: $resolvedRelativePath"
|
||||
WriteLog "Multiple installers found and ambiguous for '$appName' in '$appFolderPath'."
|
||||
if (-not $SkipRemoveOnFailure) {
|
||||
Remove-Item -Path $appFolderPath -Recurse -Force
|
||||
}
|
||||
return 6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Build the VM install base path (matches D:\win32 layout)
|
||||
$basePath = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace($BasePathOverride)) {
|
||||
$basePath = $BasePathOverride
|
||||
}
|
||||
else {
|
||||
$basePath = "D:\win32\$AppFolder"
|
||||
if (-not [string]::IsNullOrEmpty($SubFolder)) {
|
||||
$basePath = "$basePath\$SubFolder"
|
||||
}
|
||||
}
|
||||
|
||||
# Build final command/arguments
|
||||
if ($installerExt -ieq ".exe") {
|
||||
@@ -1041,34 +1449,105 @@ function Add-Win32SilentInstallCommand {
|
||||
# Path to the JSON file
|
||||
$wingetWin32AppsJson = "$OrchestrationPath\WinGetWin32Apps.json"
|
||||
|
||||
# Serialize access to WinGetWin32Apps.json to prevent corruption when multiple apps are processed in parallel
|
||||
$mutexName = Get-WinGetWin32AppsJsonMutexName -WinGetWin32AppsJsonPath $wingetWin32AppsJson
|
||||
$addOutcome = Invoke-WithNamedMutex -MutexName $mutexName -TimeoutSeconds 60 -ScriptBlock {
|
||||
# Initialize or load existing JSON data
|
||||
if (Test-Path -Path $wingetWin32AppsJson) {
|
||||
[array]$appsData = Get-Content -Path $wingetWin32AppsJson -Raw | ConvertFrom-Json
|
||||
|
||||
# Get highest priority value
|
||||
if ($appsData.Count -gt 0) {
|
||||
$highestPriority = $appsData.Count + 1
|
||||
}
|
||||
}
|
||||
else {
|
||||
$appsData = @()
|
||||
$highestPriority = 1
|
||||
if (Test-Path -Path $wingetWin32AppsJson) {
|
||||
try {
|
||||
[array]$appsData = Get-Content -Path $wingetWin32AppsJson -Raw | ConvertFrom-Json
|
||||
if ($null -eq $appsData) {
|
||||
$appsData = @()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# Backup the corrupted file so the build can continue
|
||||
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
|
||||
$backupPath = "$wingetWin32AppsJson.corrupt.$timestamp"
|
||||
try {
|
||||
Copy-Item -Path $wingetWin32AppsJson -Destination $backupPath -Force
|
||||
WriteLog "WinGetWin32Apps.json could not be parsed. Backed up corrupt file to '$backupPath' and rebuilding."
|
||||
}
|
||||
catch {
|
||||
WriteLog "WinGetWin32Apps.json could not be parsed and backup failed: $($_.Exception.Message). Rebuilding anyway."
|
||||
}
|
||||
|
||||
$appsData = @()
|
||||
}
|
||||
}
|
||||
|
||||
# De-dupe dependencies and repeated entries across apps by PackageIdentifier first, then by command+args
|
||||
$isDuplicate = $false
|
||||
if (-not [string]::IsNullOrWhiteSpace($PackageIdentifier)) {
|
||||
$existingById = $appsData | Where-Object { $_.PSObject.Properties['PackageIdentifier'] -and $_.PackageIdentifier -eq $PackageIdentifier } | Select-Object -First 1
|
||||
if ($existingById) {
|
||||
$isDuplicate = $true
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $isDuplicate) {
|
||||
$existingByCommand = $appsData | Where-Object {
|
||||
$_.PSObject.Properties['CommandLine'] -and $_.PSObject.Properties['Arguments'] -and
|
||||
$_.CommandLine -eq $silentInstallCommand -and $_.Arguments -eq $silentInstallSwitch
|
||||
} | Select-Object -First 1
|
||||
if ($existingByCommand) {
|
||||
$isDuplicate = $true
|
||||
}
|
||||
}
|
||||
|
||||
if ($isDuplicate) {
|
||||
WriteLog "Skipping duplicate Win32 install entry: Name='$appName' PackageIdentifier='$PackageIdentifier'"
|
||||
return @{
|
||||
Added = $false
|
||||
Reason = 'Duplicate'
|
||||
}
|
||||
}
|
||||
|
||||
# Calculate next priority (always set, even if the file exists but is empty)
|
||||
$highestPriority = if ($appsData.Count -gt 0) { $appsData.Count + 1 } else { 1 }
|
||||
|
||||
# Create new app entry
|
||||
$entryName = $appName
|
||||
if ([string]::IsNullOrWhiteSpace($DependencyFor)) {
|
||||
if (-not [string]::IsNullOrEmpty($SubFolder)) {
|
||||
$entryName = "$appName ($SubFolder)"
|
||||
}
|
||||
}
|
||||
|
||||
$newApp = [PSCustomObject]@{
|
||||
Priority = $highestPriority
|
||||
Name = if (-not [string]::IsNullOrEmpty($SubFolder)) { "$appName ($SubFolder)" } else { $appName }
|
||||
Name = $entryName
|
||||
CommandLine = $silentInstallCommand
|
||||
Arguments = $silentInstallSwitch
|
||||
}
|
||||
|
||||
# Add metadata for dependency ordering and dedupe tracking (ignored by installer script)
|
||||
if (-not [string]::IsNullOrWhiteSpace($DependencyFor)) {
|
||||
$newApp | Add-Member -NotePropertyName DependencyFor -NotePropertyValue $DependencyFor -Force
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($PackageIdentifier)) {
|
||||
$newApp | Add-Member -NotePropertyName PackageIdentifier -NotePropertyValue $PackageIdentifier -Force
|
||||
}
|
||||
|
||||
# Write the updated JSON file using a temp+rename to reduce partial-write risk
|
||||
$appsData += $newApp
|
||||
$appsData | ConvertTo-Json -Depth 10 | Set-Content -Path $wingetWin32AppsJson
|
||||
$jsonText = $appsData | ConvertTo-Json -Depth 10
|
||||
Set-FileContentAtomic -Path $wingetWin32AppsJson -Content $jsonText
|
||||
|
||||
WriteLog "Added $($newApp.Name) to WinGetWin32Apps.json with priority $highestPriority"
|
||||
return @{
|
||||
Added = $true
|
||||
App = $newApp
|
||||
Priority = $highestPriority
|
||||
}
|
||||
}
|
||||
|
||||
# Return 0 for success
|
||||
if ($addOutcome -and $addOutcome.Added) {
|
||||
WriteLog "Added $($addOutcome.App.Name) to WinGetWin32Apps.json with priority $($addOutcome.Priority)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Duplicate (or unexpected no-op) treated as success
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -870,6 +870,7 @@ function Invoke-RestoreDefaults {
|
||||
-CaptureISOPath $captureISOPath `
|
||||
-DeployISOPath $deployISOPath `
|
||||
-AppsISOPath $appsISOPath `
|
||||
-KBPath (Join-Path $rootPath 'KB') `
|
||||
-RemoveCaptureISO:$true `
|
||||
-RemoveDeployISO:$true `
|
||||
-RemoveAppsISO:$true `
|
||||
|
||||
@@ -794,7 +794,7 @@ $LogFileName = 'ScriptLog.txt'
|
||||
$USBDrive = Get-USBDrive
|
||||
New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null
|
||||
$LogFile = $USBDrive + $LogFilename
|
||||
$version = '2511.1Preview'
|
||||
$version = '2601.1Preview'
|
||||
WriteLog 'Begin Logging'
|
||||
WriteLog "Script version: $version"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user