From 6f9970261d6d44223a5a6feace2f7badf2918823 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Wed, 27 May 2026 15:32:07 -0400 Subject: [PATCH 1/3] add option to download full WinGet package manifest alongside installer --- .../AvaloniaPackageOperationHelper.cs | 43 +++++- .../SettingsPages/PackageManagerPage.axaml.cs | 8 + .../SettingsEngine_Names.cs | 2 + .../InternalsVisibleTo.cs | 1 + .../WinGetManifestDownloadOperation.cs | 142 ++++++++++++++++++ src/UniGetUI/AppOperationHelper.cs | 49 +++++- .../ManagersPages/PackageManager.xaml.cs | 11 ++ 7 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 src/UniGetUI.PackageEngine.Operations/WinGetManifestDownloadOperation.cs diff --git a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaPackageOperationHelper.cs b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaPackageOperationHelper.cs index 29d7a8331c..ff2167a7ff 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaPackageOperationHelper.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaPackageOperationHelper.cs @@ -16,6 +16,10 @@ using UniGetUI.PackageEngine.Operations; using UniGetUI.PackageEngine.PackageClasses; using UniGetUI.PackageEngine.PackageLoader; +using UniGetUI.PackageOperations; +#if WINDOWS +using UniGetUI.PackageEngine.Managers.WingetManager; +#endif namespace UniGetUI.Avalonia.Infrastructure; @@ -81,6 +85,14 @@ public static async Task AskLocationAndDownloadAsync(IPackage? package, TEL_Inst if (package is null) return; if (MainWindow.Instance is not { } win) return; +#if WINDOWS + if (package.Manager is WinGet && Settings.Get(Settings.K.WinGetDownloadFullManifest)) + { + await AskFolderAndDownloadWinGetManifestAsync(package, referral, win); + return; + } +#endif + await package.Details.Load(); if (package.Details.InstallerUrl is null) @@ -141,9 +153,18 @@ public static async Task DownloadSelectedAsync(IEnumerable packages, T var outputPath = folder?.TryGetLocalPath(); if (outputPath is null) return; + bool fullManifest = Settings.Get(Settings.K.WinGetDownloadFullManifest); foreach (var pkg in eligible) { - var op = new DownloadOperation(pkg, outputPath); + AbstractOperation op; +#if WINDOWS + if (fullManifest && pkg.Manager is WinGet) + op = new WinGetManifestDownloadOperation(pkg, outputPath); + else + op = new DownloadOperation(pkg, outputPath); +#else + op = new DownloadOperation(pkg, outputPath); +#endif op.OperationSucceeded += (_, _) => TelemetryHandler.DownloadPackage(pkg, TEL_OP_RESULT.SUCCESS, referral); op.OperationFailed += (_, _) => TelemetryHandler.DownloadPackage(pkg, TEL_OP_RESULT.FAILED, referral); AvaloniaOperationRegistry.Add(op); @@ -151,6 +172,26 @@ public static async Task DownloadSelectedAsync(IEnumerable packages, T } } +#if WINDOWS + private static async Task AskFolderAndDownloadWinGetManifestAsync( + IPackage package, + TEL_InstallReferral referral, + MainWindow win) + { + var folders = await win.StorageProvider.OpenFolderPickerAsync( + new FolderPickerOpenOptions { AllowMultiple = false }); + var folder = folders.FirstOrDefault(); + var outputPath = folder?.TryGetLocalPath(); + if (outputPath is null) return; + + var op = new WinGetManifestDownloadOperation(package, outputPath); + op.OperationSucceeded += (_, _) => TelemetryHandler.DownloadPackage(package, TEL_OP_RESULT.SUCCESS, referral); + op.OperationFailed += (_, _) => TelemetryHandler.DownloadPackage(package, TEL_OP_RESULT.FAILED, referral); + AvaloniaOperationRegistry.Add(op); + _ = op.MainThread(); + } +#endif + /// /// Runs the WinGet self-repair sequence elevated and shows a result notification. /// Only meaningful on Windows; no-ops on other platforms. diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs index 04747ae25d..1228cf75ff 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs @@ -420,6 +420,14 @@ private void BuildExtraControls(CheckboxCard_Dict disableNotifsCard) { Text = CoreTools.Translate("Force install location parameter when updating packages with custom locations"), SettingName = CoreSettings.K.WinGetForceLocationOnUpdate, + CornerRadius = new CornerRadius(0), + BorderThickness = new Thickness(1, 0, 1, 1), + }); + + ExtraControls.Children.Add(new CheckboxCard + { + Text = CoreTools.Translate("Download full package manifest alongside the installer"), + SettingName = CoreSettings.K.WinGetDownloadFullManifest, CornerRadius = new CornerRadius(0, 0, 8, 8), BorderThickness = new Thickness(1, 0, 1, 1), }); diff --git a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs index 601c1865f6..5a6e9e52e8 100644 --- a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs +++ b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs @@ -89,6 +89,7 @@ public enum K PerManagerMinimumUpdateAgeCustom, WinGetCliToolPreference, WinGetComApiPolicy, + WinGetDownloadFullManifest, DisableClassicMode, DisableInstallerHostChangeWarning, BunPreferLatestVersions, @@ -192,6 +193,7 @@ public static string ResolveKey(K key) K.PerManagerMinimumUpdateAgeCustom => "PerManagerMinimumUpdateAgeCustom", K.WinGetCliToolPreference => "WinGetCliToolPreference", K.WinGetComApiPolicy => "WinGetComApiPolicy", + K.WinGetDownloadFullManifest => "WinGetDownloadFullManifest", K.DisableClassicMode => "DisableClassicMode", K.DisableInstallerHostChangeWarning => "DisableInstallerHostChangeWarning", K.BunPreferLatestVersions => "BunPreferLatestVersions", diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/InternalsVisibleTo.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/InternalsVisibleTo.cs index eeb63dad19..f9f7a709af 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/InternalsVisibleTo.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/InternalsVisibleTo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("UniGetUI.PackageEngine.Tests")] +[assembly: InternalsVisibleTo("UniGetUI.PackageEngine.Operations")] diff --git a/src/UniGetUI.PackageEngine.Operations/WinGetManifestDownloadOperation.cs b/src/UniGetUI.PackageEngine.Operations/WinGetManifestDownloadOperation.cs new file mode 100644 index 0000000000..2a063e5632 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Operations/WinGetManifestDownloadOperation.cs @@ -0,0 +1,142 @@ +#if WINDOWS +using UniGetUI.Core.Logging; +using UniGetUI.Core.Tools; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.Managers.WingetManager; +using UniGetUI.PackageOperations; + +namespace UniGetUI.PackageEngine.Operations; + +public class WinGetManifestDownloadOperation : AbstractProcessOperation +{ + private readonly IPackage _package; + private readonly string _downloadDirectory; + public IPackage Package => _package; + public string DownloadLocation => _downloadDirectory; + + public WinGetManifestDownloadOperation(IPackage package, string downloadDirectory) + : base(true, null) + { + _package = package; + _downloadDirectory = downloadDirectory; + + Metadata.OperationInformation = + "Downloading installer and manifest for WinGet Package=" + + _package.Id + + " into " + + _downloadDirectory; + Metadata.Title = CoreTools.Translate( + "{package} installer and manifest download", + new Dictionary { { "package", _package.Name } } + ); + Metadata.Status = CoreTools.Translate( + "{0} installer and manifest are being downloaded", + _package.Name + ); + Metadata.SuccessTitle = CoreTools.Translate("Download succeeded"); + Metadata.SuccessMessage = CoreTools.Translate( + "{package} installer and manifest were downloaded successfully", + new Dictionary { { "package", _package.Name } } + ); + Metadata.FailureTitle = CoreTools.Translate( + "Download failed", + new Dictionary { { "package", _package.Name } } + ); + Metadata.FailureMessage = CoreTools.Translate( + "{package} installer and manifest could not be downloaded", + new Dictionary { { "package", _package.Name } } + ); + } + + public override Task GetOperationIcon() + { + return Task.Run(_package.GetIconUrl); + } + + protected override void ApplyRetryAction(string retryMode) + { + // Do nothing + } + + protected override void PrepareProcessStartInfo() + { + var winget = (WinGet)_package.Manager; + bool usePinget = winget.SelectedCliToolKind == WinGetCliToolKind.BundledPinget; + string executablePath = winget.Status.ExecutablePath; + string callArgs = winget.Status.ExecutableCallArgs; + + // pinget download writes only the installer; the YAML manifests are produced + // only by the native winget CLI. When pinget is selected, try to swap in the + // system winget for this operation so we get the full manifest folder. + if (usePinget) + { + var (found, systemWinGetPath) = CoreTools.Which("winget.exe"); + if (found) + { + Logger.Info( + $"WinGetManifestDownloadOperation: pinget is the configured CLI but only the installer would be downloaded; " + + $"using system winget at {systemWinGetPath} so YAML manifests are also written." + ); + executablePath = systemWinGetPath; + callArgs = ""; + usePinget = false; + } + else + { + Logger.Warn( + "WinGetManifestDownloadOperation: pinget is the configured CLI and system winget was not found on PATH. " + + "Only the installer file will be downloaded; manifest YAML files will not be produced." + ); + } + } + + List args = ["download"]; + + args.AddRange(WinGetPkgOperationHelper.GetIdNamePiece(_package).Split(" ")); + + if (!_package.Source.IsVirtualManager) + { + args.AddRange(["--source", _package.Source.Name]); + } + + args.AddRange(["--download-directory", $"\"{_downloadDirectory}\""]); + + if (!usePinget) + { + args.AddRange( + [ + "--accept-package-agreements", + "--accept-source-agreements", + "--disable-interactivity", + ] + ); + + string proxyArg = WinGet.GetProxyArgument(); + if (proxyArg.Length > 0) + { + args.AddRange(proxyArg.Split(' ')); + } + } + + process.StartInfo.FileName = executablePath; + process.StartInfo.Arguments = (callArgs + " " + string.Join(" ", args)).TrimStart(); + } + + protected override Task GetProcessVeredict( + int ReturnCode, + List Output + ) + { + // winget download return codes follow the standard winget code space. + uint uintCode = (uint)ReturnCode; + + if (uintCode is 0x8A150077 or 0x8A15010C or 0x8A150005) + return Task.FromResult(OperationVeredict.Canceled); + + return Task.FromResult( + ReturnCode == 0 ? OperationVeredict.Success : OperationVeredict.Failure + ); + } +} +#endif diff --git a/src/UniGetUI/AppOperationHelper.cs b/src/UniGetUI/AppOperationHelper.cs index 01348477a8..f4cc6d7806 100644 --- a/src/UniGetUI/AppOperationHelper.cs +++ b/src/UniGetUI/AppOperationHelper.cs @@ -3,6 +3,7 @@ using Microsoft.UI.Xaml.Controls; using UniGetUI.Controls.OperationWidgets; using UniGetUI.Core.Logging; +using UniGetUI.Core.SettingsEngine; using UniGetUI.Core.Tools; using UniGetUI.Interface; using UniGetUI.Interface.Enums; @@ -11,6 +12,7 @@ using UniGetUI.PackageEngine.Enums; using UniGetUI.PackageEngine.Interfaces; using UniGetUI.PackageEngine.Managers.PowerShellManager; +using UniGetUI.PackageEngine.Managers.WingetManager; using UniGetUI.PackageEngine.Operations; using UniGetUI.PackageEngine.PackageClasses; using UniGetUI.PackageEngine.PackageLoader; @@ -65,6 +67,15 @@ TEL_InstallReferral referral { if (package is null) return null; + + if ( + package.Manager is WinGet + && Settings.Get(Settings.K.WinGetDownloadFullManifest) + ) + { + return await AskFolderAndDownloadWinGetManifest(package, referral); + } + int loadingId = DialogHelper.ShowLoadingDialog(CoreTools.Translate("Please wait...")); try { @@ -152,6 +163,38 @@ TEL_InstallReferral referral } } + private static async Task AskFolderAndDownloadWinGetManifest( + IPackage package, + TEL_InstallReferral referral + ) + { + try + { + var hWnd = Instance.MainWindow.GetWindowHandle(); + var picker = new ExternalLibraries.Pickers.FolderPicker(hWnd); + var outputPath = await Task.Run(picker.Show); + if (string.IsNullOrEmpty(outputPath)) + return null; + + var op = new WinGetManifestDownloadOperation(package, outputPath); + op.OperationSucceeded += (_, _) => + TelemetryHandler.DownloadPackage(package, TEL_OP_RESULT.SUCCESS, referral); + op.OperationFailed += (_, _) => + TelemetryHandler.DownloadPackage(package, TEL_OP_RESULT.FAILED, referral); + Add(op); + Instance.MainWindow.UpdateSystemTrayStatus(); + return op; + } + catch (Exception ex) + { + Logger.Error( + $"An error occurred while downloading the WinGet manifest for package {package.Id}" + ); + Logger.Error(ex); + return null; + } + } + public static async Task Download( IEnumerable packages, TEL_InstallReferral referral @@ -168,6 +211,7 @@ TEL_InstallReferral referral if (outputPath == "") return; + bool fullManifest = Settings.Get(Settings.K.WinGetDownloadFullManifest); foreach (var package in packages) { if ( @@ -179,7 +223,10 @@ TEL_InstallReferral referral continue; } - var op = new DownloadOperation(package, outputPath); + AbstractOperation op = + fullManifest && package.Manager is WinGet + ? new WinGetManifestDownloadOperation(package, outputPath) + : new DownloadOperation(package, outputPath); op.OperationSucceeded += (_, _) => TelemetryHandler.DownloadPackage(package, TEL_OP_RESULT.SUCCESS, referral); op.OperationFailed += (_, _) => diff --git a/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs b/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs index b951159157..f40d790f28 100644 --- a/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs +++ b/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs @@ -223,6 +223,17 @@ protected override void OnNavigatedTo(NavigationEventArgs e) }; ExtraControls.Children.Add(WinGet_ForceLocationWhenUpdating); + CheckboxCard WinGet_DownloadFullManifest = new() + { + Text = CoreTools.Translate( + "Download full package manifest alongside the installer" + ), + SettingName = Settings.K.WinGetDownloadFullManifest, + CornerRadius = new CornerRadius(0), + BorderThickness = new Thickness(1, 0, 1, 1), + }; + ExtraControls.Children.Add(WinGet_DownloadFullManifest); + CheckboxCard WinGet_EnableTroubleshooter = new() { Text = CoreTools.Translate("Enable the automatic WinGet troubleshooter"), From d5ab5b409385d2f0d50723f44368f3ca924bbede Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Wed, 27 May 2026 16:39:29 -0400 Subject: [PATCH 2/3] Drop system-winget fallback now that pinget emits the manifest --- .../WinGetManifestDownloadOperation.cs | 33 ++----------------- 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/src/UniGetUI.PackageEngine.Operations/WinGetManifestDownloadOperation.cs b/src/UniGetUI.PackageEngine.Operations/WinGetManifestDownloadOperation.cs index 2a063e5632..db52ad744e 100644 --- a/src/UniGetUI.PackageEngine.Operations/WinGetManifestDownloadOperation.cs +++ b/src/UniGetUI.PackageEngine.Operations/WinGetManifestDownloadOperation.cs @@ -1,5 +1,4 @@ #if WINDOWS -using UniGetUI.Core.Logging; using UniGetUI.Core.Tools; using UniGetUI.PackageEngine.Enums; using UniGetUI.PackageEngine.Interfaces; @@ -63,33 +62,6 @@ protected override void PrepareProcessStartInfo() { var winget = (WinGet)_package.Manager; bool usePinget = winget.SelectedCliToolKind == WinGetCliToolKind.BundledPinget; - string executablePath = winget.Status.ExecutablePath; - string callArgs = winget.Status.ExecutableCallArgs; - - // pinget download writes only the installer; the YAML manifests are produced - // only by the native winget CLI. When pinget is selected, try to swap in the - // system winget for this operation so we get the full manifest folder. - if (usePinget) - { - var (found, systemWinGetPath) = CoreTools.Which("winget.exe"); - if (found) - { - Logger.Info( - $"WinGetManifestDownloadOperation: pinget is the configured CLI but only the installer would be downloaded; " - + $"using system winget at {systemWinGetPath} so YAML manifests are also written." - ); - executablePath = systemWinGetPath; - callArgs = ""; - usePinget = false; - } - else - { - Logger.Warn( - "WinGetManifestDownloadOperation: pinget is the configured CLI and system winget was not found on PATH. " - + "Only the installer file will be downloaded; manifest YAML files will not be produced." - ); - } - } List args = ["download"]; @@ -119,8 +91,9 @@ protected override void PrepareProcessStartInfo() } } - process.StartInfo.FileName = executablePath; - process.StartInfo.Arguments = (callArgs + " " + string.Join(" ", args)).TrimStart(); + process.StartInfo.FileName = winget.Status.ExecutablePath; + process.StartInfo.Arguments = + winget.Status.ExecutableCallArgs + " " + string.Join(" ", args); } protected override Task GetProcessVeredict( From 712d98a06b519361d2e643be0f08f78c98f73969 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Tue, 2 Jun 2026 08:48:07 -0400 Subject: [PATCH 3/3] Use pinget 0.8.1 Picks up the pre-indexed search fix (non-correlated tag/command subqueries that previously hung the WinGet source under Turso, leaving Discover empty) and the resilient source-index replace. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj | 2 +- .../UniGetUI.PackageEngine.Managers.WinGet.csproj | 2 +- src/UniGetUI/UniGetUI.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index 1f6da35cc0..5b5ca0a8bc 100644 --- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj +++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj @@ -94,7 +94,7 @@ - + diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/UniGetUI.PackageEngine.Managers.WinGet.csproj b/src/UniGetUI.PackageEngine.Managers.WinGet/UniGetUI.PackageEngine.Managers.WinGet.csproj index 07c211a81b..d394ea4d22 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/UniGetUI.PackageEngine.Managers.WinGet.csproj +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/UniGetUI.PackageEngine.Managers.WinGet.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/UniGetUI/UniGetUI.csproj b/src/UniGetUI/UniGetUI.csproj index 618d2bee53..1e4b25d8ce 100644 --- a/src/UniGetUI/UniGetUI.csproj +++ b/src/UniGetUI/UniGetUI.csproj @@ -221,7 +221,7 @@ - +