mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 10:19:36 -06:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d39ec8802 | |||
| e3a4634d3c | |||
| ad35a0b7f9 | |||
| b2352e338b | |||
| 53741632a4 | |||
| e9652daba9 | |||
| ed5b7f669f |
@@ -1,5 +1,31 @@
|
|||||||
# Change Log
|
# 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
|
# 2512.1 UI Preview
|
||||||
|
|
||||||
## What's Changed
|
## What's Changed
|
||||||
|
|||||||
+394
-36
@@ -443,7 +443,7 @@ param(
|
|||||||
[switch]$Cleanup
|
[switch]$Cleanup
|
||||||
)
|
)
|
||||||
$ProgressPreference = 'SilentlyContinue'
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
$version = '2512.1Preview'
|
$version = '2601.1Preview'
|
||||||
|
|
||||||
# Remove any existing modules to avoid conflicts
|
# Remove any existing modules to avoid conflicts
|
||||||
if (Get-Module -Name 'FFU.Common.Core' -ErrorAction SilentlyContinue) {
|
if (Get-Module -Name 'FFU.Common.Core' -ErrorAction SilentlyContinue) {
|
||||||
@@ -568,7 +568,7 @@ class VhdxCacheItem {
|
|||||||
|
|
||||||
#Support for ini reading
|
#Support for ini reading
|
||||||
$definition = @'
|
$definition = @'
|
||||||
[DllImport("kernel32.dll")]
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||||
public static extern uint GetPrivateProfileString(
|
public static extern uint GetPrivateProfileString(
|
||||||
string lpAppName,
|
string lpAppName,
|
||||||
string lpKeyName,
|
string lpKeyName,
|
||||||
@@ -2746,8 +2746,27 @@ function Get-PrivateProfileString {
|
|||||||
[Parameter()]
|
[Parameter()]
|
||||||
[string]$KeyName
|
[string]$KeyName
|
||||||
)
|
)
|
||||||
$sbuilder = [System.Text.StringBuilder]::new(1024)
|
|
||||||
[void][Win32.Kernel32]::GetPrivateProfileString($SectionName, $KeyName, "", $sbuilder, $sbuilder.Capacity, $FileName)
|
# Read key from an INF/INI file. Use a larger buffer and allow it to grow if needed.
|
||||||
|
$bufferSize = 4096
|
||||||
|
$maxBufferSize = 65536
|
||||||
|
$sbuilder = $null
|
||||||
|
$charsCopied = 0
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$sbuilder = [System.Text.StringBuilder]::new($bufferSize)
|
||||||
|
$charsCopied = [Win32.Kernel32]::GetPrivateProfileString($SectionName, $KeyName, "", $sbuilder, [uint32]$sbuilder.Capacity, $FileName)
|
||||||
|
|
||||||
|
if ([int]$charsCopied -lt ($sbuilder.Capacity - 1)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bufferSize -ge $maxBufferSize) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
$bufferSize = [Math]::Min(($bufferSize * 2), $maxBufferSize)
|
||||||
|
}
|
||||||
|
|
||||||
return $sbuilder.ToString()
|
return $sbuilder.ToString()
|
||||||
}
|
}
|
||||||
@@ -2759,21 +2778,228 @@ function Get-PrivateProfileSection {
|
|||||||
[Parameter()]
|
[Parameter()]
|
||||||
[string]$SectionName
|
[string]$SectionName
|
||||||
)
|
)
|
||||||
$buffer = [byte[]]::new(16384)
|
# Read the requested section from an INF/INI file
|
||||||
[void][Win32.Kernel32]::GetPrivateProfileSection($SectionName, $buffer, $buffer.Length, $FileName)
|
# Some INF sections can be large; grow the buffer to avoid truncated results
|
||||||
$keyValues = [System.Text.Encoding]::Unicode.GetString($buffer).TrimEnd("`0").Split("`0")
|
|
||||||
$hashTable = @{}
|
$hashTable = @{}
|
||||||
|
$bufferSize = 16384
|
||||||
|
$buffer = $null
|
||||||
|
$charsCopied = 0
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$buffer = [byte[]]::new($bufferSize)
|
||||||
|
$charsCopied = [Win32.Kernel32]::GetPrivateProfileSection($SectionName, $buffer, $buffer.Length, $FileName)
|
||||||
|
|
||||||
|
# No section found or no content
|
||||||
|
if ($charsCopied -eq 0) {
|
||||||
|
return $hashTable
|
||||||
|
}
|
||||||
|
|
||||||
|
# If the returned data is close to the buffer size, assume truncation and retry bigger
|
||||||
|
if (($charsCopied -ge ($bufferSize - 2)) -and ($bufferSize -lt 1048576)) {
|
||||||
|
$bufferSize = $bufferSize * 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert only the returned portion of the buffer (Unicode = 2 bytes per char)
|
||||||
|
$sectionText = [System.Text.Encoding]::Unicode.GetString($buffer, 0, ($charsCopied * 2))
|
||||||
|
$keyValues = $sectionText.TrimEnd("`0").Split("`0")
|
||||||
|
|
||||||
foreach ($keyValue in $keyValues) {
|
foreach ($keyValue in $keyValues) {
|
||||||
if (![string]::IsNullOrEmpty($keyValue)) {
|
if (![string]::IsNullOrEmpty($keyValue)) {
|
||||||
$parts = $keyValue -split "="
|
$parts = $keyValue -split "=", 2
|
||||||
|
if ($parts.Count -eq 2) {
|
||||||
$hashTable[$parts[0]] = $parts[1]
|
$hashTable[$parts[0]] = $parts[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $hashTable
|
return $hashTable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-AvailableDriveLetter {
|
||||||
|
# Get an unused drive letter for temporary SUBST mappings
|
||||||
|
$usedLetters = (Get-PSDrive -PSProvider FileSystem).Name | ForEach-Object { $_.ToUpperInvariant() }
|
||||||
|
for ($ascii = [int][char]'Z'; $ascii -ge [int][char]'A'; $ascii--) {
|
||||||
|
$candidate = [char]$ascii
|
||||||
|
if ($usedLetters -notcontains $candidate) {
|
||||||
|
return $candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-DriverSubstMapping {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SourcePath
|
||||||
|
)
|
||||||
|
|
||||||
|
# Map a long driver source folder to a short drive root using SUBST
|
||||||
|
$resolvedPath = (Resolve-Path -Path $SourcePath -ErrorAction Stop).Path
|
||||||
|
$driveLetter = Get-AvailableDriveLetter
|
||||||
|
if ($null -eq $driveLetter) {
|
||||||
|
throw 'No drive letters are available for SUBST mapping.'
|
||||||
|
}
|
||||||
|
$driveName = "$driveLetter`:"
|
||||||
|
$mappedPath = "$driveLetter`:\"
|
||||||
|
WriteLog "Mapping driver folder '$resolvedPath' to $driveName with SUBST."
|
||||||
|
$escapedPath = $resolvedPath -replace '"', '""'
|
||||||
|
$arguments = "/c subst $driveName `"$escapedPath`""
|
||||||
|
Invoke-Process -FilePath cmd.exe -ArgumentList $arguments
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
DriveLetter = $driveLetter
|
||||||
|
DriveName = $driveName
|
||||||
|
DrivePath = $mappedPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Remove-DriverSubstMapping {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$DriveLetter
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove the temporary SUBST mapping
|
||||||
|
$driveName = "$DriveLetter`:"
|
||||||
|
WriteLog "Removing SUBST drive $driveName"
|
||||||
|
try {
|
||||||
|
$arguments = "/c subst $driveName /d"
|
||||||
|
Invoke-Process -FilePath cmd.exe -ArgumentList $arguments
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Failed to remove SUBST drive $($driveName): $_"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-DismDriverInjectionWithSubstLoop {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ImagePath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$DriverRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
# Resolve input paths
|
||||||
|
$resolvedImagePath = (Resolve-Path -Path $ImagePath -ErrorAction Stop).Path
|
||||||
|
$resolvedDriverRoot = (Resolve-Path -Path $DriverRoot -ErrorAction Stop).Path
|
||||||
|
|
||||||
|
# Discover INF files under the driver root
|
||||||
|
WriteLog "Scanning for INF files under: $resolvedDriverRoot"
|
||||||
|
$infFiles = Get-ChildItem -Path $resolvedDriverRoot -Filter '*.inf' -File -Recurse -ErrorAction SilentlyContinue
|
||||||
|
if ($null -eq $infFiles -or $infFiles.Count -eq 0) {
|
||||||
|
WriteLog "No INF files found under: $resolvedDriverRoot"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine the deepest stable folders we can map with SUBST (SUBST has its own max path constraints)
|
||||||
|
# Strategy:
|
||||||
|
# - Start at the INF parent folder
|
||||||
|
# - If too long for SUBST, walk up until the path is short enough
|
||||||
|
# - Deduplicate and avoid redundant child folders when a parent already covers them via DISM /Recurse
|
||||||
|
$substTargetMaxLength = 240
|
||||||
|
$candidateDirs = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
|
||||||
|
foreach ($infFile in $infFiles) {
|
||||||
|
$candidateDir = Split-Path -Path $infFile.FullName -Parent
|
||||||
|
|
||||||
|
while ($candidateDir.Length -gt $substTargetMaxLength) {
|
||||||
|
$parentDir = Split-Path -Path $candidateDir -Parent
|
||||||
|
if ([string]::IsNullOrWhiteSpace($parentDir) -or $parentDir -eq $candidateDir) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
$candidateDir = $parentDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($candidateDir.Length -gt $substTargetMaxLength) {
|
||||||
|
WriteLog "Warning: Skipping INF folder due to SUBST length limit (len=$($candidateDir.Length)): $candidateDir"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
[void]$candidateDirs.Add($candidateDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
$sortedCandidates = $candidateDirs | Sort-Object Length, @{ Expression = { $_ }; Ascending = $true }
|
||||||
|
$selectedDirs = [System.Collections.Generic.List[string]]::new()
|
||||||
|
|
||||||
|
foreach ($candidateDir in $sortedCandidates) {
|
||||||
|
$isCovered = $false
|
||||||
|
foreach ($selectedDir in $selectedDirs) {
|
||||||
|
if ($candidateDir.Equals($selectedDir, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
$isCovered = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefix = $selectedDir.TrimEnd('\') + '\'
|
||||||
|
if ($candidateDir.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
$isCovered = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $isCovered) {
|
||||||
|
[void]$selectedDirs.Add($candidateDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$infDirs = $selectedDirs | Sort-Object
|
||||||
|
WriteLog "Driver injection will process $($infDirs.Count) SUBST-safe folders (candidateFolders=$($candidateDirs.Count), INF total=$($infFiles.Count), substMaxLen=$substTargetMaxLength)."
|
||||||
|
|
||||||
|
# Use a single SUBST drive letter and reuse it in a loop (map -> dism -> unmap)
|
||||||
|
$driveLetter = Get-AvailableDriveLetter
|
||||||
|
if ($null -eq $driveLetter) {
|
||||||
|
throw 'No drive letters are available for SUBST mapping.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$driveName = "$driveLetter`:"
|
||||||
|
$drivePath = "$driveLetter`:\"
|
||||||
|
WriteLog "Using SUBST drive $driveName for driver injection loop."
|
||||||
|
|
||||||
|
$currentIndex = 0
|
||||||
|
foreach ($infDir in $infDirs) {
|
||||||
|
$currentIndex++
|
||||||
|
$escapedPath = $infDir -replace '"', '""'
|
||||||
|
|
||||||
|
try {
|
||||||
|
WriteLog "[$currentIndex/$($infDirs.Count)] Mapping '$infDir' to $driveName with SUBST."
|
||||||
|
$mapArgs = "/c subst $driveName `"$escapedPath`""
|
||||||
|
Invoke-Process -FilePath cmd.exe -ArgumentList $mapArgs | Out-Null
|
||||||
|
|
||||||
|
# Inject drivers (do not use \\?\ with DISM)
|
||||||
|
$dismArgs = @(
|
||||||
|
"/Image:`"$resolvedImagePath`""
|
||||||
|
'/Add-Driver'
|
||||||
|
"/Driver:$drivePath"
|
||||||
|
'/Recurse'
|
||||||
|
)
|
||||||
|
|
||||||
|
WriteLog "dism.exe $($dismArgs -join ' ')"
|
||||||
|
Invoke-Process -FilePath dism.exe -ArgumentList $dismArgs | Out-Null
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Warning: Driver injection failed for '$infDir': $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
try {
|
||||||
|
WriteLog "Removing SUBST drive $driveName"
|
||||||
|
$unmapArgs = "/c subst $driveName /d"
|
||||||
|
Invoke-Process -FilePath cmd.exe -ArgumentList $unmapArgs | Out-Null
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Warning: Failed removing SUBST drive $($driveName): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteLog "Driver injection loop complete for $resolvedDriverRoot"
|
||||||
|
}
|
||||||
|
|
||||||
function Copy-Drivers {
|
function Copy-Drivers {
|
||||||
param (
|
param (
|
||||||
[Parameter()]
|
[Parameter()]
|
||||||
@@ -2791,54 +3017,183 @@ function Copy-Drivers {
|
|||||||
# 745a17a0-74d3-11d0-b6fe-00a0c90f57da = Human Interface Devices
|
# 745a17a0-74d3-11d0-b6fe-00a0c90f57da = Human Interface Devices
|
||||||
$filterGUIDs = @("{4D36E97D-E325-11CE-BFC1-08002BE10318}", "{4D36E97B-E325-11CE-BFC1-08002BE10318}", "{4d36e96b-e325-11ce-bfc1-08002be10318}", "{4d36e96f-e325-11ce-bfc1-08002be10318}", "{745a17a0-74d3-11d0-b6fe-00a0c90f57da}")
|
$filterGUIDs = @("{4D36E97D-E325-11CE-BFC1-08002BE10318}", "{4D36E97B-E325-11CE-BFC1-08002BE10318}", "{4d36e96b-e325-11ce-bfc1-08002be10318}", "{4d36e96f-e325-11ce-bfc1-08002be10318}", "{745a17a0-74d3-11d0-b6fe-00a0c90f57da}")
|
||||||
$exclusionList = "wdmaudio.inf|Sound|Machine Learning|Camera|Firmware"
|
$exclusionList = "wdmaudio.inf|Sound|Machine Learning|Camera|Firmware"
|
||||||
|
|
||||||
|
# Log start and validate paths
|
||||||
|
WriteLog "Copying PE drivers from '$Path' to '$Output' (WindowsArch: $WindowsArch)"
|
||||||
|
if (-not (Test-Path -Path $Path)) {
|
||||||
|
WriteLog "ERROR: Drivers source path not found: $Path"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
[void](New-Item -Path $Output -ItemType Directory -Force)
|
||||||
|
|
||||||
|
$driverSourcePath = $Path
|
||||||
$pathLength = $Path.Length
|
$pathLength = $Path.Length
|
||||||
|
|
||||||
|
# Determine common arch-specific SourceDisksFiles section names
|
||||||
|
# Many INFs use 'amd64' rather than 'x64' for 64-bit paths (e.g. SourceDisksFiles.amd64)
|
||||||
|
$sourceDisksFileSections = @("SourceDisksFiles")
|
||||||
|
if ($WindowsArch -eq 'x64') {
|
||||||
|
$sourceDisksFileSections += "SourceDisksFiles.amd64"
|
||||||
|
}
|
||||||
|
elseif ($WindowsArch -eq 'arm64') {
|
||||||
|
$sourceDisksFileSections += "SourceDisksFiles.arm64"
|
||||||
|
}
|
||||||
|
|
||||||
$infFiles = Get-ChildItem -Path $Path -Recurse -Filter "*.inf"
|
$infFiles = Get-ChildItem -Path $Path -Recurse -Filter "*.inf"
|
||||||
|
WriteLog "Found $($infFiles.Count) INF files under: $driverSourcePath"
|
||||||
|
|
||||||
|
$matchedInfCount = 0
|
||||||
|
$skippedInfCount = 0
|
||||||
|
$copiedFileCount = 0
|
||||||
|
$errorCount = 0
|
||||||
|
|
||||||
for ($i = 0; $i -lt $infFiles.Count; $i++) {
|
for ($i = 0; $i -lt $infFiles.Count; $i++) {
|
||||||
$infFullName = $infFiles[$i].FullName
|
$infFullName = $infFiles[$i].FullName
|
||||||
|
# Add long path prefix to handle long paths
|
||||||
|
$longInfFullName = "\\?\$infFullName"
|
||||||
$infPath = Split-Path -Path $infFullName
|
$infPath = Split-Path -Path $infFullName
|
||||||
$childPath = $infPath.Substring($pathLength)
|
$childPath = $infPath.Substring($pathLength).TrimStart('\')
|
||||||
$targetPath = Join-Path -Path $Output -ChildPath $childPath
|
$targetPath = Join-Path -Path $Output -ChildPath $childPath
|
||||||
|
|
||||||
if ((Get-PrivateProfileString -FileName $infFullName -SectionName "version" -KeyName "ClassGUID") -in $filterGUIDs) {
|
# Log the INF files found
|
||||||
|
WriteLog "Examining PE driver INF ($($i + 1)/$($infFiles.Count)): $infFullName"
|
||||||
|
|
||||||
|
# Filter to known device classes
|
||||||
|
# Some INFs include trailing comments after the value (e.g. "{GUID} ; TODO: ..."), so normalize to the GUID token only.
|
||||||
|
$classGuidRaw = Get-PrivateProfileString -FileName $longInfFullName -SectionName "version" -KeyName "ClassGUID"
|
||||||
|
$classGuid = $classGuidRaw
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($classGuid)) {
|
||||||
|
# Remove any trailing ';' comment and trim whitespace
|
||||||
|
$classGuid = ($classGuid -split ';', 2)[0].Trim()
|
||||||
|
|
||||||
|
# Extract the GUID token if the value contains other text
|
||||||
|
if ($classGuid -match '\{[0-9A-Fa-f\-]{36}\}') {
|
||||||
|
$classGuid = $matches[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# WriteLog "ClassGUID: $classGuid"
|
||||||
|
if ($classGuid -notin $filterGUIDs) {
|
||||||
|
# WriteLog "Skipping PE driver INF due to GUID: $infFullName"
|
||||||
|
$skippedInfCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
# Avoid drivers that reference keywords from the exclusion list to keep the total size small
|
# Avoid drivers that reference keywords from the exclusion list to keep the total size small
|
||||||
if (((Get-Content -Path $infFullName) -match $exclusionList).Length -eq 0) {
|
if (((Get-Content -Path $infFullName) -match $exclusionList).Length -ne 0) {
|
||||||
$providerName = (Get-PrivateProfileString -FileName $infFullName -SectionName "Version" -KeyName "Provider").Trim("%")
|
WriteLog "Skipping PE driver INF due to exclusion match: $infFullName"
|
||||||
|
$skippedInfCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$matchedInfCount++
|
||||||
|
|
||||||
|
# Log the INF being processed
|
||||||
|
$providerName = (Get-PrivateProfileString -FileName $longInfFullName -SectionName "Version" -KeyName "Provider").Trim("%")
|
||||||
|
if ([string]::IsNullOrWhiteSpace($providerName)) {
|
||||||
|
$providerName = "Unknown Provider"
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteLog "Processing PE driver INF: $infFullName"
|
||||||
|
WriteLog "Provider: $providerName | ClassGUID: $classGuid"
|
||||||
|
WriteLog "Target folder: $targetPath"
|
||||||
|
|
||||||
WriteLog "Copying PE drivers for $providerName"
|
|
||||||
WriteLog "Driver inf is: $infFullName"
|
|
||||||
[void](New-Item -Path $targetPath -ItemType Directory -Force)
|
[void](New-Item -Path $targetPath -ItemType Directory -Force)
|
||||||
Copy-Item -Path $infFullName -Destination $targetPath -Force
|
|
||||||
$CatalogFileName = Get-PrivateProfileString -FileName $infFullName -SectionName "version" -KeyName "Catalogfile"
|
|
||||||
Copy-Item -Path "$infPath\$CatalogFileName" -Destination $targetPath -Force
|
|
||||||
|
|
||||||
$sourceDiskFiles = Get-PrivateProfileSection -FileName $infFullName -SectionName "SourceDisksFiles"
|
# Copy the INF itself
|
||||||
foreach ($sourceDiskFile in $sourceDiskFiles.Keys) {
|
try {
|
||||||
if (!$sourceDiskFiles[$sourceDiskFile].Contains(",")) {
|
Copy-Item -LiteralPath "$infFullName" -Destination "$targetPath" -Force -ErrorAction Stop
|
||||||
Copy-Item -Path "$infPath\$sourceDiskFile" -Destination $targetPath -Force
|
$copiedFileCount++
|
||||||
|
WriteLog "Copied: $infFullName -> $targetPath"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$errorCount++
|
||||||
|
WriteLog "ERROR: Failed to copy INF '$infFullName' to '$targetPath': $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy the catalog file (if specified)
|
||||||
|
$CatalogFileName = Get-PrivateProfileString -FileName $longInfFullName -SectionName "version" -KeyName "Catalogfile"
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($CatalogFileName)) {
|
||||||
|
$catalogSource = Join-Path -Path $infPath -ChildPath $CatalogFileName
|
||||||
|
if (Test-Path -Path $catalogSource) {
|
||||||
|
try {
|
||||||
|
Copy-Item -LiteralPath "$catalogSource" -Destination "$targetPath" -Force -ErrorAction Stop
|
||||||
|
$copiedFileCount++
|
||||||
|
WriteLog "Copied: $catalogSource -> $targetPath"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$errorCount++
|
||||||
|
WriteLog "ERROR: Failed to copy catalog '$catalogSource' to '$targetPath': $($_.Exception.Message)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$subdir = ($sourceDiskFiles[$sourceDiskFile] -split ",")[1]
|
$errorCount++
|
||||||
[void](New-Item -Path "$targetPath\$subdir" -ItemType Directory -Force)
|
WriteLog "ERROR: Catalog file not found: $catalogSource (INF: $infFullName)"
|
||||||
Copy-Item -Path "$infPath\$subdir\$sourceDiskFile" -Destination "$targetPath\$subdir" -Force
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "WARNING: No CatalogFile entry found in INF: $infFullName"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy all files referenced by SourceDisksFiles sections
|
||||||
|
foreach ($sectionName in $sourceDisksFileSections) {
|
||||||
|
$sourceDiskFiles = Get-PrivateProfileSection -FileName $longInfFullName -SectionName $sectionName
|
||||||
|
if ($sourceDiskFiles.Count -eq 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteLog "Copying files from INF section [$sectionName] ($($sourceDiskFiles.Count) entries)"
|
||||||
|
|
||||||
|
foreach ($sourceDiskFile in $sourceDiskFiles.Keys) {
|
||||||
|
# Determine if the file lives in a subfolder relative to the INF path
|
||||||
|
$rawValue = $sourceDiskFiles[$sourceDiskFile]
|
||||||
|
$subdir = ""
|
||||||
|
|
||||||
|
if (($null -ne $rawValue) -and ($rawValue.Contains(","))) {
|
||||||
|
$splitParts = $rawValue -split ","
|
||||||
|
if ($splitParts.Count -ge 2) {
|
||||||
|
$subdir = $splitParts[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Arch specific files override the files specified in the universal section
|
if ([string]::IsNullOrWhiteSpace($subdir)) {
|
||||||
$sourceDiskFiles = Get-PrivateProfileSection -FileName $infFullName -SectionName "SourceDisksFiles.$WindowsArch"
|
$subdir = ""
|
||||||
foreach ($sourceDiskFile in $sourceDiskFiles.Keys) {
|
}
|
||||||
if (!$sourceDiskFiles[$sourceDiskFile].Contains(",")) {
|
|
||||||
Copy-Item -Path "$infPath\$sourceDiskFile" -Destination $targetPath -Force
|
# Build source and destination paths
|
||||||
|
if ([string]::IsNullOrEmpty($subdir)) {
|
||||||
|
$sourceFilePath = Join-Path -Path $infPath -ChildPath $sourceDiskFile
|
||||||
|
$destinationFolder = $targetPath
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$subdir = ($sourceDiskFiles[$sourceDiskFile] -split ",")[1]
|
$sourceFolder = Join-Path -Path $infPath -ChildPath $subdir
|
||||||
[void](New-Item -Path "$targetPath\$subdir" -ItemType Directory -Force)
|
$sourceFilePath = Join-Path -Path $sourceFolder -ChildPath $sourceDiskFile
|
||||||
Copy-Item -Path "$infPath\$subdir\$sourceDiskFile" -Destination "$targetPath\$subdir" -Force
|
$destinationFolder = Join-Path -Path $targetPath -ChildPath $subdir
|
||||||
}
|
[void](New-Item -Path $destinationFolder -ItemType Directory -Force)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy with logging and error handling
|
||||||
|
if (Test-Path -Path $sourceFilePath) {
|
||||||
|
try {
|
||||||
|
Copy-Item -LiteralPath "$sourceFilePath" -Destination "$destinationFolder" -Force -ErrorAction Stop
|
||||||
|
$copiedFileCount++
|
||||||
|
WriteLog "Copied: $sourceFilePath -> $destinationFolder"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$errorCount++
|
||||||
|
WriteLog "ERROR: Failed to copy '$sourceFilePath' to '$destinationFolder' (INF: $infFullName): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$errorCount++
|
||||||
|
WriteLog "ERROR: Source file not found for [$sectionName] entry '$sourceDiskFile': $sourceFilePath (INF: $infFullName)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Final summary
|
||||||
|
WriteLog "PE driver copy summary: INF total=$($infFiles.Count) matched=$matchedInfCount skipped=$skippedInfCount filesCopied=$copiedFileCount errors=$errorCount"
|
||||||
}
|
}
|
||||||
|
|
||||||
function New-PEMedia {
|
function New-PEMedia {
|
||||||
@@ -2945,7 +3300,10 @@ function New-PEMedia {
|
|||||||
|
|
||||||
WriteLog "Adding drivers to WinPE media"
|
WriteLog "Adding drivers to WinPE media"
|
||||||
try {
|
try {
|
||||||
Add-WindowsDriver -Path "$WinPEFFUPath\Mount" -Driver $PEDriversFolder -Recurse -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-null
|
$WinPEMount = "$WinPEFFUPath\Mount"
|
||||||
|
|
||||||
|
# Inject drivers using deep SUBST mapping (reuse one drive letter and loop each INF folder)
|
||||||
|
Invoke-DismDriverInjectionWithSubstLoop -ImagePath $WinPEMount -DriverRoot $PEDriversFolder
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
WriteLog 'Some drivers failed to be added. This can be expected. Continuing.'
|
WriteLog 'Some drivers failed to be added. This can be expected. Continuing.'
|
||||||
@@ -3247,7 +3605,8 @@ function New-FFU {
|
|||||||
WriteLog 'Mounting complete'
|
WriteLog 'Mounting complete'
|
||||||
WriteLog 'Adding drivers - This will take a few minutes, please be patient'
|
WriteLog 'Adding drivers - This will take a few minutes, please be patient'
|
||||||
try {
|
try {
|
||||||
Add-WindowsDriver -Path "$FFUDevelopmentPath\Mount" -Driver "$DriversFolder" -Recurse -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-null
|
# Inject drivers using deep SUBST mapping (reuse one drive letter and loop each INF folder)
|
||||||
|
Invoke-DismDriverInjectionWithSubstLoop -ImagePath "$FFUDevelopmentPath\Mount" -DriverRoot "$DriversFolder"
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
WriteLog 'Some drivers failed to be added to the FFU. This can be expected. Continuing.'
|
WriteLog 'Some drivers failed to be added to the FFU. This can be expected. Continuing.'
|
||||||
@@ -6243,7 +6602,6 @@ try {
|
|||||||
New-FFU $FFUVM.Name
|
New-FFU $FFUVM.Name
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Set-Progress -Percentage 81 -Message "Starting FFU capture from VHDX..."
|
|
||||||
#Shorten Windows SKU for use in FFU file name to remove spaces and long names
|
#Shorten Windows SKU for use in FFU file name to remove spaces and long names
|
||||||
WriteLog "Shortening Windows SKU: $WindowsSKU for FFU file name"
|
WriteLog "Shortening Windows SKU: $WindowsSKU for FFU file name"
|
||||||
$shortenedWindowsSKU = Get-ShortenedWindowsSKU -WindowsSKU $WindowsSKU
|
$shortenedWindowsSKU = Get-ShortenedWindowsSKU -WindowsSKU $WindowsSKU
|
||||||
|
|||||||
@@ -221,12 +221,20 @@ function Get-Application {
|
|||||||
WriteLog "$AppName moved to $NewAppPath"
|
WriteLog "$AppName moved to $NewAppPath"
|
||||||
$result = 0 # Success for UWP app
|
$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') {
|
elseif ($appFolderPath -match 'Win32') {
|
||||||
if (-not $SkipWin32Json) {
|
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"
|
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
|
$result = Add-Win32SilentInstallCommand -AppFolder $AppName -AppFolderPath $appFolderPath -OrchestrationPath $OrchestrationPath -SubFolder $subFolderForCommand
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
WriteLog "$AppName is a Win32 app. Skipping WinGetWin32Apps.json generation (UI mode)."
|
WriteLog "$AppName is a Win32 app. Skipping WinGetWin32Apps.json generation (UI mode)."
|
||||||
$result = 0
|
$result = 0
|
||||||
@@ -448,13 +456,43 @@ function Start-WingetAppDownloadTask {
|
|||||||
$archFolders = Get-ChildItem -Path $appFolder -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -in @('x86', 'x64', 'arm64') }
|
$archFolders = Get-ChildItem -Path $appFolder -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -in @('x86', 'x64', 'arm64') }
|
||||||
if ($archFolders) {
|
if ($archFolders) {
|
||||||
foreach ($archFolder in $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"
|
WriteLog "Adding silent install command for pre-downloaded $sanitizedAppName ($($archFolder.Name)) to $OrchestrationPath\WinGetWin32Apps.json"
|
||||||
Add-Win32SilentInstallCommand -AppFolder $sanitizedAppName -AppFolderPath $archFolder.FullName -OrchestrationPath $OrchestrationPath -SubFolder $archFolder.Name | Out-Null
|
$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 {
|
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"
|
WriteLog "Adding silent install command for pre-downloaded $sanitizedAppName to $OrchestrationPath\WinGetWin32Apps.json"
|
||||||
Add-Win32SilentInstallCommand -AppFolder $sanitizedAppName -AppFolderPath $appFolder -OrchestrationPath $OrchestrationPath | Out-Null
|
$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 {
|
else {
|
||||||
@@ -602,6 +640,8 @@ function Start-WingetAppDownloadTask {
|
|||||||
2 { $status = "Silent install switch could not be found. Did not download." }
|
2 { $status = "Silent install switch could not be found. Did not download." }
|
||||||
3 { $status = "Error: Publisher does not support download" }
|
3 { $status = "Error: Publisher does not support download" }
|
||||||
4 { $status = "Skipped: Use 'msstore' source instead." }
|
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" }
|
default { $status = "Downloaded with status: $resultCode" }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -792,6 +832,9 @@ function Get-Apps {
|
|||||||
if ($overrideMap.Count -gt 0) {
|
if ($overrideMap.Count -gt 0) {
|
||||||
$winGetWin32Path = Join-Path -Path $OrchestrationPath -ChildPath 'WinGetWin32Apps.json'
|
$winGetWin32Path = Join-Path -Path $OrchestrationPath -ChildPath 'WinGetWin32Apps.json'
|
||||||
if (Test-Path -Path $winGetWin32Path) {
|
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
|
[array]$appsDataUpdated = Get-Content -Path $winGetWin32Path -Raw | ConvertFrom-Json
|
||||||
$changed = $false
|
$changed = $false
|
||||||
foreach ($entry in $appsDataUpdated) {
|
foreach ($entry in $appsDataUpdated) {
|
||||||
@@ -820,13 +863,15 @@ function Get-Apps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($changed) {
|
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"
|
WriteLog "Applied AppList.json command overrides to WinGetWin32Apps.json"
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
WriteLog "No matching apps required command overrides."
|
WriteLog "No matching apps required command overrides."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
WriteLog "WinGetWin32Apps.json not found; no overrides applied."
|
WriteLog "WinGetWin32Apps.json not found; no overrides applied."
|
||||||
}
|
}
|
||||||
@@ -835,6 +880,119 @@ function Get-Apps {
|
|||||||
catch {
|
catch {
|
||||||
WriteLog "Failed to apply AppList.json command overrides: $($_.Exception.Message)"
|
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 {
|
function Install-WinGet {
|
||||||
param (
|
param (
|
||||||
@@ -909,27 +1067,244 @@ function Confirm-WinGetInstallation {
|
|||||||
WriteLog "Installed WinGet version: $wingetVersion"
|
WriteLog "Installed WinGet version: $wingetVersion"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function Add-Win32SilentInstallCommand {
|
# --------------------------------------------------------------------------
|
||||||
|
# SECTION: WinGetWin32Apps.json File Locking Helpers
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
function Get-WinGetWin32AppsJsonMutexName {
|
||||||
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$AppFolder,
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$AppFolderPath,
|
[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)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$OrchestrationPath,
|
[string]$OrchestrationPath,
|
||||||
[string]$SubFolder
|
[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
|
$installerCandidates = Get-ChildItem -Path "$appFolderPath\*" -Include "*.exe", "*.msi" -File -ErrorAction SilentlyContinue
|
||||||
if (-not $installerCandidates) {
|
if (-not $installerCandidates) {
|
||||||
WriteLog "No win32 app installers were found. Skipping the inclusion of $AppFolder"
|
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
|
Remove-Item -Path $AppFolderPath -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Read the exported WinGet YAML
|
# Read the exported WinGet YAML (explicit file if provided; otherwise pick the first YAML found)
|
||||||
$yamlFile = Get-ChildItem -Path "$appFolderPath\*" -Include "*.yaml" -File -ErrorAction Stop
|
$yamlFile = $null
|
||||||
$yamlText = Get-Content -Path $yamlFile -Raw
|
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
|
# Attempt to resolve the correct installer from YAML NestedInstallerFiles within the matching Architecture block
|
||||||
$desiredArch = if (-not [string]::IsNullOrEmpty($SubFolder)) { $SubFolder } else { $null }
|
$desiredArch = if (-not [string]::IsNullOrEmpty($SubFolder)) { $SubFolder } else { $null }
|
||||||
@@ -987,7 +1362,12 @@ function Add-Win32SilentInstallCommand {
|
|||||||
}
|
}
|
||||||
if (-not $silentInstallSwitch) {
|
if (-not $silentInstallSwitch) {
|
||||||
WriteLog "Silent install switch for $appName could not be found. Skipping the inclusion of $appName."
|
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
|
Remove-Item -Path $appFolderPath -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1031,19 +1411,27 @@ function Add-Win32SilentInstallCommand {
|
|||||||
WriteLog "Multiple installers found. YAML not used. Falling back to single EXE: $resolvedRelativePath"
|
WriteLog "Multiple installers found. YAML not used. Falling back to single EXE: $resolvedRelativePath"
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$first = $installerCandidates | Select-Object -First 1
|
WriteLog "Multiple installers found and ambiguous for '$appName' in '$appFolderPath'."
|
||||||
$resolvedRelativePath = $first.Name
|
if (-not $SkipRemoveOnFailure) {
|
||||||
$installerExt = $first.Extension
|
Remove-Item -Path $appFolderPath -Recurse -Force
|
||||||
WriteLog "Multiple installers found and ambiguous. Selecting the first candidate: $resolvedRelativePath"
|
}
|
||||||
|
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"
|
$basePath = "D:\win32\$AppFolder"
|
||||||
if (-not [string]::IsNullOrEmpty($SubFolder)) {
|
if (-not [string]::IsNullOrEmpty($SubFolder)) {
|
||||||
$basePath = "$basePath\$SubFolder"
|
$basePath = "$basePath\$SubFolder"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Build final command/arguments
|
# Build final command/arguments
|
||||||
if ($installerExt -ieq ".exe") {
|
if ($installerExt -ieq ".exe") {
|
||||||
@@ -1061,34 +1449,105 @@ function Add-Win32SilentInstallCommand {
|
|||||||
# Path to the JSON file
|
# Path to the JSON file
|
||||||
$wingetWin32AppsJson = "$OrchestrationPath\WinGetWin32Apps.json"
|
$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
|
# 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 = @()
|
$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
|
# Create new app entry
|
||||||
|
$entryName = $appName
|
||||||
|
if ([string]::IsNullOrWhiteSpace($DependencyFor)) {
|
||||||
|
if (-not [string]::IsNullOrEmpty($SubFolder)) {
|
||||||
|
$entryName = "$appName ($SubFolder)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$newApp = [PSCustomObject]@{
|
$newApp = [PSCustomObject]@{
|
||||||
Priority = $highestPriority
|
Priority = $highestPriority
|
||||||
Name = if (-not [string]::IsNullOrEmpty($SubFolder)) { "$appName ($SubFolder)" } else { $appName }
|
Name = $entryName
|
||||||
CommandLine = $silentInstallCommand
|
CommandLine = $silentInstallCommand
|
||||||
Arguments = $silentInstallSwitch
|
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 += $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
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -794,7 +794,7 @@ $LogFileName = 'ScriptLog.txt'
|
|||||||
$USBDrive = Get-USBDrive
|
$USBDrive = Get-USBDrive
|
||||||
New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null
|
New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null
|
||||||
$LogFile = $USBDrive + $LogFilename
|
$LogFile = $USBDrive + $LogFilename
|
||||||
$version = '2512.1Preview'
|
$version = '2601.1Preview'
|
||||||
WriteLog 'Begin Logging'
|
WriteLog 'Begin Logging'
|
||||||
WriteLog "Script version: $version"
|
WriteLog "Script version: $version"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user