Adds Winget Win32 dependency handling and ordering

Some apps (Camtasia) require dependency apps to be installed first. Winget will download said dependency apps. This commit will place those dependencies before the calling app in the WingetWin32Apps.json file.
This commit is contained in:
rbalsleyMSFT
2026-01-12 18:12:26 -08:00
parent b2352e338b
commit ad35a0b7f9
+268 -21
View File
@@ -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
@@ -448,13 +456,43 @@ function Start-WingetAppDownloadTask {
$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"
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 {
# 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"
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 {
@@ -602,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" }
}
@@ -878,8 +918,17 @@ function Get-Apps {
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)\)$', '')
}
@@ -890,15 +939,22 @@ function Get-Apps {
$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, stable within same app using OriginalIndex
$sorted = $indexed | Sort-Object -Property OrderKey, OriginalIndex
# 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)
@@ -1101,27 +1157,154 @@ function Set-FileContentAtomic {
}
}
function Add-Win32SilentInstallCommand {
function Get-WinGetYamlScalarValue {
[CmdletBinding()]
param(
[string]$AppFolder,
[string]$AppFolderPath,
[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 }
@@ -1179,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
}
@@ -1223,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") {
@@ -1255,7 +1451,7 @@ function Add-Win32SilentInstallCommand {
# Serialize access to WinGetWin32Apps.json to prevent corruption when multiple apps are processed in parallel
$mutexName = Get-WinGetWin32AppsJsonMutexName -WinGetWin32AppsJsonPath $wingetWin32AppsJson
Invoke-WithNamedMutex -MutexName $mutexName -TimeoutSeconds 60 -ScriptBlock {
$addOutcome = Invoke-WithNamedMutex -MutexName $mutexName -TimeoutSeconds 60 -ScriptBlock {
# Initialize or load existing JSON data
$appsData = @()
if (Test-Path -Path $wingetWin32AppsJson) {
@@ -1281,26 +1477,77 @@ function Add-Win32SilentInstallCommand {
}
}
# 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
$jsonText = $appsData | ConvertTo-Json -Depth 10
Set-FileContentAtomic -Path $wingetWin32AppsJson -Content $jsonText
return @{
Added = $true
App = $newApp
Priority = $highestPriority
}
}
WriteLog "Added $($newApp.Name) to WinGetWin32Apps.json with priority $highestPriority"
if ($addOutcome -and $addOutcome.Added) {
WriteLog "Added $($addOutcome.App.Name) to WinGetWin32Apps.json with priority $($addOutcome.Priority)"
return 0
}
# Return 0 for success
# Duplicate (or unexpected no-op) treated as success
return 0
}