From 579ba0dc5c9cf4098dab997fff93ba00647bf9d2 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Wed, 1 Apr 2026 11:12:15 -0400 Subject: [PATCH 1/7] Fixed pip --- .../OverridenInstallationOptions.cs | 1 + .../Helpers/PipPkgOperationHelper.cs | 9 + .../Pip.cs | 214 +++++++++++------- 3 files changed, 138 insertions(+), 86 deletions(-) diff --git a/src/UniGetUI.PackageEngine.Enums/OverridenInstallationOptions.cs b/src/UniGetUI.PackageEngine.Enums/OverridenInstallationOptions.cs index 90fb8ebd7..c8b79a201 100644 --- a/src/UniGetUI.PackageEngine.Enums/OverridenInstallationOptions.cs +++ b/src/UniGetUI.PackageEngine.Enums/OverridenInstallationOptions.cs @@ -6,6 +6,7 @@ public struct OverridenInstallationOptions public bool? RunAsAdministrator; public bool PowerShell_DoNotSetScopeParameter = false; public bool? WinGet_SpecifyVersion = null; + public bool Pip_BreakSystemPackages = false; public OverridenInstallationOptions(string? scope = null, bool? runAsAdministrator = null) { diff --git a/src/UniGetUI.PackageEngine.Managers.Pip/Helpers/PipPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.Pip/Helpers/PipPkgOperationHelper.cs index 9aa674177..5e8c44585 100644 --- a/src/UniGetUI.PackageEngine.Managers.Pip/Helpers/PipPkgOperationHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.Pip/Helpers/PipPkgOperationHelper.cs @@ -52,6 +52,9 @@ package.OverridenOptions.Scope is null parameters.Add("--user"); } + if (package.OverridenOptions.Pip_BreakSystemPackages) + parameters.Add("--break-system-packages"); + parameters.Add(Pip.GetProxyArgument()); parameters.AddRange( @@ -80,6 +83,12 @@ int returnCode string output_string = string.Join("\n", processOutput); + if (output_string.Contains("externally-managed-environment") && !package.OverridenOptions.Pip_BreakSystemPackages) + { + package.OverridenOptions.Pip_BreakSystemPackages = true; + return OperationVeredict.AutoRetry; + } + if (output_string.Contains("--user") && package.OverridenOptions.Scope != PackageScope.User) { package.OverridenOptions.Scope = PackageScope.User; diff --git a/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs b/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs index 8b7a151e9..b6ea05546 100644 --- a/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs +++ b/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs @@ -1,5 +1,9 @@ using System.Diagnostics; +using System.Net.Http; +using System.Text.Json.Nodes; using System.Text.RegularExpressions; +using UniGetUI.Core.Data; +using UniGetUI.Core.Logging; using UniGetUI.Core.SettingsEngine; using UniGetUI.Core.Tools; using UniGetUI.Interface.Enums; @@ -101,111 +105,132 @@ public static string GetProxyArgument() + $"@{proxyUri.AbsoluteUri.Replace($"{proxyUri.Scheme}://", "")}"; } + // In-memory cache of all PyPI package names, shared across searches + private static string[]? _cachedNames; + private static DateTime _cacheTimestamp = DateTime.MinValue; + private static readonly object _cacheLock = new(); + private const int CacheMaxAgeHours = 24; + private const int MaxSearchResults = 20; + protected override IReadOnlyList FindPackages_UnSafe(string query) { - List Packages = []; + INativeTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages); - var (found, path) = CoreTools.Which("parse_pip_search.exe"); - if (!found) - { - Process proc = new() - { - StartInfo = new ProcessStartInfo - { - FileName = Status.ExecutablePath, - Arguments = - Status.ExecutableCallArgs - + " install parse_pip_search " - + GetProxyArgument(), - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - }, - }; - IProcessTaskLogger aux_logger = TaskLogger.CreateNew( - LoggableTaskType.InstallManagerDependency, - proc - ); - proc.Start(); + string[] allNames = GetOrRefreshIndex(logger); - aux_logger.AddToStdOut(proc.StandardOutput.ReadToEnd()); - aux_logger.AddToStdErr(proc.StandardError.ReadToEnd()); + string queryLower = query.ToLowerInvariant(); + string[] matches = allNames + .Where(n => n.Contains(queryLower, StringComparison.OrdinalIgnoreCase)) + .OrderBy(n => n.StartsWith(queryLower, StringComparison.OrdinalIgnoreCase) ? 0 : 1) + .ThenBy(n => n.Length) + .Take(MaxSearchResults) + .ToArray(); - proc.WaitForExit(); - aux_logger.Close(proc.ExitCode); - path = "parse_pip_search.exe"; + logger.Log($"Matched {matches.Length} packages for query '{query}'"); + + // Fetch latest version for each match in parallel + var versionTasks = matches + .Select(name => Task.Run(() => FetchLatestVersion(name))) + .ToArray(); + Task.WhenAll(versionTasks).GetAwaiter().GetResult(); + + List packages = []; + for (int i = 0; i < matches.Length; i++) + { + string version = versionTasks[i].Result ?? "latest"; + packages.Add(new Package( + CoreTools.FormatAsName(matches[i]), + matches[i], + version, + DefaultSource, + this, + new(PackageScope.Global) + )); } - using Process p = new() + logger.Close(0); + return packages; + } + + private static string[] GetOrRefreshIndex(INativeTaskLogger logger) + { + lock (_cacheLock) { - StartInfo = new ProcessStartInfo - { - FileName = path, - Arguments = "\"" + query + "\" " + GetProxyArgument(), - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - StandardOutputEncoding = System.Text.Encoding.UTF8, - }, - }; + if (_cachedNames is not null && (DateTime.Now - _cacheTimestamp).TotalHours < CacheMaxAgeHours) + return _cachedNames; + } - p.StartInfo = CoreTools.UpdateEnvironmentVariables(p.StartInfo); - IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages, p); - p.Start(); + string cacheFile = Path.Join(CoreData.UniGetUICacheDirectory_Data, "pip_simple_index.cache"); - string? line; - bool DashesPassed = false; - while ((line = p.StandardOutput.ReadLine()) is not null) + // Use file cache if fresh enough + if (File.Exists(cacheFile) && (DateTime.Now - File.GetLastWriteTime(cacheFile)).TotalHours < CacheMaxAgeHours) { - logger.AddToStdOut(line); - if (!DashesPassed) + logger.Log($"Loading PyPI index from file cache ({File.GetLastWriteTime(cacheFile):g})"); + string[] cached = File.ReadAllLines(cacheFile); + if (cached.Length > 0) { - if (line.Contains("----")) - { - DashesPassed = true; - } + lock (_cacheLock) { _cachedNames = cached; _cacheTimestamp = File.GetLastWriteTime(cacheFile); } + return cached; } - else - { - string[] elements = line.Split('|'); - if (elements.Length < 2) - { - continue; - } + logger.Error("PyPI index file cache was empty, re-downloading..."); + } - for (int i = 0; i < elements.Length; i++) - { - elements[i] = elements[i].Trim(); - } + // Download fresh index + logger.Log("Downloading PyPI simple index (one-time ~38 MB download, cached for 24 h)..."); + using HttpClient client = new(CoreTools.GenericHttpClientParameters); + client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); + client.DefaultRequestHeaders.Add("Accept", "application/vnd.pypi.simple.v1+json"); - if ( - FALSE_PACKAGE_IDS.Contains(elements[0]) - || FALSE_PACKAGE_VERSIONS.Contains(elements[1]) - ) - { - continue; - } + HttpResponseMessage response = client.GetAsync("https://pypi.org/simple/").GetAwaiter().GetResult(); + if (!response.IsSuccessStatusCode) + throw new HttpRequestException($"PyPI simple index returned {(int)response.StatusCode} {response.ReasonPhrase}"); - Packages.Add( - new Package( - CoreTools.FormatAsName(elements[0]), - elements[0], - elements[1], - DefaultSource, - this, - new(PackageScope.Global) - ) - ); - } + string json = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + var projects = (JsonNode.Parse(json) as JsonObject)?["projects"] as JsonArray; + string[] names = projects? + .Select(p => p?["name"]?.GetValue()) + .Where(n => !string.IsNullOrEmpty(n)) + .Select(n => n!) + .ToArray() ?? []; + + if (names.Length == 0) + throw new InvalidDataException("PyPI simple index returned 0 packages — response may be malformed"); + + logger.Log($"Downloaded {names.Length} package names from PyPI"); + + // Update memory cache before attempting file write so searches work even if file write fails + lock (_cacheLock) { _cachedNames = names; _cacheTimestamp = DateTime.Now; } + + try + { + Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)!); + File.WriteAllLines(cacheFile, names); + } + catch (Exception e) + { + logger.Error($"Could not write PyPI index file cache to {cacheFile}: {e.Message}"); } - logger.AddToStdErr(p.StandardError.ReadToEnd()); - p.WaitForExit(); - logger.Close(p.ExitCode); + return names; + } - return Packages; + private static string? FetchLatestVersion(string packageName) + { + try + { + using HttpClient client = new(CoreTools.GenericHttpClientParameters); + client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); + string json = client + .GetStringAsync($"https://pypi.org/pypi/{Uri.EscapeDataString(packageName)}/json") + .GetAwaiter() + .GetResult(); + return (JsonNode.Parse(json) as JsonObject)?["info"]?["version"]?.GetValue(); + } + catch + { + return null; + } } protected override IReadOnlyList GetAvailableUpdates_UnSafe() @@ -429,6 +454,7 @@ protected override void _loadManagerVersion(out string version) }; process.Start(); version = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(); if (process.ExitCode is 9009) { @@ -445,6 +471,22 @@ protected override void _performExtraLoadingSteps() "false", EnvironmentVariableTarget.Process ); + + // Pre-warm the package name index in the background so the first search doesn't + // need to wait for the ~38 MB download inside the search timeout window. + Task.Run(() => + { + try + { + var logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages); + GetOrRefreshIndex(logger); + logger.Close(0); + } + catch (Exception e) + { + Logger.Warn($"Pip: background index pre-warm failed: {e.Message}"); + } + }); } } } From 2c15ee02839810e17a84a414d86eb393510ee385 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Wed, 1 Apr 2026 11:48:05 -0400 Subject: [PATCH 2/7] fixed powershell7 --- .../BaseNuGet.cs | 37 +++-- .../Helpers/PowerShell7SourceHelper.cs | 6 +- .../PowerShell7.cs | 127 +++++++++--------- 3 files changed, 96 insertions(+), 74 deletions(-) diff --git a/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs b/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs index e17b58803..e8a65a838 100644 --- a/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs +++ b/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs @@ -14,6 +14,13 @@ namespace UniGetUI.PackageEngine.Managers.PowerShellManager { public abstract class BaseNuGet : PackageManager { + /// + /// When true, searches use Packages()?$filter=substringof(query,Id) which searches by + /// package name only but returns reliable results (e.g. PSGallery's Search() endpoint + /// silently omits some packages). When false, the standard Search() endpoint is used + /// which supports full-text search across name, description, and tags. + /// + protected virtual bool UseSubstringSearch => false; public static Dictionary Manifests = new(); public sealed override void Initialize() @@ -71,17 +78,25 @@ protected sealed override IReadOnlyList FindPackages_UnSafe(string quer { try { - Uri? SearchUrl = new( - $"{source.Url}/Search()" - + $"?$filter=IsLatestVersion" - + $"&$orderby=Id&searchTerm='{HttpUtility.UrlEncode(query)}'" - + $"&targetFramework=''" - + $"&includePrerelease={(canPrerelease ? "true" : "false")}" - + $"&$skip=0" - + $"&$top=50" - + $"&semVerLevel=2.0.0" - ); - // Uri SearchUrl = new($"{source.Url}/Search()?$filter=IsLatestVersion&searchTerm=%27{HttpUtility.UrlEncode(query)}%27&targetFramework=%27%27&includePrerelease=false"); + string versionFilter = canPrerelease ? "IsAbsoluteLatestVersion eq true" : "IsLatestVersion eq true"; + Uri? SearchUrl = UseSubstringSearch + ? new Uri( + $"{source.Url}/Packages()" + + $"?$filter=substringof('{HttpUtility.UrlEncode(query)}',Id) and {versionFilter}" + + $"&$orderby=DownloadCount desc" + + $"&$skip=0" + + $"&$top=50" + ) + : new Uri( + $"{source.Url}/Search()" + + $"?$filter=IsLatestVersion" + + $"&$orderby=Id&searchTerm='{HttpUtility.UrlEncode(query)}'" + + $"&targetFramework=''" + + $"&includePrerelease={(canPrerelease ? "true" : "false")}" + + $"&$skip=0" + + $"&$top=50" + + $"&semVerLevel=2.0.0" + ); logger.Log($"Begin package search with url={SearchUrl} on manager {Name}"); Dictionary AlreadyProcessedPackages = []; diff --git a/src/UniGetUI.PackageEngine.Managers.PowerShell7/Helpers/PowerShell7SourceHelper.cs b/src/UniGetUI.PackageEngine.Managers.PowerShell7/Helpers/PowerShell7SourceHelper.cs index af36cab2f..2777d28b6 100644 --- a/src/UniGetUI.PackageEngine.Managers.PowerShell7/Helpers/PowerShell7SourceHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.PowerShell7/Helpers/PowerShell7SourceHelper.cs @@ -65,8 +65,10 @@ protected override IReadOnlyList GetSources_UnSafe() FileName = Manager.Status.ExecutablePath, Arguments = Manager.Status.ExecutableCallArgs - + " \"Get-PSRepository | Format-Table -Property " - + "Name,@{N='SourceLocation';E={If ($_.Uri) {$_.Uri.AbsoluteUri} Else {$_.SourceLocation}}}\"", + + " \"if (Get-Command Get-PSResourceRepository -ErrorAction SilentlyContinue)" + + " { Get-PSResourceRepository | Format-Table -Property Name,Uri }" + + " else { Get-PSRepository | Format-Table -Property" + + " Name,@{N='SourceLocation';E={If ($_.Uri) {$_.Uri.AbsoluteUri} Else {$_.SourceLocation}}} }\"", RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true, diff --git a/src/UniGetUI.PackageEngine.Managers.PowerShell7/PowerShell7.cs b/src/UniGetUI.PackageEngine.Managers.PowerShell7/PowerShell7.cs index b66450a4b..fbd91163f 100644 --- a/src/UniGetUI.PackageEngine.Managers.PowerShell7/PowerShell7.cs +++ b/src/UniGetUI.PackageEngine.Managers.PowerShell7/PowerShell7.cs @@ -80,77 +80,82 @@ public PowerShell7() protected override IReadOnlyList _getInstalledPackages_UnSafe() { List Packages = []; - foreach (var env in new[] { "AllUsers", "CurrentUser" }) + + using Process p = new() { - using Process p = new() + StartInfo = new ProcessStartInfo { - StartInfo = new ProcessStartInfo - { - FileName = Status.ExecutablePath, - Arguments = - Status.ExecutableCallArgs - + $" \"Get-InstalledPSResource -Scope {env} | Format-Table -Property Name,Version,Repository\"", - RedirectStandardOutput = true, - RedirectStandardError = true, - RedirectStandardInput = true, - UseShellExecute = false, - CreateNoWindow = true, - StandardOutputEncoding = Encoding.UTF8, - }, - }; - - IProcessTaskLogger logger = TaskLogger.CreateNew( - LoggableTaskType.ListInstalledPackages, - p - ); - - p.Start(); - string? line; - bool DashesPassed = false; - while ((line = p.StandardOutput.ReadLine()) is not null) + FileName = Status.ExecutablePath, + Arguments = + Status.ExecutableCallArgs + + " \"Write-Output '##SCOPE:AllUsers##';" + + " Get-InstalledPSResource -Scope AllUsers | Format-Table -Property Name,Version,Repository;" + + " Write-Output '##SCOPE:CurrentUser##';" + + " Get-InstalledPSResource -Scope CurrentUser | Format-Table -Property Name,Version,Repository\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + UseShellExecute = false, + CreateNoWindow = true, + StandardOutputEncoding = Encoding.UTF8, + }, + }; + + IProcessTaskLogger logger = TaskLogger.CreateNew( + LoggableTaskType.ListInstalledPackages, + p + ); + + p.Start(); + string? line; + bool DashesPassed = false; + string currentScope = "AllUsers"; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + logger.AddToStdOut(line); + if (line.StartsWith("##SCOPE:")) + { + currentScope = line.Trim('#').Split(':')[1]; + DashesPassed = false; + continue; + } + + if (!DashesPassed) { - logger.AddToStdOut(line); - if (!DashesPassed) - { - if (line.Contains("-----")) - { - DashesPassed = true; - } - } - else - { - string[] elements = Regex.Replace(line, " {2,}", " ").Split(' '); - if (elements.Length < 3) - { - continue; - } - - for (int i = 0; i < elements.Length; i++) - { - elements[i] = elements[i].Trim(); - } - - Packages.Add( - new Package( - CoreTools.FormatAsName(elements[0]), - elements[0], - elements[1], - SourcesHelper.Factory.GetSourceOrDefault(elements[2]), - this, - new(env == "CurrentUser" ? PackageScope.User : PackageScope.Machine) - ) - ); - } + if (line.Contains("-----")) + DashesPassed = true; } + else + { + string[] elements = Regex.Replace(line, " {2,}", " ").Split(' '); + if (elements.Length < 3) + continue; + + for (int i = 0; i < elements.Length; i++) + elements[i] = elements[i].Trim(); - logger.AddToStdErr(p.StandardError.ReadToEnd()); - p.WaitForExit(); - logger.Close(p.ExitCode); + Packages.Add( + new Package( + CoreTools.FormatAsName(elements[0]), + elements[0], + elements[1], + SourcesHelper.Factory.GetSourceOrDefault(elements[2]), + this, + new(currentScope == "CurrentUser" ? PackageScope.User : PackageScope.Machine) + ) + ); + } } + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + return Packages; } + protected override bool UseSubstringSearch => true; + public override IReadOnlyList FindCandidateExecutableFiles() => CoreTools.WhichMultiple(OperatingSystem.IsWindows() ? "pwsh.exe" : "pwsh"); From 2972fd0c74c13e4ba526956830f373d01edc2f69 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Wed, 1 Apr 2026 13:37:03 -0400 Subject: [PATCH 3/7] fixed cargo --- .../Cargo.cs | 15 ++++++----- .../Helpers/CargoPkgOperationHelper.cs | 25 +++++++++++++------ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs b/src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs index d898794b5..4360c8d50 100644 --- a/src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs +++ b/src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs @@ -18,7 +18,7 @@ namespace UniGetUI.PackageEngine.Managers.CargoManager; public partial class Cargo : PackageManager { - [GeneratedRegex(@"(\w+)\s=\s""(\d+\.\d+\.\d+)""\s*#\s(.*)")] + [GeneratedRegex(@"([\w-]+)\s=\s""(\d+\.\d+\.\d+)""\s*#\s(.*)")] private static partial Regex SearchLineRegex(); [GeneratedRegex(@"(.+)v(\d+\.\d+\.\d+)\s*v(\d+\.\d+\.\d+)\s*(Yes|No)")] @@ -126,19 +126,19 @@ protected override IReadOnlyList FindPackages_UnSafe(string query) package.VersionString ); if (versionInfo.bin_names?.Length > 0) - { BinPackages.Add(package); - } } catch (Exception ex) { - logger.AddToStdErr($"{ex.Message}"); + // On API failure, include the package rather than silently drop it + logger.AddToStdErr($"bin_names check failed for {package.Id}: {ex.Message}"); + BinPackages.Add(package); } if (i + 1 == Packages.Count) break; - // Crates.io api requests that we send no more than one request per second - Task.Delay(Math.Max(0, 1000 - (int)((DateTime.Now - startTime).TotalMilliseconds))) + // Crates.io requires no more than one request per second + Task.Delay(Math.Max(0, 1000 - (int)(DateTime.Now - startTime).TotalMilliseconds)) .GetAwaiter() .GetResult(); } @@ -158,6 +158,9 @@ protected override IReadOnlyList GetInstalledPackages_UnSafe() return GetPackages(LoggableTaskType.ListInstalledPackages); } + public readonly bool HasBinstall = + CoreTools.Which(OperatingSystem.IsWindows() ? "cargo-binstall.exe" : "cargo-binstall").Item1; + public override IReadOnlyList FindCandidateExecutableFiles() => CoreTools.WhichMultiple(OperatingSystem.IsWindows() ? "cargo.exe" : "cargo"); diff --git a/src/UniGetUI.PackageEngine.Managers.Cargo/Helpers/CargoPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.Cargo/Helpers/CargoPkgOperationHelper.cs index 0db601a49..fee54789d 100644 --- a/src/UniGetUI.PackageEngine.Managers.Cargo/Helpers/CargoPkgOperationHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.Cargo/Helpers/CargoPkgOperationHelper.cs @@ -15,15 +15,23 @@ OperationType operation { var installVersion = options.Version == string.Empty ? package.VersionString : options.Version; + bool hasBinstall = ((Cargo)Manager).HasBinstall; + List parameters; switch (operation) { case OperationType.Install: - parameters = [Manager.Properties.InstallVerb, "--version", installVersion, package.Id]; + if (hasBinstall) + parameters = [Manager.Properties.InstallVerb, "--version", installVersion, package.Id]; + else + parameters = ["install", package.Id, "--version", installVersion]; break; case OperationType.Update: - parameters = [Manager.Properties.UpdateVerb, package.Id]; + if (hasBinstall) + parameters = [Manager.Properties.UpdateVerb, package.Id]; + else + parameters = ["install", package.Id]; break; case OperationType.Uninstall: @@ -36,13 +44,16 @@ OperationType operation if (operation is OperationType.Install or OperationType.Update) { - parameters.Add("--no-confirm"); + if (hasBinstall) + { + parameters.Add("--no-confirm"); - if (options.SkipHashCheck) - parameters.Add("--skip-signatures"); + if (options.SkipHashCheck) + parameters.Add("--skip-signatures"); - if (options.CustomInstallLocation != "") - parameters.AddRange(["--install-path", options.CustomInstallLocation]); + if (options.CustomInstallLocation != "") + parameters.AddRange(["--install-path", options.CustomInstallLocation]); + } } parameters.AddRange( From 9e02ef32022c2f434a1772dd596bc3c01ede8911 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Wed, 1 Apr 2026 14:24:30 -0400 Subject: [PATCH 4/7] Fixed vcpkg --- .../Pages/SettingsPages/PackageManagerPage.axaml.cs | 7 ++++--- src/UniGetUI.PackageEngine.Managers.Vcpkg/Vcpkg.cs | 9 ++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs index d8da8197b..cbeaf8988 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs @@ -221,7 +221,7 @@ private void BuildExtraControls(CheckboxCard_Dict disableNotifsCard) { ExtraControls.Children.Clear(); var manager = ViewModel.Manager; - bool managerHasSources = manager.Capabilities.SupportsCustomSources && manager.Name != "Vcpkg"; + bool managerHasSources = manager.Capabilities.SupportsCustomSources && manager.Name != "vcpkg"; if (managerHasSources) { @@ -330,7 +330,7 @@ private void BuildExtraControls(CheckboxCard_Dict disableNotifsCard) ExtraControls.Children.Add(chocoSysChoco); break; - case "Vcpkg": + case "vcpkg": disableNotifsCard.CornerRadius = new CornerRadius(8, 8, 0, 0); disableNotifsCard.BorderThickness = new Thickness(1, 1, 1, 0); ExtraControls.Children.Add(disableNotifsCard); @@ -405,7 +405,8 @@ private ButtonCard BuildVcpkgRootCard() descPanel.Children.Add(openBtn); vcpkgRootCard.Description = descPanel; - vcpkgRootCard.Click += (_, _) => ViewModel.PickVcpkgRootCommand.Execute(vcpkgRootCard); + vcpkgRootCard.Command = ViewModel.PickVcpkgRootCommand; + vcpkgRootCard.CommandParameter = vcpkgRootCard; return vcpkgRootCard; } diff --git a/src/UniGetUI.PackageEngine.Managers.Vcpkg/Vcpkg.cs b/src/UniGetUI.PackageEngine.Managers.Vcpkg/Vcpkg.cs index ebc906b93..14f20a9aa 100644 --- a/src/UniGetUI.PackageEngine.Managers.Vcpkg/Vcpkg.cs +++ b/src/UniGetUI.PackageEngine.Managers.Vcpkg/Vcpkg.cs @@ -380,19 +380,22 @@ protected override void _loadManagerVersion(out string version) }; process.Start(); version = process.StandardOutput.ReadLine()?.Trim() ?? ""; - version += $"\n%VCPKG_ROOT% = {rootPath}"; + version += RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? $"\n%VCPKG_ROOT% = {rootPath}" + : $"\n$VCPKG_ROOT = {rootPath}"; } protected override void _performExtraLoadingSteps() { var (_, rootPath) = GetVcpkgRoot(); + bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); using Process p2 = new() { StartInfo = new ProcessStartInfo { - FileName = "cmd.exe", + FileName = isWindows ? "cmd.exe" : "sh", WorkingDirectory = rootPath, - Arguments = "/C .\\bootstrap-vcpkg.bat", + Arguments = isWindows ? "/C .\\bootstrap-vcpkg.bat" : "./bootstrap-vcpkg.sh", UseShellExecute = false, CreateNoWindow = true, }, From e7bd55ffc9b0b88763638086d5db9dd4adfd0112 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Thu, 2 Apr 2026 08:23:38 -0400 Subject: [PATCH 5/7] fix for linux --- .../Cargo.cs | 33 +++++++++++++++++-- .../Helpers/CargoPkgOperationHelper.cs | 7 +++- .../Pip.cs | 15 +++++++++ .../Helpers/VcpkgPkgOperationHelper.cs | 2 +- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs b/src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs index 4360c8d50..20807eb9e 100644 --- a/src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs +++ b/src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs @@ -24,6 +24,10 @@ public partial class Cargo : PackageManager [GeneratedRegex(@"(.+)v(\d+\.\d+\.\d+)\s*v(\d+\.\d+\.\d+)\s*(Yes|No)")] private static partial Regex UpdateLineRegex(); + // Matches "ripgrep v15.1.0:" lines from `cargo install --list` + [GeneratedRegex(@"^([\w-]+)\s+v(\d+\.\d+\.\d+):")] + private static partial Regex InstallListLineRegex(); + public Cargo() { string cargoCommand = OperatingSystem.IsWindows() ? "cargo.exe" : "cargo"; @@ -186,6 +190,9 @@ protected override void _loadManagerVersion(out string version) Logger.Error("cargo version error: " + error); } + public void InvalidateInstalledCache() => + TaskRecycler>.RemoveFromCache(GetInstalledCommandOutput); + private IReadOnlyList GetPackages(LoggableTaskType taskType) { List Packages = []; @@ -217,13 +224,35 @@ private List GetInstalledCommandOutput() logger.AddToStdOut(line); var match = UpdateLineRegex().Match(line); if (match.Success) - { output.Add(match); - } } logger.AddToStdErr(p.StandardError.ReadToEnd()); p.WaitForExit(); logger.Close(p.ExitCode); + + if (output.Count > 0) + return output; + + // Fallback: cargo-update is not installed, use the built-in `cargo install --list`. + // No latest-version info is available, so updates won't be detected, but the installed + // packages list will be populated correctly. + using Process fallback = GetProcess(Status.ExecutablePath, "install --list"); + IProcessTaskLogger fallbackLogger = TaskLogger.CreateNew(LoggableTaskType.OtherTask, fallback); + fallbackLogger.AddToStdOut("Falling back to `cargo install --list` (cargo-update not available)"); + fallback.Start(); + while ((line = fallback.StandardOutput.ReadLine()) is not null) + { + fallbackLogger.AddToStdOut(line); + var m = InstallListLineRegex().Match(line); + if (!m.Success) continue; + // Synthesise a match compatible with UpdateLineRegex (same installed and latest version → no update) + var fake = UpdateLineRegex().Match($"{m.Groups[1].Value} v{m.Groups[2].Value} v{m.Groups[2].Value} No"); + if (fake.Success) + output.Add(fake); + } + fallbackLogger.AddToStdErr(fallback.StandardError.ReadToEnd()); + fallback.WaitForExit(); + fallbackLogger.Close(fallback.ExitCode); return output; } diff --git a/src/UniGetUI.PackageEngine.Managers.Cargo/Helpers/CargoPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.Cargo/Helpers/CargoPkgOperationHelper.cs index fee54789d..67fd23c66 100644 --- a/src/UniGetUI.PackageEngine.Managers.Cargo/Helpers/CargoPkgOperationHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.Cargo/Helpers/CargoPkgOperationHelper.cs @@ -75,6 +75,11 @@ protected override OperationVeredict _getOperationResult( int returnCode ) { - return returnCode == 0 ? OperationVeredict.Success : OperationVeredict.Failure; + if (returnCode == 0) + { + ((Cargo)Manager).InvalidateInstalledCache(); + return OperationVeredict.Success; + } + return OperationVeredict.Failure; } } diff --git a/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs b/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs index b6ea05546..f9e107af0 100644 --- a/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs +++ b/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs @@ -431,6 +431,21 @@ protected override void _loadManagerExecutableFile( out string callArguments ) { + // On non-Windows, prefer pip3/pip as standalone executables (avoids "No module named pip" + // errors on systems where pip is installed as a command but not as a Python module). + // Fall back to python/python3 + "-m pip" if no standalone pip is found. + if (!OperatingSystem.IsWindows()) + { + var pipPaths = CoreTools.WhichMultiple("pip3").Concat(CoreTools.WhichMultiple("pip")).ToList(); + if (pipPaths.Count > 0) + { + found = true; + path = pipPaths[0]; + callArguments = ""; + return; + } + } + var (_found, _path) = GetExecutableFile(); found = _found; path = _path; diff --git a/src/UniGetUI.PackageEngine.Managers.Vcpkg/Helpers/VcpkgPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.Vcpkg/Helpers/VcpkgPkgOperationHelper.cs index c8993b8d6..1b6a7b96b 100644 --- a/src/UniGetUI.PackageEngine.Managers.Vcpkg/Helpers/VcpkgPkgOperationHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.Vcpkg/Helpers/VcpkgPkgOperationHelper.cs @@ -20,7 +20,7 @@ OperationType operation { OperationType.Install => [Manager.Properties.InstallVerb, package.Id], OperationType.Update => [Manager.Properties.UpdateVerb, package.Id, "--no-dry-run"], - OperationType.Uninstall => [Manager.Properties.UninstallVerb, package.Id], + OperationType.Uninstall => [Manager.Properties.UninstallVerb, package.Id, "--recurse"], _ => throw new InvalidDataException("Invalid package operation"), }; From ea0155be9ea3f3659443d6afc0e448832238f95f Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Thu, 2 Apr 2026 08:37:24 -0400 Subject: [PATCH 6/7] removed unused using --- src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs b/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs index f9e107af0..67c7c6ac2 100644 --- a/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs +++ b/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Net.Http; using System.Text.Json.Nodes; using System.Text.RegularExpressions; using UniGetUI.Core.Data; @@ -8,7 +7,6 @@ using UniGetUI.Core.Tools; using UniGetUI.Interface.Enums; using UniGetUI.PackageEngine.Classes.Manager; -using UniGetUI.PackageEngine.Classes.Manager.ManagerHelpers; using UniGetUI.PackageEngine.Enums; using UniGetUI.PackageEngine.ManagerClasses.Classes; using UniGetUI.PackageEngine.ManagerClasses.Manager; From 5882e9c2c7b53257c0df12aaacb985e0c6170aa5 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Thu, 2 Apr 2026 09:40:02 -0400 Subject: [PATCH 7/7] made some Changes proposed by Copilot --- .../OverridenInstallationOptions.cs | 2 +- .../Helpers/CargoPkgOperationHelper.cs | 2 +- .../BaseNuGet.cs | 5 +- .../Pip.cs | 101 +++++++++++------- 4 files changed, 67 insertions(+), 43 deletions(-) diff --git a/src/UniGetUI.PackageEngine.Enums/OverridenInstallationOptions.cs b/src/UniGetUI.PackageEngine.Enums/OverridenInstallationOptions.cs index c8b79a201..85e07d2a6 100644 --- a/src/UniGetUI.PackageEngine.Enums/OverridenInstallationOptions.cs +++ b/src/UniGetUI.PackageEngine.Enums/OverridenInstallationOptions.cs @@ -16,6 +16,6 @@ public OverridenInstallationOptions(string? scope = null, bool? runAsAdministrat public override string ToString() { - return $""; + return $""; } } diff --git a/src/UniGetUI.PackageEngine.Managers.Cargo/Helpers/CargoPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.Cargo/Helpers/CargoPkgOperationHelper.cs index 67fd23c66..9ac2122f9 100644 --- a/src/UniGetUI.PackageEngine.Managers.Cargo/Helpers/CargoPkgOperationHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.Cargo/Helpers/CargoPkgOperationHelper.cs @@ -31,7 +31,7 @@ OperationType operation if (hasBinstall) parameters = [Manager.Properties.UpdateVerb, package.Id]; else - parameters = ["install", package.Id]; + parameters = ["install", package.Id, "--force"]; break; case OperationType.Uninstall: diff --git a/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs b/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs index e8a65a838..171ad3007 100644 --- a/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs +++ b/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs @@ -79,10 +79,11 @@ protected sealed override IReadOnlyList FindPackages_UnSafe(string quer try { string versionFilter = canPrerelease ? "IsAbsoluteLatestVersion eq true" : "IsLatestVersion eq true"; + string odataQuery = HttpUtility.UrlEncode(query.Replace("'", "''")); Uri? SearchUrl = UseSubstringSearch ? new Uri( $"{source.Url}/Packages()" - + $"?$filter=substringof('{HttpUtility.UrlEncode(query)}',Id) and {versionFilter}" + + $"?$filter=substringof('{odataQuery}',Id) and {versionFilter}" + $"&$orderby=DownloadCount desc" + $"&$skip=0" + $"&$top=50" @@ -90,7 +91,7 @@ protected sealed override IReadOnlyList FindPackages_UnSafe(string quer : new Uri( $"{source.Url}/Search()" + $"?$filter=IsLatestVersion" - + $"&$orderby=Id&searchTerm='{HttpUtility.UrlEncode(query)}'" + + $"&$orderby=Id&searchTerm='{odataQuery}'" + $"&targetFramework=''" + $"&includePrerelease={(canPrerelease ? "true" : "false")}" + $"&$skip=0" diff --git a/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs b/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs index 67c7c6ac2..785afd004 100644 --- a/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs +++ b/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs @@ -110,44 +110,63 @@ public static string GetProxyArgument() private const int CacheMaxAgeHours = 24; private const int MaxSearchResults = 20; + // Shared HTTP client and bounded concurrency for version fetches + private static readonly HttpClient _httpClient = CreateSharedHttpClient(); + private static readonly SemaphoreSlim _versionFetchSemaphore = new(6, 6); + + private static HttpClient CreateSharedHttpClient() + { + var client = new HttpClient(CoreTools.GenericHttpClientParameters); + client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); + return client; + } + protected override IReadOnlyList FindPackages_UnSafe(string query) { INativeTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages); + try + { + string[] allNames = GetOrRefreshIndex(logger); + + string queryLower = query.ToLowerInvariant(); + string[] matches = allNames + .Where(n => n.Contains(queryLower, StringComparison.OrdinalIgnoreCase)) + .OrderBy(n => n.StartsWith(queryLower, StringComparison.OrdinalIgnoreCase) ? 0 : 1) + .ThenBy(n => n.Length) + .Take(MaxSearchResults) + .ToArray(); + + logger.Log($"Matched {matches.Length} packages for query '{query}'"); + + // Fetch latest version for each match in parallel, bounded to 6 concurrent requests + var versionTasks = matches + .Select(FetchLatestVersionAsync) + .ToArray(); + Task.WhenAll(versionTasks).GetAwaiter().GetResult(); + + List packages = []; + for (int i = 0; i < matches.Length; i++) + { + string version = versionTasks[i].Result ?? "latest"; + packages.Add(new Package( + CoreTools.FormatAsName(matches[i]), + matches[i], + version, + DefaultSource, + this, + new(PackageScope.Global) + )); + } - string[] allNames = GetOrRefreshIndex(logger); - - string queryLower = query.ToLowerInvariant(); - string[] matches = allNames - .Where(n => n.Contains(queryLower, StringComparison.OrdinalIgnoreCase)) - .OrderBy(n => n.StartsWith(queryLower, StringComparison.OrdinalIgnoreCase) ? 0 : 1) - .ThenBy(n => n.Length) - .Take(MaxSearchResults) - .ToArray(); - - logger.Log($"Matched {matches.Length} packages for query '{query}'"); - - // Fetch latest version for each match in parallel - var versionTasks = matches - .Select(name => Task.Run(() => FetchLatestVersion(name))) - .ToArray(); - Task.WhenAll(versionTasks).GetAwaiter().GetResult(); - - List packages = []; - for (int i = 0; i < matches.Length; i++) + logger.Close(0); + return packages; + } + catch (Exception e) { - string version = versionTasks[i].Result ?? "latest"; - packages.Add(new Package( - CoreTools.FormatAsName(matches[i]), - matches[i], - version, - DefaultSource, - this, - new(PackageScope.Global) - )); + logger.Error(e); + logger.Close(1); + throw; } - - logger.Close(0); - return packages; } private static string[] GetOrRefreshIndex(INativeTaskLogger logger) @@ -213,22 +232,24 @@ private static string[] GetOrRefreshIndex(INativeTaskLogger logger) return names; } - private static string? FetchLatestVersion(string packageName) + private static async Task FetchLatestVersionAsync(string packageName) { + await _versionFetchSemaphore.WaitAsync().ConfigureAwait(false); try { - using HttpClient client = new(CoreTools.GenericHttpClientParameters); - client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); - string json = client + string json = await _httpClient .GetStringAsync($"https://pypi.org/pypi/{Uri.EscapeDataString(packageName)}/json") - .GetAwaiter() - .GetResult(); + .ConfigureAwait(false); return (JsonNode.Parse(json) as JsonObject)?["info"]?["version"]?.GetValue(); } catch { return null; } + finally + { + _versionFetchSemaphore.Release(); + } } protected override IReadOnlyList GetAvailableUpdates_UnSafe() @@ -489,14 +510,16 @@ protected override void _performExtraLoadingSteps() // need to wait for the ~38 MB download inside the search timeout window. Task.Run(() => { + var logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages); try { - var logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages); GetOrRefreshIndex(logger); logger.Close(0); } catch (Exception e) { + logger.Error(e); + logger.Close(1); Logger.Warn($"Pip: background index pre-warm failed: {e.Message}"); } });