From d9480e092229bd304631c38a73a514d389449103 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Wed, 13 May 2026 14:13:03 -0400 Subject: [PATCH] Update WinGet manager for pinget 0.4 --- .../UniGetUI.Avalonia.csproj | 2 +- .../ClientHelpers/PingetCliHelper.cs | 18 +- .../PingetPackageDetailsProvider.cs | 1 - .../Helpers/WinGetPkgOperationHelper.cs | 53 ++++-- ...GetUI.PackageEngine.Managers.WinGet.csproj | 2 +- .../WinGet.cs | 5 +- .../WinGetManagerTests.cs | 164 +++++++++++++++++- src/UniGetUI/UniGetUI.csproj | 2 +- 8 files changed, 214 insertions(+), 33 deletions(-) diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index cf86490fb..f9341b591 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/ClientHelpers/PingetCliHelper.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/PingetCliHelper.cs index 7166f1e92..dd87579b7 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/PingetCliHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/PingetCliHelper.cs @@ -18,6 +18,7 @@ internal sealed partial class PingetCliHelper : IWinGetManagerHelper private static readonly JsonSerializerOptions SerializationOptions = new() { PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, }; private static readonly PingetCliJsonContext SerializationContext = new(SerializationOptions); @@ -77,7 +78,7 @@ public IReadOnlyList GetInstalledPackages_UnSafe() { ListResponse result = RunJson( LoggableTaskType.ListInstalledPackages, - "list --accept-source-agreements --output json" + "list --output json" ); return result @@ -97,7 +98,7 @@ public IReadOnlyList FindPackages_UnSafe(string query) { SearchResponse result = RunJson( LoggableTaskType.FindPackages, - $"search {Quote(query)} --accept-source-agreements --output json" + $"search {Quote(query)} --output json" ); return result @@ -115,9 +116,12 @@ public IReadOnlyList FindPackages_UnSafe(string query) public IReadOnlyList GetSources_UnSafe() { + // pinget 0.4.1 dropped JSON support for `source list`; `source export` is the + // equivalent JSON-emitting command (PascalCase keys, but case-insensitive matching + // in SerializationOptions binds them to our records). PingetSourcesResponse result = RunJson( LoggableTaskType.ListSources, - "source list --output json" + "source export --output json" ); return result @@ -133,7 +137,7 @@ public IReadOnlyList GetInstallableVersions_Unsafe(IPackage package) { VersionsResult result = RunJson( LoggableTaskType.LoadPackageVersions, - $"show {WinGetPkgOperationHelper.GetIdNamePiece(package)} --versions --accept-source-agreements --output json" + $"show {WinGetPkgOperationHelper.GetIdNamePiece(package)} --versions --output json" ); return result @@ -203,9 +207,11 @@ private T RunJson(LoggableTaskType taskType, string arguments) } process.Start(); - string output = process.StandardOutput.ReadToEnd(); - string error = process.StandardError.ReadToEnd(); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); process.WaitForExit(); + string output = stdoutTask.GetAwaiter().GetResult(); + string error = stderrTask.GetAwaiter().GetResult(); logger.AddToStdOut(output.Split(Environment.NewLine)); logger.AddToStdErr(error.Split(Environment.NewLine)); diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/PingetPackageDetailsProvider.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/PingetPackageDetailsProvider.cs index 4fde3193e..4e225c639 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/PingetPackageDetailsProvider.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/PingetPackageDetailsProvider.cs @@ -142,7 +142,6 @@ private static IReadOnlyList BuildShowArguments(PackageQuery query, stri arguments.Add(query.Version); } - arguments.Add("--accept-source-agreements"); arguments.Add("--output"); arguments.Add("json"); return arguments; diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgOperationHelper.cs index 7e0a04533..3e8bc3dd3 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgOperationHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgOperationHelper.cs @@ -35,6 +35,12 @@ protected override IReadOnlyList _getOperationParameters( OperationType operation ) { + // Pinget 0.4.x does not accept --accept-source-agreements, --disable-interactivity, + // or --proxy on any verb; --accept-package-agreements, --force, --location, and + // --interactive are accepted on install/uninstall but rejected on upgrade. + bool usePinget = + ((WinGet)Manager).SelectedCliToolKind == WinGetCliToolKind.BundledPinget; + List parameters = [ operation switch @@ -51,7 +57,10 @@ OperationType operation { parameters.AddRange(["--source", package.Source.Name]); } - parameters.AddRange(["--accept-source-agreements", "--disable-interactivity"]); + if (!usePinget) + { + parameters.AddRange(["--accept-source-agreements", "--disable-interactivity"]); + } // package.OverridenInstallationOptions.Scope is meaningless in WinGet packages. Default is unspecified, hence the _ => []. parameters.AddRange( @@ -76,7 +85,15 @@ operation is OperationType.Uninstall parameters.AddRange(["--version", $"\"{options.Version}\""]); } - parameters.Add(options.InteractiveInstallation ? "--interactive" : "--silent"); + if (usePinget && operation is OperationType.Update) + { + // pinget upgrade only supports --silent (no --interactive). + parameters.Add("--silent"); + } + else + { + parameters.Add(options.InteractiveInstallation ? "--interactive" : "--silent"); + } if (operation is OperationType.Update) { @@ -90,16 +107,19 @@ operation is OperationType.Uninstall } parameters.Add("--include-unknown"); - if (options.CustomInstallLocation != "") + if (!usePinget) { - if (Settings.Get(Settings.K.WinGetForceLocationOnUpdate)) - parameters.AddRange(["--location", $"\"{options.CustomInstallLocation}\""]); - } - else - { - var detectedLocation = TryGetPortableInstallLocation(package); - if (detectedLocation is not null) - parameters.AddRange(["--location", $"\"{detectedLocation}\""]); + if (options.CustomInstallLocation != "") + { + if (Settings.Get(Settings.K.WinGetForceLocationOnUpdate)) + parameters.AddRange(["--location", $"\"{options.CustomInstallLocation}\""]); + } + else + { + var detectedLocation = TryGetPortableInstallLocation(package); + if (detectedLocation is not null) + parameters.AddRange(["--location", $"\"{detectedLocation}\""]); + } } } else if (operation is OperationType.Install) @@ -110,7 +130,11 @@ operation is OperationType.Uninstall if (operation is not OperationType.Uninstall) { - parameters.AddRange(["--accept-package-agreements", "--force"]); + // pinget upgrade does not accept --accept-package-agreements or --force. + if (!(usePinget && operation is OperationType.Update)) + { + parameters.AddRange(["--accept-package-agreements", "--force"]); + } if (options.SkipHashCheck) parameters.Add("--ignore-security-hash"); @@ -189,7 +213,10 @@ or ElevationRequirement.ElevatesSelf Logger.Error(ex); } - parameters.Add(WinGet.GetProxyArgument()); + if (!usePinget) + { + parameters.Add(WinGet.GetProxyArgument()); + } parameters.AddRange( operation switch 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 9b9876968..f89198cdf 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.PackageEngine.Managers.WinGet/WinGet.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs index 1f7205ef8..71980979e 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs @@ -725,7 +725,10 @@ public override void RefreshPackageIndexes() FileName = Status.ExecutablePath, Arguments = Status.ExecutableCallArgs - + " source update --disable-interactivity " + + " source update" + + (SelectedCliToolKind == WinGetCliToolKind.SystemWinGet + ? " --disable-interactivity " + : " ") + GetCliToolProxyArgument(), UseShellExecute = false, RedirectStandardOutput = true, diff --git a/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs index ab3a12f28..aeafbbe82 100644 --- a/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs @@ -3,10 +3,12 @@ using UniGetUI.Core.Data; using UniGetUI.Core.SettingsEngine; using UniGetUI.PackageEngine.Classes.Manager; +using UniGetUI.PackageEngine.Enums; using UniGetUI.PackageEngine.Interfaces; using UniGetUI.PackageEngine.Managers.WingetManager; using UniGetUI.PackageEngine.ManagerClasses.Classes; using UniGetUI.PackageEngine.PackageClasses; +using UniGetUI.PackageEngine.Serializable; using UniGetUI.PackageEngine.Tests.Infrastructure.Assertions; using UniGetUI.PackageEngine.Tests.Infrastructure.Builders; @@ -298,23 +300,24 @@ public void FindCandidateExecutableFilesReturnsEmptyWhenNoCliToolExists() [Fact] public void PingetCliHelperDeserializesListResponsesWithGeneratedContext() { + // pinget 0.4.1+ emits snake_case keys. const string json = """ { "matches": [ { "name": "Contoso Tool", "id": "Contoso.Tool", - "localId": null, - "installedVersion": "1.2.3", - "availableVersion": "2.0.0", - "sourceName": "winget", + "local_id": null, + "installed_version": "1.2.3", + "available_version": "2.0.0", + "source_name": "winget", "publisher": null, "scope": null, - "installerCategory": null, - "installLocation": null, - "packageFamilyNames": [], - "productCodes": [], - "upgradeCodes": [] + "installer_category": null, + "install_location": null, + "package_family_names": [], + "product_codes": [], + "upgrade_codes": [] } ], "warnings": [], @@ -708,6 +711,149 @@ public void WinGetCliHelperUsesPingetProviderForPackageDetails() ); } + [Theory] + [InlineData(0)] // OperationType.Install + [InlineData(1)] // OperationType.Update + [InlineData(2)] // OperationType.Uninstall + public void WinGetOperationHelperEmitsWinGetCompatibleFlagsForSystemCli(int operationType) + { + var manager = new WinGet(); + SetCliToolKind(manager, WinGetCliToolKind.SystemWinGet); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("Contoso.Tool") + .WithVersion("1.0.0") + .WithNewVersion("2.0.0") + .Build(); + + var parameters = manager.OperationHelper.GetParameters( + package, + new InstallOptions(), + (OperationType)operationType + ); + + Assert.Contains("--accept-source-agreements", parameters); + Assert.Contains("--disable-interactivity", parameters); + } + + [Fact] + public void WinGetOperationHelperSkipsUnsupportedFlagsForPingetInstall() + { + var manager = new WinGet(); + SetCliToolKind(manager, WinGetCliToolKind.BundledPinget); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("Spotify.Spotify") + .Build(); + + var parameters = manager.OperationHelper.GetParameters( + package, + new InstallOptions(), + OperationType.Install + ); + + Assert.DoesNotContain("--accept-source-agreements", parameters); + Assert.DoesNotContain("--disable-interactivity", parameters); + // pinget install does accept these. + Assert.Contains("--accept-package-agreements", parameters); + Assert.Contains("--force", parameters); + Assert.Contains("--silent", parameters); + } + + [Fact] + public void WinGetOperationHelperSkipsUnsupportedFlagsForPingetUpgrade() + { + var manager = new WinGet(); + SetCliToolKind(manager, WinGetCliToolKind.BundledPinget); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("Contoso.Tool") + .WithVersion("1.0.0") + .WithNewVersion("2.0.0") + .Build(); + var options = new InstallOptions + { + InteractiveInstallation = true, + CustomInstallLocation = @"C:\Apps\Contoso", + }; + + var parameters = manager.OperationHelper.GetParameters( + package, + options, + OperationType.Update + ); + + Assert.DoesNotContain("--accept-source-agreements", parameters); + Assert.DoesNotContain("--disable-interactivity", parameters); + // pinget upgrade does NOT accept these even though winget upgrade does. + Assert.DoesNotContain("--accept-package-agreements", parameters); + Assert.DoesNotContain("--force", parameters); + Assert.DoesNotContain("--interactive", parameters); + Assert.DoesNotContain("--location", parameters); + // pinget upgrade still supports --include-unknown and --silent. + Assert.Contains("--include-unknown", parameters); + Assert.Contains("--silent", parameters); + } + + [Fact] + public void WinGetOperationHelperSkipsUnsupportedFlagsForPingetUninstall() + { + var manager = new WinGet(); + SetCliToolKind(manager, WinGetCliToolKind.BundledPinget); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("Contoso.Tool") + .WithVersion("1.0.0") + .Build(); + + var parameters = manager.OperationHelper.GetParameters( + package, + new InstallOptions(), + OperationType.Uninstall + ); + + Assert.DoesNotContain("--accept-source-agreements", parameters); + Assert.DoesNotContain("--disable-interactivity", parameters); + } + + [Fact] + public void WinGetOperationHelperOmitsProxyArgumentForPinget() + { + Settings.Set(Settings.K.EnableProxy, true); + Settings.Set(Settings.K.EnableProxyAuth, false); + Settings.SetValue(Settings.K.ProxyURL, "http://proxy.example.test:3128/"); + try + { + var manager = new WinGet(); + SetCliToolKind(manager, WinGetCliToolKind.BundledPinget); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("Contoso.Tool") + .Build(); + + var parameters = manager.OperationHelper.GetParameters( + package, + new InstallOptions(), + OperationType.Install + ); + + Assert.DoesNotContain(parameters, p => p.StartsWith("--proxy", StringComparison.Ordinal)); + } + finally + { + Settings.Set(Settings.K.EnableProxy, false); + Settings.SetValue(Settings.K.ProxyURL, ""); + } + } + + private static void SetCliToolKind(WinGet manager, WinGetCliToolKind kind) + { + typeof(WinGet) + .GetProperty(nameof(WinGet.SelectedCliToolKind), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)! + .GetSetMethod(nonPublic: true)! + .Invoke(manager, [kind]); + } + private sealed class TestableWinGet : WinGet { public IReadOnlyList InvokeGetInstalledPackages() => base.GetInstalledPackages_UnSafe(); diff --git a/src/UniGetUI/UniGetUI.csproj b/src/UniGetUI/UniGetUI.csproj index 79bd880ea..7396fb302 100644 --- a/src/UniGetUI/UniGetUI.csproj +++ b/src/UniGetUI/UniGetUI.csproj @@ -220,7 +220,7 @@ - +