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.Enums/OverridenInstallationOptions.cs b/src/UniGetUI.PackageEngine.Enums/OverridenInstallationOptions.cs index 90fb8ebd7..85e07d2a6 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) { @@ -15,6 +16,6 @@ public OverridenInstallationOptions(string? scope = null, bool? runAsAdministrat public override string ToString() { - return $""; + return $""; } } diff --git a/src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs b/src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs index d898794b5..20807eb9e 100644 --- a/src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs +++ b/src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs @@ -18,12 +18,16 @@ 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)")] 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"; @@ -126,19 +130,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 +162,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"); @@ -183,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 = []; @@ -214,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 0db601a49..9ac2122f9 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, "--force"]; 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( @@ -64,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.Generic.NuGet/BaseNuGet.cs b/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs index e17b58803..171ad3007 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,26 @@ 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"; + string odataQuery = HttpUtility.UrlEncode(query.Replace("'", "''")); + Uri? SearchUrl = UseSubstringSearch + ? new Uri( + $"{source.Url}/Packages()" + + $"?$filter=substringof('{odataQuery}',Id) and {versionFilter}" + + $"&$orderby=DownloadCount desc" + + $"&$skip=0" + + $"&$top=50" + ) + : new Uri( + $"{source.Url}/Search()" + + $"?$filter=IsLatestVersion" + + $"&$orderby=Id&searchTerm='{odataQuery}'" + + $"&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.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..785afd004 100644 --- a/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs +++ b/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs @@ -1,10 +1,12 @@ using System.Diagnostics; +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; 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; @@ -101,111 +103,153 @@ public static string GetProxyArgument() + $"@{proxyUri.AbsoluteUri.Replace($"{proxyUri.Scheme}://", "")}"; } - protected override IReadOnlyList FindPackages_UnSafe(string query) + // 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; + + // 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() { - List Packages = []; + var client = new HttpClient(CoreTools.GenericHttpClientParameters); + client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); + return client; + } - var (found, path) = CoreTools.Which("parse_pip_search.exe"); - if (!found) + protected override IReadOnlyList FindPackages_UnSafe(string query) + { + INativeTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages); + try { - Process proc = new() + 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++) { - 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(); - - aux_logger.AddToStdOut(proc.StandardOutput.ReadToEnd()); - aux_logger.AddToStdErr(proc.StandardError.ReadToEnd()); + string version = versionTasks[i].Result ?? "latest"; + packages.Add(new Package( + CoreTools.FormatAsName(matches[i]), + matches[i], + version, + DefaultSource, + this, + new(PackageScope.Global) + )); + } - proc.WaitForExit(); - aux_logger.Close(proc.ExitCode); - path = "parse_pip_search.exe"; + logger.Close(0); + return packages; } + catch (Exception e) + { + logger.Error(e); + logger.Close(1); + throw; + } + } - using Process p = new() + 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 async Task FetchLatestVersionAsync(string packageName) + { + await _versionFetchSemaphore.WaitAsync().ConfigureAwait(false); + try + { + string json = await _httpClient + .GetStringAsync($"https://pypi.org/pypi/{Uri.EscapeDataString(packageName)}/json") + .ConfigureAwait(false); + return (JsonNode.Parse(json) as JsonObject)?["info"]?["version"]?.GetValue(); + } + catch + { + return null; + } + finally + { + _versionFetchSemaphore.Release(); + } } protected override IReadOnlyList GetAvailableUpdates_UnSafe() @@ -406,6 +450,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; @@ -429,6 +488,7 @@ protected override void _loadManagerVersion(out string version) }; process.Start(); version = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(); if (process.ExitCode is 9009) { @@ -445,6 +505,24 @@ 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(() => + { + var logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages); + try + { + GetOrRefreshIndex(logger); + logger.Close(0); + } + catch (Exception e) + { + logger.Error(e); + logger.Close(1); + Logger.Warn($"Pip: background index pre-warm failed: {e.Message}"); + } + }); } } } 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"); 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"), }; 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, },