diff --git a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs index b8d1876c9a..67d3e2974b 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs @@ -148,6 +148,12 @@ private static async Task LoadElevatorAsync() return; } + if (OperatingSystem.IsLinux()) + { + await LoadLinuxElevatorAsync(); + return; + } + if (SecureSettings.Get(SecureSettings.K.ForceUserGSudo)) { var res = await CoreTools.WhichAsync("gsudo.exe"); @@ -179,6 +185,72 @@ private static async Task LoadElevatorAsync() } } + [System.Runtime.Versioning.SupportedOSPlatform("linux")] + private static async Task LoadLinuxElevatorAsync() + { + // Prefer sudo over pkexec: sudo caches credentials on disk (per user, not per + // process), so the user is only prompted once per ~15-minute window regardless + // of how many packages are installed. pkexec prompts on every single invocation + // because polkit ties its authorization cache to the calling process PID. + var results = await Task.WhenAll( + CoreTools.WhichAsync("sudo"), + CoreTools.WhichAsync("pkexec"), + CoreTools.WhichAsync("zenity")); + var (sudoFound, sudoPath) = results[0]; + var (pkexecFound, pkexecPath) = results[1]; + var (zenityFound, zenityPath) = results[2]; + + if (sudoFound) + { + // Find a graphical askpass helper so sudo can prompt without a terminal. + // Most DEs (KDE, XFCE, ...) pre-set SSH_ASKPASS to their native tool; + // GNOME doesn't, so we fall back to zenity with a small wrapper script + // (zenity --password ignores positional args, so it needs the wrapper + // to forward the prompt text via --text="$1"). + string? askpass = null; + var envAskpass = Environment.GetEnvironmentVariable("SSH_ASKPASS"); + if (!string.IsNullOrEmpty(envAskpass) && File.Exists(envAskpass)) + askpass = envAskpass; + else if (zenityFound) + { + askpass = Path.Join(CoreData.UniGetUIDataDirectory, "linux-askpass.sh"); + await File.WriteAllTextAsync(askpass, + $"#!/bin/sh\n\"{zenityPath}\" --password --title=\"UniGetUI\" --text=\"$1\"\n"); + File.SetUnixFileMode(askpass, + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.GroupRead | UnixFileMode.GroupExecute | + UnixFileMode.OtherRead | UnixFileMode.OtherExecute); + } + + if (askpass != null) + { + Environment.SetEnvironmentVariable("SUDO_ASKPASS", askpass); + CoreData.ElevatorPath = sudoPath; + CoreData.ElevatorArgs = "-A"; + Logger.Debug($"Using sudo -A with askpass '{askpass}'"); + return; + } + } + + // Fall back to pkexec when no usable sudo+askpass combination is found. + // pkexec handles its own graphical prompt via polkit but prompts every invocation. + if (pkexecFound) + { + CoreData.ElevatorPath = pkexecPath; + Logger.Warn($"Using pkexec at {pkexecPath} (prompts on every operation)"); + return; + } + + if (sudoFound) + { + CoreData.ElevatorPath = sudoPath; + Logger.Warn($"Falling back to sudo without graphical askpass at {sudoPath}"); + return; + } + + Logger.Warn("No elevation tool found (pkexec/sudo). Admin operations will fail."); + } + /// /// Checks all ready package managers for missing dependencies. /// Returns the list of dependencies whose installation was not skipped by the user. diff --git a/src/UniGetUI.Avalonia/ViewModels/DialogPages/ManageIgnoredUpdatesViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/DialogPages/ManageIgnoredUpdatesViewModel.cs index 5a740ddaf2..5426859ae1 100644 --- a/src/UniGetUI.Avalonia/ViewModels/DialogPages/ManageIgnoredUpdatesViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/DialogPages/ManageIgnoredUpdatesViewModel.cs @@ -108,8 +108,9 @@ private async Task ResetAll() await entry.RemoveAsync(); } - private static string ResolveManagerIcon(string managerKey) => - (managerKey switch + private static string ResolveManagerIcon(string managerKey) + { + string name = managerKey switch { "winget" => "winget", "scoop" => "scoop", @@ -123,10 +124,13 @@ private static string ResolveManagerIcon(string managerKey) => "steam" => "steam", "gog" => "gog", "uplay" => "uplay", + "apt" => "apt", + "dnf" => "dnf", + "pacman" => "pacman", _ => "ms_store", - }) is var name - ? $"avares://UniGetUI.Avalonia/Assets/Symbols/{name}.svg" - : $"avares://UniGetUI.Avalonia/Assets/Symbols/ms_store.svg"; + }; + return $"avares://UniGetUI.Avalonia/Assets/Symbols/{name}.svg"; + } } public partial class IgnoredPackageEntryViewModel : ObservableObject diff --git a/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsPageButton.cs b/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsPageButton.cs index fa65be12d5..f08d53dea4 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsPageButton.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsPageButton.cs @@ -20,7 +20,7 @@ public IconType Icon { set => HeaderIcon = new SvgIcon { - Path = $"avares://UniGetUI.Avalonia/Assets/Symbols/{IconTypeToName(value)}.svg", + Path = IconTypeToPath(value), Width = 24, Height = 24, }; @@ -32,21 +32,25 @@ public SettingsPageButton() IsClickEnabled = true; } - private static string IconTypeToName(IconType icon) => icon switch + private static string IconTypeToPath(IconType icon) { - IconType.Chocolatey => "choco", - IconType.Package => "package", - IconType.UAC => "uac", - IconType.Update => "update", - IconType.Help => "help", - IconType.Console => "console", - IconType.Checksum => "checksum", - IconType.Download => "download", - IconType.Settings => "settings", - IconType.SaveAs => "save_as", - IconType.OpenFolder => "open_folder", - IconType.Experimental => "experimental", - IconType.ClipboardList => "clipboard_list", - _ => icon.ToString().ToLower(), - }; + string name = icon switch + { + IconType.Chocolatey => "choco", + IconType.Package => "package", + IconType.UAC => "uac", + IconType.Update => "update", + IconType.Help => "help", + IconType.Console => "console", + IconType.Checksum => "checksum", + IconType.Download => "download", + IconType.Settings => "settings", + IconType.SaveAs => "save_as", + IconType.OpenFolder => "open_folder", + IconType.Experimental => "experimental", + IconType.ClipboardList => "clipboard_list", + _ => icon.ToString().ToLower(), + }; + return $"avares://UniGetUI.Avalonia/Assets/Symbols/{name}.svg"; + } } diff --git a/src/UniGetUI.Core.Data/CoreData.cs b/src/UniGetUI.Core.Data/CoreData.cs index eb526288e6..a5ca7f794f 100644 --- a/src/UniGetUI.Core.Data/CoreData.cs +++ b/src/UniGetUI.Core.Data/CoreData.cs @@ -370,6 +370,12 @@ public static string UniGetUIExecutableFile public static string ElevatorPath = ""; + /// + /// Extra arguments to insert between the elevator binary and the elevated command. + /// For example, "-A" when using sudo with an askpass helper on Linux. + /// + public static string ElevatorArgs = ""; + /// /// This method will return the most appropriate data directory. /// If the new directory exists, it will be used. diff --git a/src/UniGetUI.Core.LanguageEngine/Assets/Data/TranslatedPercentages.json b/src/UniGetUI.Core.LanguageEngine/Assets/Data/TranslatedPercentages.json index 7101fa3e23..12d11b5a31 100644 --- a/src/UniGetUI.Core.LanguageEngine/Assets/Data/TranslatedPercentages.json +++ b/src/UniGetUI.Core.LanguageEngine/Assets/Data/TranslatedPercentages.json @@ -6,13 +6,13 @@ "bn": "100%", "da": "100%", "el": "100%", - "eo": "100%", + "eo": "99%", "es": "100%", "es-MX": "100%", "et": "100%", "fa": "100%", "fil": "100%", - "gl": "100%", + "gl": "99%", "gu": "100%", "he": "100%", "hi": "100%", @@ -23,10 +23,10 @@ "ka": "100%", "kn": "100%", "ko": "100%", - "ku": "100%", + "ku": "99%", "lt": "100%", "mk": "100%", - "mr": "100%", + "mr": "99%", "nb": "100%", "nn": "100%", "pt_PT": "100%", @@ -58,4 +58,4 @@ "zh_CN": "100%", "zh_TW": "100%", "en": "100%" -} +} diff --git a/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json b/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json index 5006ff8daf..79b145a0a7 100644 --- a/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json +++ b/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json @@ -502,6 +502,9 @@ "A repository full of tools and executables designed with Microsoft's .NET ecosystem in mind.
Contains: .NET related tools and scripts": "A repository full of tools and executables designed with Microsoft's .NET ecosystem in mind.
Contains: .NET related tools and scripts", "NuPkg (zipped manifest)": "NuPkg (zipped manifest)", "The Missing Package Manager for macOS (or Linux).
Contains: Formulae, Casks": "The Missing Package Manager for macOS (or Linux).
Contains: Formulae, Casks", + "The default package manager for Debian/Ubuntu-based Linux distributions.
Contains: Debian/Ubuntu packages": "The default package manager for Debian/Ubuntu-based Linux distributions.
Contains: Debian/Ubuntu packages", + "The default package manager for RHEL/Fedora-based Linux distributions.
Contains: RPM packages": "The default package manager for RHEL/Fedora-based Linux distributions.
Contains: RPM packages", + "The default package manager for Arch Linux and its derivatives.
Contains: Arch Linux packages": "The default package manager for Arch Linux and its derivatives.
Contains: Arch Linux packages", "Node JS's package manager. Full of libraries and other utilities that orbit the javascript world
Contains: Node javascript libraries and other related utilities": "Node JS's package manager. Full of libraries and other utilities that orbit the javascript world
Contains: Node javascript libraries and other related utilities", "Python's library manager. Full of python libraries and other python-related utilities
Contains: Python libraries and related utilities": "Python's library manager. Full of python libraries and other python-related utilities
Contains: Python libraries and related utilities", "PowerShell's package manager. Find libraries and scripts to expand PowerShell capabilities
Contains: Modules, Scripts, Cmdlets": "PowerShell's package manager. Find libraries and scripts to expand PowerShell capabilities
Contains: Modules, Scripts, Cmdlets", diff --git a/src/UniGetUI.Core.Tools/Tools.cs b/src/UniGetUI.Core.Tools/Tools.cs index b7e683d1b7..5ba7454c6f 100644 --- a/src/UniGetUI.Core.Tools/Tools.cs +++ b/src/UniGetUI.Core.Tools/Tools.cs @@ -505,12 +505,29 @@ public static async Task CacheUACForCurrentProcess() { _isCaching = true; Logger.Info("Caching admin rights for process id " + Environment.ProcessId); + + var elevatorName = Path.GetFileName(CoreData.ElevatorPath); + + // pkexec prompts on every invocation and has no caching protocol. + if (elevatorName == "pkexec") + { + _isCaching = false; + return; + } + + // sudo: -v validates/extends the cached timestamp. + // Prepend -A only when the SUDO_ASKPASS helper is configured. + // gsudo / UniGetUI Elevator.exe: use the gsudo cache protocol. + string cacheArgs = elevatorName == "sudo" + ? (CoreData.ElevatorArgs.Contains("-A") ? "-Av" : "-v") + : "cache on --pid " + Environment.ProcessId + " -d 1"; + using Process p = new() { StartInfo = new ProcessStartInfo { FileName = CoreData.ElevatorPath, - Arguments = "cache on --pid " + Environment.ProcessId + " -d 1", + Arguments = cacheArgs, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -546,12 +563,27 @@ public static async Task ResetUACForCurrentProcess() Logger.Info( "Resetting administrator rights cache for process id " + Environment.ProcessId ); + + var elevatorName = Path.GetFileName(CoreData.ElevatorPath); + + // pkexec prompts on every invocation and has no caching protocol. + if (elevatorName == "pkexec") + { + return; + } + + // sudo: -K removes all cached timestamps. + // gsudo / UniGetUI Elevator.exe: use the gsudo cache protocol. + string resetArgs = elevatorName == "sudo" + ? "-K" + : "cache off --pid " + Environment.ProcessId; + using Process p = new() { StartInfo = new ProcessStartInfo { FileName = CoreData.ElevatorPath, - Arguments = "cache off --pid " + Environment.ProcessId, + Arguments = resetArgs, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, diff --git a/src/UniGetUI.Interface.Enums/Enums.cs b/src/UniGetUI.Interface.Enums/Enums.cs index 9e56b6d078..f4254b6aa7 100644 --- a/src/UniGetUI.Interface.Enums/Enums.cs +++ b/src/UniGetUI.Interface.Enums/Enums.cs @@ -85,6 +85,9 @@ public enum IconType Rust = '\uE941', Vcpkg = '\uE942', Homebrew = '\uE943', + Apt = '\uE944', + Dnf = '\uE945', + Pacman = '\uE946', } public class NotificationArguments diff --git a/src/UniGetUI.PackageEngine.Managers.Apt/Apt.cs b/src/UniGetUI.PackageEngine.Managers.Apt/Apt.cs new file mode 100644 index 0000000000..622ca02101 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Apt/Apt.cs @@ -0,0 +1,255 @@ +using System.Diagnostics; +using System.Text.RegularExpressions; +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.Interfaces; +using UniGetUI.PackageEngine.ManagerClasses.Classes; +using UniGetUI.PackageEngine.ManagerClasses.Manager; +using UniGetUI.PackageEngine.PackageClasses; +using UniGetUI.PackageEngine.Structs; + +namespace UniGetUI.PackageEngine.Managers.AptManager; + +public class Apt : PackageManager +{ + public Apt() + { + Dependencies = []; + + Capabilities = new ManagerCapabilities + { + CanRunAsAdmin = true, + CanSkipIntegrityChecks = true, + SupportsCustomSources = false, + SupportsProxy = ProxySupport.No, + SupportsProxyAuth = false, + }; + + Properties = new ManagerProperties + { + Name = "Apt", + Description = CoreTools.Translate( + "The default package manager for Debian/Ubuntu-based Linux distributions.
Contains: Debian/Ubuntu packages" + ), + IconId = IconType.Apt, + ColorIconId = "debian", + ExecutableFriendlyName = "apt", + InstallVerb = "install", + UpdateVerb = "install", + UninstallVerb = "remove", + DefaultSource = new ManagerSource(this, "apt", new Uri("https://packages.debian.org")), + KnownSources = [new ManagerSource(this, "apt", new Uri("https://packages.debian.org"))], + }; + + DetailsHelper = new AptPkgDetailsHelper(this); + OperationHelper = new AptPkgOperationHelper(this); + } + + // ── Executable discovery ─────────────────────────────────────────────── + + public override IReadOnlyList FindCandidateExecutableFiles() + { + var candidates = new List(CoreTools.WhichMultiple("apt")); + foreach (var path in new[] { "/usr/bin/apt", "/usr/local/bin/apt" }) + { + if (File.Exists(path) && !candidates.Contains(path)) + candidates.Add(path); + } + return candidates; + } + + protected override void _loadManagerExecutableFile( + out bool found, + out string path, + out string callArguments) + { + (found, path) = GetExecutableFile(); + callArguments = ""; + } + + protected override void _loadManagerVersion(out string version) + { + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + p.Start(); + // First line: "apt X.Y.Z (arch)" + var line = p.StandardOutput.ReadLine()?.Trim() ?? ""; + var parts = line.Split(' '); + version = parts.Length >= 2 ? parts[1] : line; + p.WaitForExit(); + } + + // ── Index refresh ────────────────────────────────────────────────────── + + public override void RefreshPackageIndexes() + { + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = "update", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.RefreshIndexes, p); + p.Start(); + logger.AddToStdOut(p.StandardOutput.ReadToEnd()); + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + } + + // ── Package listing ──────────────────────────────────────────────────── + + protected override IReadOnlyList FindPackages_UnSafe(string query) + { + var packages = new List(); + + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "apt-cache", + Arguments = $"search -- {query}", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages, p); + p.Start(); + + // Output format: " - " + string? line; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + logger.AddToStdOut(line); + var dashIdx = line.IndexOf(" - ", StringComparison.Ordinal); + if (dashIdx <= 0) continue; + + var id = line[..dashIdx].Trim(); + if (id.Length == 0) continue; + + packages.Add(new Package( + CoreTools.FormatAsName(id), + id, + CoreTools.Translate("Latest"), + Properties.DefaultSource!, + this)); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + return packages; + } + + protected override IReadOnlyList GetInstalledPackages_UnSafe() + { + var packages = new List(); + + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = "list --installed", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.ListInstalledPackages, p); + p.Start(); + + // Output format: "/,... [installed,...]" + // First line is "Listing..." header — skip it. + var idVersionPattern = new Regex(@"^([^/\s]+)/\S+\s+(\S+)"); + string? line; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + logger.AddToStdOut(line); + var m = idVersionPattern.Match(line); + if (!m.Success) continue; + + var id = m.Groups[1].Value; + var version = m.Groups[2].Value; + packages.Add(new Package( + CoreTools.FormatAsName(id), + id, + version, + Properties.DefaultSource!, + this)); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + return packages; + } + + protected override IReadOnlyList GetAvailableUpdates_UnSafe() + { + var packages = new List(); + + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = "list --upgradable", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.ListUpdates, p); + p.Start(); + + // Output format: "/,... [upgradable from: ]" + var pattern = new Regex(@"^([^/\s]+)/\S+\s+(\S+)\s+\S+\s+\[upgradable from: ([^\]]+)\]"); + string? line; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + logger.AddToStdOut(line); + var m = pattern.Match(line); + if (!m.Success) continue; + + var id = m.Groups[1].Value; + var newVersion = m.Groups[2].Value; + var oldVersion = m.Groups[3].Value; + packages.Add(new Package( + CoreTools.FormatAsName(id), + id, + oldVersion, + newVersion, + Properties.DefaultSource!, + this)); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + return packages; + } +} diff --git a/src/UniGetUI.PackageEngine.Managers.Apt/Helpers/AptPkgDetailsHelper.cs b/src/UniGetUI.PackageEngine.Managers.Apt/Helpers/AptPkgDetailsHelper.cs new file mode 100644 index 0000000000..fd82b5f0fb --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Apt/Helpers/AptPkgDetailsHelper.cs @@ -0,0 +1,162 @@ +using System.Diagnostics; +using UniGetUI.Core.IconEngine; +using UniGetUI.PackageEngine.Classes.Manager.BaseProviders; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.ManagerClasses.Classes; + +namespace UniGetUI.PackageEngine.Managers.AptManager; + +internal sealed class AptPkgDetailsHelper : BasePkgDetailsHelper +{ + public AptPkgDetailsHelper(Apt manager) + : base(manager) { } + + protected override void GetDetails_UnSafe(IPackageDetails details) + { + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "apt-cache", + Arguments = $"show {details.Package.Id}", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + + IProcessTaskLogger logger = Manager.TaskLogger.CreateNew( + LoggableTaskType.LoadPackageDetails, p); + p.Start(); + + // apt-cache show outputs key: value pairs, one per line. + // Multi-line values are indented with a leading space. + var descLines = new List(); + bool inDescription = false; + + string? line; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + logger.AddToStdOut(line); + + if (line.Length == 0) + { + // Blank line ends the current record — stop after the first record. + if (inDescription) break; + continue; + } + + if (line.StartsWith(' ') && inDescription) + { + var descLine = line.TrimStart(); + if (descLine != ".") descLines.Add(descLine); + continue; + } + + inDescription = false; + + var colonIdx = line.IndexOf(": ", StringComparison.Ordinal); + if (colonIdx <= 0) continue; + + var key = line[..colonIdx].Trim(); + var value = line[(colonIdx + 2)..].Trim(); + + switch (key) + { + case "Version": + // Already known; fill in so it's accessible if needed + break; + case "Homepage": + if (Uri.TryCreate(value, UriKind.Absolute, out var homepage)) + details.HomepageUrl = homepage; + break; + case "Description": + case "Description-en": + details.Description = value; + inDescription = true; + break; + case "Maintainer": + details.Publisher = value; + break; + case "Depends": + details.Dependencies.Clear(); + foreach (var dep in value.Split(',')) + { + var depName = dep.Trim().Split(' ')[0]; + if (depName.Length > 0) + details.Dependencies.Add(new() { Name = depName, Version = "", Mandatory = true }); + } + break; + case "Recommends": + foreach (var dep in value.Split(',')) + { + var depName = dep.Trim().Split(' ')[0]; + if (depName.Length > 0) + details.Dependencies.Add(new() { Name = depName, Version = "", Mandatory = false }); + } + break; + case "Installed-Size": + details.InstallerSize = long.TryParse(value.Replace(" kB", "").Trim(), out var kb) + ? kb * 1024 + : 0; + break; + case "Source": + details.ManifestUrl = new Uri($"https://packages.debian.org/source/stable/{value}"); + break; + } + } + + if (descLines.Count > 0) + details.Description = (details.Description ?? "") + "\n" + string.Join("\n", descLines); + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + } + + protected override CacheableIcon? GetIcon_UnSafe(IPackage package) + => throw new NotImplementedException(); + + protected override IReadOnlyList GetScreenshots_UnSafe(IPackage package) + => throw new NotImplementedException(); + + protected override string? GetInstallLocation_UnSafe(IPackage package) + { + // Debian packages install to system paths; the most reliable way + // to find the install location is to query dpkg. + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "dpkg", + Arguments = $"-L {package.Id}", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + p.Start(); + + // Must drain all stdout before WaitForExit — packages with many files + // will fill the pipe buffer and deadlock if we stop reading early. + string? result = null; + string? line; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + if (result is not null) continue; + var path = line.Trim(); + if (Directory.Exists(path)) + result = path; + } + + p.StandardError.ReadToEnd(); + p.WaitForExit(); + return result; + } + + protected override IReadOnlyList GetInstallableVersions_UnSafe(IPackage package) + => throw new InvalidOperationException("APT does not support installing arbitrary versions"); +} diff --git a/src/UniGetUI.PackageEngine.Managers.Apt/Helpers/AptPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.Apt/Helpers/AptPkgOperationHelper.cs new file mode 100644 index 0000000000..d247277757 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Apt/Helpers/AptPkgOperationHelper.cs @@ -0,0 +1,60 @@ +using UniGetUI.PackageEngine.Classes.Manager.BaseProviders; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.Serializable; + +namespace UniGetUI.PackageEngine.Managers.AptManager; + +internal sealed class AptPkgOperationHelper : BasePkgOperationHelper +{ + public AptPkgOperationHelper(Apt manager) + : base(manager) { } + + protected override IReadOnlyList _getOperationParameters( + IPackage package, + InstallOptions options, + OperationType operation) + { + // apt always requires root — force elevation via InstallOptions (reference type, persists) + options.RunAsAdministrator = true; + + List parameters = + [ + operation switch + { + OperationType.Install => Manager.Properties.InstallVerb, + OperationType.Uninstall => Manager.Properties.UninstallVerb, + OperationType.Update => Manager.Properties.UpdateVerb, + _ => throw new InvalidDataException("Invalid package operation"), + }, + ]; + + if (operation == OperationType.Update) + parameters.Add("--only-upgrade"); + + parameters.Add("-y"); + parameters.Add(package.Id); + + if (options.SkipHashCheck) + parameters.Add("--allow-unauthenticated"); + + parameters.AddRange( + operation switch + { + OperationType.Update => options.CustomParameters_Update, + OperationType.Uninstall => options.CustomParameters_Uninstall, + _ => options.CustomParameters_Install, + }); + + return parameters; + } + + protected override OperationVeredict _getOperationResult( + IPackage package, + OperationType operation, + IReadOnlyList processOutput, + int returnCode) + { + return returnCode == 0 ? OperationVeredict.Success : OperationVeredict.Failure; + } +} diff --git a/src/UniGetUI.PackageEngine.Managers.Apt/UniGetUI.PackageEngine.Managers.Apt.csproj b/src/UniGetUI.PackageEngine.Managers.Apt/UniGetUI.PackageEngine.Managers.Apt.csproj new file mode 100644 index 0000000000..00d99aa0e4 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Apt/UniGetUI.PackageEngine.Managers.Apt.csproj @@ -0,0 +1,23 @@ + + + $(SharedTargetFrameworks) + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.PackageEngine.Managers.Dnf/Dnf.cs b/src/UniGetUI.PackageEngine.Managers.Dnf/Dnf.cs new file mode 100644 index 0000000000..f54325fbd9 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Dnf/Dnf.cs @@ -0,0 +1,301 @@ +using System.Diagnostics; +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.Interfaces; +using UniGetUI.PackageEngine.ManagerClasses.Classes; +using UniGetUI.PackageEngine.ManagerClasses.Manager; +using UniGetUI.PackageEngine.PackageClasses; +using UniGetUI.PackageEngine.Structs; + +namespace UniGetUI.PackageEngine.Managers.DnfManager; + +public class Dnf : PackageManager +{ + // Known RPM architectures — used to strip the trailing . from package names. + private static readonly HashSet _knownArches = + ["x86_64", "aarch64", "noarch", "i686", "i386", "ppc64le", "s390x", "src"]; + + public Dnf() + { + Dependencies = []; + + Capabilities = new ManagerCapabilities + { + CanRunAsAdmin = true, + CanSkipIntegrityChecks = true, + SupportsCustomSources = false, + SupportsProxy = ProxySupport.No, + SupportsProxyAuth = false, + }; + + Properties = new ManagerProperties + { + Name = "Dnf", + Description = CoreTools.Translate( + "The default package manager for RHEL/Fedora-based Linux distributions.
Contains: RPM packages" + ), + IconId = IconType.Dnf, + ColorIconId = "dnf", + ExecutableFriendlyName = "dnf", + InstallVerb = "install", + UpdateVerb = "upgrade", + UninstallVerb = "remove", + DefaultSource = new ManagerSource(this, "dnf", new Uri("https://fedoraproject.org/wiki/DNF")), + KnownSources = [new ManagerSource(this, "dnf", new Uri("https://fedoraproject.org/wiki/DNF"))], + }; + + DetailsHelper = new DnfPkgDetailsHelper(this); + OperationHelper = new DnfPkgOperationHelper(this); + } + + // ── Executable discovery ─────────────────────────────────────────────── + + public override IReadOnlyList FindCandidateExecutableFiles() + { + var candidates = new List(CoreTools.WhichMultiple("dnf5")); + foreach (var path in CoreTools.WhichMultiple("dnf")) + { + if (!candidates.Contains(path)) + candidates.Add(path); + } + foreach (var path in new[] { "/usr/bin/dnf5", "/usr/bin/dnf", "/usr/local/bin/dnf" }) + { + if (File.Exists(path) && !candidates.Contains(path)) + candidates.Add(path); + } + return candidates; + } + + protected override void _loadManagerExecutableFile( + out bool found, + out string path, + out string callArguments) + { + (found, path) = GetExecutableFile(); + callArguments = ""; + } + + protected override void _loadManagerVersion(out string version) + { + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + p.Start(); + // First line is the version number: "X.Y.Z" + version = p.StandardOutput.ReadLine()?.Trim() ?? ""; + p.StandardOutput.ReadToEnd(); + p.StandardError.ReadToEnd(); + p.WaitForExit(); + } + + // ── Index refresh ────────────────────────────────────────────────────── + + public override void RefreshPackageIndexes() + { + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = "makecache", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.RefreshIndexes, p); + p.Start(); + logger.AddToStdOut(p.StandardOutput.ReadToEnd()); + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + } + + // ── Package listing ──────────────────────────────────────────────────── + + protected override IReadOnlyList FindPackages_UnSafe(string query) + { + var packages = new List(); + + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = $"search --quiet {query}", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages, p); + p.Start(); + + // Output: ". : " + // Section headers look like "===== Name Matched: =====" — skip them. + var seen = new HashSet(StringComparer.Ordinal); + string? line; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + logger.AddToStdOut(line); + if (line.StartsWith('=') || line.Length == 0) continue; + + var colonIdx = line.IndexOf(" : ", StringComparison.Ordinal); + if (colonIdx <= 0) continue; + + var nameArch = line[..colonIdx].Trim(); + var id = StripArch(nameArch); + if (id.Length == 0 || !seen.Add(id)) continue; + + packages.Add(new Package( + CoreTools.FormatAsName(id), + id, + CoreTools.Translate("Latest"), + Properties.DefaultSource!, + this)); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + // dnf search exits 1 when no packages match the query — not an error + logger.Close(p.ExitCode == 1 && packages.Count == 0 ? 0 : p.ExitCode); + return packages; + } + + protected override IReadOnlyList GetInstalledPackages_UnSafe() + { + var packages = new List(); + + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = "list --installed", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.ListInstalledPackages, p); + p.Start(); + + // Output: ". - <@repo>" + // Skip header/metadata lines (e.g. "Installed Packages", + // "Last metadata expiration check: ...") by requiring a known arch suffix. + string? line; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + logger.AddToStdOut(line); + var parts = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 2 || !IsPackageLine(parts[0])) continue; + + var id = StripArch(parts[0]); + var version = parts[1]; + packages.Add(new Package( + CoreTools.FormatAsName(id), + id, + version, + Properties.DefaultSource!, + this)); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + return packages; + } + + protected override IReadOnlyList GetAvailableUpdates_UnSafe() + { + var packages = new List(); + + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = "list --upgrades", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + // Build lookup before starting the process — reading from its pipe + // while a second process runs risks filling the pipe buffer and deadlocking. + Dictionary installed = []; + foreach (var pkg in GetInstalledPackages_UnSafe()) + installed.TryAdd(pkg.Id, pkg.VersionString); + + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.ListUpdates, p); + p.Start(); + + // Output: ". - " + // Skip header/metadata lines (e.g. "Available Upgrades", + // "Last metadata expiration check: ...") by requiring a known arch suffix. + string? line; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + logger.AddToStdOut(line); + var parts = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 2 || !IsPackageLine(parts[0])) continue; + + var id = StripArch(parts[0]); + var newVersion = parts[1]; + installed.TryGetValue(id, out var oldVersion); + packages.Add(new Package( + CoreTools.FormatAsName(id), + id, + oldVersion ?? "", + newVersion, + Properties.DefaultSource!, + this)); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + return packages; + } + + // ── Helpers ──────────────────────────────────────────────────────────── + + /// + /// Strips the trailing . from a DNF package token (e.g. "vim.x86_64" → "vim"). + /// Package names that do not end in a known arch are returned unchanged. + /// + internal static string StripArch(string nameArch) + { + var dot = nameArch.LastIndexOf('.'); + if (dot > 0 && _knownArches.Contains(nameArch[(dot + 1)..])) + return nameArch[..dot]; + return nameArch; + } + + /// + /// Returns true when looks like a DNF package entry + /// (i.e. ends in a known architecture suffix such as ".x86_64" or ".noarch"). + /// Used to skip header/metadata lines in dnf list output. + /// + private static bool IsPackageLine(string token) + { + var dot = token.LastIndexOf('.'); + return dot > 0 && _knownArches.Contains(token[(dot + 1)..]); + } +} diff --git a/src/UniGetUI.PackageEngine.Managers.Dnf/Helpers/DnfPkgDetailsHelper.cs b/src/UniGetUI.PackageEngine.Managers.Dnf/Helpers/DnfPkgDetailsHelper.cs new file mode 100644 index 0000000000..faa1dabbce --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Dnf/Helpers/DnfPkgDetailsHelper.cs @@ -0,0 +1,159 @@ +using System.Diagnostics; +using UniGetUI.Core.IconEngine; +using UniGetUI.PackageEngine.Classes.Manager.BaseProviders; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.ManagerClasses.Classes; + +namespace UniGetUI.PackageEngine.Managers.DnfManager; + +internal sealed class DnfPkgDetailsHelper : BasePkgDetailsHelper +{ + public DnfPkgDetailsHelper(Dnf manager) + : base(manager) { } + + protected override void GetDetails_UnSafe(IPackageDetails details) + { + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Manager.Status.ExecutablePath, + Arguments = $"info {details.Package.Id}", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + + IProcessTaskLogger logger = Manager.TaskLogger.CreateNew( + LoggableTaskType.LoadPackageDetails, p); + p.Start(); + + // dnf info outputs "Key : value" pairs. + // Multi-line Description values are indented. + var descLines = new List(); + bool inDescription = false; + + string? line; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + logger.AddToStdOut(line); + + // Blank line marks the end of a package block. dnf info can output multiple + // blocks (e.g. "Installed Packages" then "Available Packages") — stop after + // the first so the second block doesn't silently overwrite parsed fields. + if (line.Length == 0) break; + + // Continuation lines for Description are indented with " : " prefix: + // " : second line of the description" + if (inDescription && line.StartsWith(' ')) + { + var contColon = line.IndexOf(" : ", StringComparison.Ordinal); + descLines.Add(contColon >= 0 ? line[(contColon + 3)..].Trim() : line.Trim()); + continue; + } + + inDescription = false; + + var colonIdx = line.IndexOf(" : ", StringComparison.Ordinal); + if (colonIdx <= 0) continue; + + var key = line[..colonIdx].Trim(); + var value = line[(colonIdx + 3)..].Trim(); + + switch (key) + { + case "URL": + if (Uri.TryCreate(value, UriKind.Absolute, out var url)) + details.HomepageUrl = url; + break; + case "Summary": + details.Description = value; + break; + case "Description": + descLines.Add(value); + inDescription = true; + break; + case "License": + details.License = value; + break; + case "Packager": + details.Publisher = value; + break; + case "Size": + // e.g. "1.5 M" or "234 k" + details.InstallerSize = ParseDnfSize(value); + break; + } + } + + if (descLines.Count > 0) + details.Description = string.Join("\n", descLines); + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + } + + protected override CacheableIcon? GetIcon_UnSafe(IPackage package) + => throw new NotImplementedException(); + + protected override IReadOnlyList GetScreenshots_UnSafe(IPackage package) + => throw new NotImplementedException(); + + protected override string? GetInstallLocation_UnSafe(IPackage package) + { + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "rpm", + Arguments = $"-ql {package.Id}", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + p.Start(); + + // Must drain all stdout before WaitForExit — packages like glibc have thousands + // of file entries and will fill the pipe buffer, causing a deadlock. + string? result = null; + string? line; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + if (result is not null) continue; + var path = line.Trim(); + if (Directory.Exists(path)) + result = path; + } + + p.StandardError.ReadToEnd(); + p.WaitForExit(); + return result; + } + + protected override IReadOnlyList GetInstallableVersions_UnSafe(IPackage package) + => throw new InvalidOperationException("DNF does not support installing arbitrary versions"); + + private static long ParseDnfSize(string value) + { + // Format: "1.5 M", "234 k", "56 G" + var parts = value.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 2 || !double.TryParse(parts[0], + System.Globalization.NumberStyles.Any, + System.Globalization.CultureInfo.InvariantCulture, out var num)) + return 0; + + return parts[1].ToUpperInvariant() switch + { + "K" => (long)(num * 1024), + "M" => (long)(num * 1024 * 1024), + "G" => (long)(num * 1024 * 1024 * 1024), + _ => (long)num, + }; + } +} diff --git a/src/UniGetUI.PackageEngine.Managers.Dnf/Helpers/DnfPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.Dnf/Helpers/DnfPkgOperationHelper.cs new file mode 100644 index 0000000000..4df6cf8971 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Dnf/Helpers/DnfPkgOperationHelper.cs @@ -0,0 +1,56 @@ +using UniGetUI.PackageEngine.Classes.Manager.BaseProviders; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.Serializable; + +namespace UniGetUI.PackageEngine.Managers.DnfManager; + +internal sealed class DnfPkgOperationHelper : BasePkgOperationHelper +{ + public DnfPkgOperationHelper(Dnf manager) + : base(manager) { } + + protected override IReadOnlyList _getOperationParameters( + IPackage package, + InstallOptions options, + OperationType operation) + { + // dnf always requires root — force elevation via InstallOptions (reference type, persists) + options.RunAsAdministrator = true; + + List parameters = + [ + operation switch + { + OperationType.Install => Manager.Properties.InstallVerb, + OperationType.Update => Manager.Properties.UpdateVerb, + OperationType.Uninstall => Manager.Properties.UninstallVerb, + _ => throw new InvalidDataException("Invalid package operation"), + }, + "-y", + package.Id, + ]; + + if (options.SkipHashCheck) + parameters.Add("--nogpgcheck"); + + parameters.AddRange( + operation switch + { + OperationType.Update => options.CustomParameters_Update, + OperationType.Uninstall => options.CustomParameters_Uninstall, + _ => options.CustomParameters_Install, + }); + + return parameters; + } + + protected override OperationVeredict _getOperationResult( + IPackage package, + OperationType operation, + IReadOnlyList processOutput, + int returnCode) + { + return returnCode == 0 ? OperationVeredict.Success : OperationVeredict.Failure; + } +} diff --git a/src/UniGetUI.PackageEngine.Managers.Dnf/UniGetUI.PackageEngine.Managers.Dnf.csproj b/src/UniGetUI.PackageEngine.Managers.Dnf/UniGetUI.PackageEngine.Managers.Dnf.csproj new file mode 100644 index 0000000000..00d99aa0e4 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Dnf/UniGetUI.PackageEngine.Managers.Dnf.csproj @@ -0,0 +1,23 @@ + + + $(SharedTargetFrameworks) + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.PackageEngine.Managers.Pacman/Helpers/PacmanPkgDetailsHelper.cs b/src/UniGetUI.PackageEngine.Managers.Pacman/Helpers/PacmanPkgDetailsHelper.cs new file mode 100644 index 0000000000..bcfdb2f8ad --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Pacman/Helpers/PacmanPkgDetailsHelper.cs @@ -0,0 +1,183 @@ +using System.Diagnostics; +using UniGetUI.Core.IconEngine; +using UniGetUI.PackageEngine.Classes.Manager.BaseProviders; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.ManagerClasses.Classes; + +namespace UniGetUI.PackageEngine.Managers.PacmanManager; + +internal sealed class PacmanPkgDetailsHelper : BasePkgDetailsHelper +{ + public PacmanPkgDetailsHelper(Pacman manager) + : base(manager) { } + + protected override void GetDetails_UnSafe(IPackageDetails details) + { + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Manager.Status.ExecutablePath, + Arguments = $"-Si {details.Package.Id}", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + + IProcessTaskLogger logger = Manager.TaskLogger.CreateNew( + LoggableTaskType.LoadPackageDetails, p); + p.Start(); + + // "pacman -Si" outputs "Key : value" pairs (key padded with spaces). + // Multi-dep fields (Depends On, Optional Deps) wrap onto continuation lines: + // " : next-dep another-dep" + // Continuation lines have an empty key (all spaces before " : "). + string? line; + string lastKey = ""; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + logger.AddToStdOut(line); + if (line.Length == 0) break; // blank line separates package records + + var colonIdx = line.IndexOf(" : ", StringComparison.Ordinal); + if (colonIdx <= 0) continue; + + var key = line[..colonIdx].Trim(); + var value = line[(colonIdx + 3)..].Trim(); + if (value == "None" || value.Length == 0) continue; + + if (key.Length == 0) + { + // Continuation line — append to whatever field was active + switch (lastKey) + { + case "Depends On": + foreach (var dep in value.Split(" ", StringSplitOptions.RemoveEmptyEntries)) + { + var depName = dep.Split(new[] { '>', '<', '=', ':' })[0].Trim(); + if (depName.Length > 0) + details.Dependencies.Add(new() { Name = depName, Version = "", Mandatory = true }); + } + break; + case "Optional Deps": + foreach (var dep in value.Split(" ", StringSplitOptions.RemoveEmptyEntries)) + { + var depName = dep.Split(':')[0].Trim(); + if (depName.Length > 0) + details.Dependencies.Add(new() { Name = depName, Version = "", Mandatory = false }); + } + break; + } + continue; + } + + lastKey = key; + switch (key) + { + case "URL": + if (Uri.TryCreate(value, UriKind.Absolute, out var url)) + details.HomepageUrl = url; + break; + case "Description": + details.Description = value; + break; + case "Licenses": + details.License = value; + break; + case "Packager": + details.Publisher = value; + break; + case "Download Size": + details.InstallerSize = ParsePacmanSize(value); + break; + case "Depends On": + details.Dependencies.Clear(); + foreach (var dep in value.Split(" ", StringSplitOptions.RemoveEmptyEntries)) + { + var depName = dep.Split(new[] { '>', '<', '=', ':' })[0].Trim(); + if (depName.Length > 0) + details.Dependencies.Add(new() { Name = depName, Version = "", Mandatory = true }); + } + break; + case "Optional Deps": + foreach (var dep in value.Split(" ", StringSplitOptions.RemoveEmptyEntries)) + { + var depName = dep.Split(':')[0].Trim(); + if (depName.Length > 0) + details.Dependencies.Add(new() { Name = depName, Version = "", Mandatory = false }); + } + break; + } + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + } + + protected override CacheableIcon? GetIcon_UnSafe(IPackage package) + => throw new NotImplementedException(); + + protected override IReadOnlyList GetScreenshots_UnSafe(IPackage package) + => throw new NotImplementedException(); + + protected override string? GetInstallLocation_UnSafe(IPackage package) + { + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Manager.Status.ExecutablePath, + Arguments = $"-Ql {package.Id}", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + p.Start(); + + // Output format: " " — find the first directory entry. + // Must drain stdout fully before WaitForExit to avoid a pipe-full deadlock + // on packages with thousands of file entries (e.g. linux-firmware). + string? result = null; + string? line; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + if (result is not null) continue; + var spaceIdx = line.IndexOf(' '); + if (spaceIdx < 0) continue; + var path = line[(spaceIdx + 1)..].Trim(); + if (Directory.Exists(path)) + result = path; + } + + p.StandardError.ReadToEnd(); + p.WaitForExit(); + return result; + } + + protected override IReadOnlyList GetInstallableVersions_UnSafe(IPackage package) + => throw new InvalidOperationException("Pacman does not support installing arbitrary versions"); + + private static long ParsePacmanSize(string value) + { + // Format: "2.34 MiB", "234.56 KiB", "1.20 GiB" + var parts = value.Split(' '); + if (parts.Length < 2 || !double.TryParse(parts[0], + System.Globalization.NumberStyles.Any, + System.Globalization.CultureInfo.InvariantCulture, out var num)) + return 0; + + return parts[1] switch + { + "KiB" or "kB" => (long)(num * 1024), + "MiB" or "MB" => (long)(num * 1024 * 1024), + "GiB" or "GB" => (long)(num * 1024 * 1024 * 1024), + _ => (long)num, + }; + } +} diff --git a/src/UniGetUI.PackageEngine.Managers.Pacman/Helpers/PacmanPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.Pacman/Helpers/PacmanPkgOperationHelper.cs new file mode 100644 index 0000000000..912f2fb0c9 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Pacman/Helpers/PacmanPkgOperationHelper.cs @@ -0,0 +1,53 @@ +using UniGetUI.PackageEngine.Classes.Manager.BaseProviders; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.Serializable; + +namespace UniGetUI.PackageEngine.Managers.PacmanManager; + +internal sealed class PacmanPkgOperationHelper : BasePkgOperationHelper +{ + public PacmanPkgOperationHelper(Pacman manager) + : base(manager) { } + + protected override IReadOnlyList _getOperationParameters( + IPackage package, + InstallOptions options, + OperationType operation) + { + // pacman always requires root — force elevation via InstallOptions (reference type, persists) + options.RunAsAdministrator = true; + + List parameters = + [ + operation switch + { + OperationType.Install => Manager.Properties.InstallVerb, + OperationType.Update => Manager.Properties.UpdateVerb, + OperationType.Uninstall => Manager.Properties.UninstallVerb, + _ => throw new InvalidDataException("Invalid package operation"), + }, + "--noconfirm", + package.Id, + ]; + + parameters.AddRange( + operation switch + { + OperationType.Update => options.CustomParameters_Update, + OperationType.Uninstall => options.CustomParameters_Uninstall, + _ => options.CustomParameters_Install, + }); + + return parameters; + } + + protected override OperationVeredict _getOperationResult( + IPackage package, + OperationType operation, + IReadOnlyList processOutput, + int returnCode) + { + return returnCode == 0 ? OperationVeredict.Success : OperationVeredict.Failure; + } +} diff --git a/src/UniGetUI.PackageEngine.Managers.Pacman/Pacman.cs b/src/UniGetUI.PackageEngine.Managers.Pacman/Pacman.cs new file mode 100644 index 0000000000..862166710d --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Pacman/Pacman.cs @@ -0,0 +1,255 @@ +using System.Diagnostics; +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.Interfaces; +using UniGetUI.PackageEngine.ManagerClasses.Classes; +using UniGetUI.PackageEngine.ManagerClasses.Manager; +using UniGetUI.PackageEngine.PackageClasses; +using UniGetUI.PackageEngine.Structs; + +namespace UniGetUI.PackageEngine.Managers.PacmanManager; + +public class Pacman : PackageManager +{ + public Pacman() + { + Dependencies = []; + + Capabilities = new ManagerCapabilities + { + CanRunAsAdmin = true, + CanSkipIntegrityChecks = false, + SupportsCustomSources = false, + SupportsProxy = ProxySupport.No, + SupportsProxyAuth = false, + }; + + Properties = new ManagerProperties + { + Name = "Pacman", + Description = CoreTools.Translate( + "The default package manager for Arch Linux and its derivatives.
Contains: Arch Linux packages" + ), + IconId = IconType.Pacman, + ColorIconId = "pacman", + ExecutableFriendlyName = "pacman", + InstallVerb = "-S", + UpdateVerb = "-S", + UninstallVerb = "-Rs", + DefaultSource = new ManagerSource(this, "arch", new Uri("https://archlinux.org/packages/")), + KnownSources = [new ManagerSource(this, "arch", new Uri("https://archlinux.org/packages/"))], + }; + + DetailsHelper = new PacmanPkgDetailsHelper(this); + OperationHelper = new PacmanPkgOperationHelper(this); + } + + // ── Executable discovery ─────────────────────────────────────────────── + + public override IReadOnlyList FindCandidateExecutableFiles() + { + var candidates = new List(CoreTools.WhichMultiple("pacman")); + foreach (var path in new[] { "/usr/bin/pacman", "/usr/local/bin/pacman" }) + { + if (File.Exists(path) && !candidates.Contains(path)) + candidates.Add(path); + } + return candidates; + } + + protected override void _loadManagerExecutableFile( + out bool found, + out string path, + out string callArguments) + { + (found, path) = GetExecutableFile(); + callArguments = ""; + } + + protected override void _loadManagerVersion(out string version) + { + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = "-Q pacman", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + p.Start(); + // First line: "pacman X.Y.Z-N" + var line = p.StandardOutput.ReadLine()?.Trim() ?? ""; + var parts = line.Split(' '); + version = parts.Length >= 2 ? parts[1] : line; + p.StandardOutput.ReadToEnd(); + p.StandardError.ReadToEnd(); + p.WaitForExit(); + } + + // ── Index refresh ────────────────────────────────────────────────────── + + public override void RefreshPackageIndexes() + { + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = "-Sy", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.RefreshIndexes, p); + p.Start(); + logger.AddToStdOut(p.StandardOutput.ReadToEnd()); + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + } + + // ── Package listing ──────────────────────────────────────────────────── + + protected override IReadOnlyList FindPackages_UnSafe(string query) + { + var packages = new List(); + + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = $"-Ss {query}", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages, p); + p.Start(); + + // Output format: "/ [groups]\n " + // Name lines start at column 0; description lines are indented. + string? line; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + logger.AddToStdOut(line); + if (line.Length == 0 || line.StartsWith(' ')) continue; + + var slashIdx = line.IndexOf('/'); + if (slashIdx < 0) continue; + + var afterSlash = line[(slashIdx + 1)..]; + var spaceIdx = afterSlash.IndexOf(' '); + if (spaceIdx <= 0) continue; + + var id = afterSlash[..spaceIdx]; + packages.Add(new Package( + CoreTools.FormatAsName(id), + id, + CoreTools.Translate("Latest"), + Properties.DefaultSource!, + this)); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + // pacman -Ss exits 1 when no packages match the query — not an error + logger.Close(p.ExitCode == 1 && packages.Count == 0 ? 0 : p.ExitCode); + return packages; + } + + protected override IReadOnlyList GetInstalledPackages_UnSafe() + { + var packages = new List(); + + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = "-Q", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.ListInstalledPackages, p); + p.Start(); + + // Output format: " " + string? line; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + logger.AddToStdOut(line); + var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 2) continue; + + packages.Add(new Package( + CoreTools.FormatAsName(parts[0]), + parts[0], + parts[1], + Properties.DefaultSource!, + this)); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + return packages; + } + + protected override IReadOnlyList GetAvailableUpdates_UnSafe() + { + var packages = new List(); + + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = "-Qu", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.ListUpdates, p); + p.Start(); + + // Output format: " -> " + string? line; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + logger.AddToStdOut(line); + var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 4 || parts[2] != "->") continue; + + packages.Add(new Package( + CoreTools.FormatAsName(parts[0]), + parts[0], + parts[1], + parts[3], + Properties.DefaultSource!, + this)); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + // pacman -Qu exits 1 when there are no upgradable packages — not an error + logger.Close(p.ExitCode == 1 && packages.Count == 0 ? 0 : p.ExitCode); + return packages; + } +} diff --git a/src/UniGetUI.PackageEngine.Managers.Pacman/UniGetUI.PackageEngine.Managers.Pacman.csproj b/src/UniGetUI.PackageEngine.Managers.Pacman/UniGetUI.PackageEngine.Managers.Pacman.csproj new file mode 100644 index 0000000000..00d99aa0e4 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Pacman/UniGetUI.PackageEngine.Managers.Pacman.csproj @@ -0,0 +1,23 @@ + + + $(SharedTargetFrameworks) + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs b/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs index e3e86131fa..cb5aa15dda 100644 --- a/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs +++ b/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs @@ -114,7 +114,8 @@ protected sealed override void PrepareProcessStartInfo() { IsAdmin = true; if ( - Settings.Get(Settings.K.DoCacheAdminRights) + OperatingSystem.IsLinux() + || Settings.Get(Settings.K.DoCacheAdminRights) || Settings.Get(Settings.K.DoCacheAdminRightsForBatches) ) { @@ -123,7 +124,7 @@ protected sealed override void PrepareProcessStartInfo() FileName = CoreData.ElevatorPath; Arguments = - $"\"{Package.Manager.Status.ExecutablePath}\" {Package.Manager.Status.ExecutableCallArgs} {operation_args}"; + $"{CoreData.ElevatorArgs} \"{Package.Manager.Status.ExecutablePath}\" {Package.Manager.Status.ExecutableCallArgs} {operation_args}".TrimStart(); } else { diff --git a/src/UniGetUI.PackageEngine.Operations/SourceOperations.cs b/src/UniGetUI.PackageEngine.Operations/SourceOperations.cs index 65954aa182..7987f01ef6 100644 --- a/src/UniGetUI.PackageEngine.Operations/SourceOperations.cs +++ b/src/UniGetUI.PackageEngine.Operations/SourceOperations.cs @@ -122,7 +122,8 @@ protected override void PrepareProcessStartInfo() if (RequiresAdminRights()) { if ( - Settings.Get(Settings.K.DoCacheAdminRights) + OperatingSystem.IsLinux() + || Settings.Get(Settings.K.DoCacheAdminRights) || Settings.Get(Settings.K.DoCacheAdminRightsForBatches) ) RequestCachingOfUACPrompt(); @@ -133,10 +134,10 @@ protected override void PrepareProcessStartInfo() admin = true; process.StartInfo.FileName = CoreData.ElevatorPath; process.StartInfo.Arguments = - $"\"{exePath}\" " + ($"{CoreData.ElevatorArgs} \"{exePath}\" " + Source.Manager.Status.ExecutableCallArgs + " " - + string.Join(" ", Source.Manager.SourcesHelper.GetAddSourceParameters(Source)); + + string.Join(" ", Source.Manager.SourcesHelper.GetAddSourceParameters(Source))).TrimStart(); } else { @@ -227,7 +228,8 @@ protected override void PrepareProcessStartInfo() if (RequiresAdminRights()) { if ( - Settings.Get(Settings.K.DoCacheAdminRights) + OperatingSystem.IsLinux() + || Settings.Get(Settings.K.DoCacheAdminRights) || Settings.Get(Settings.K.DoCacheAdminRightsForBatches) ) RequestCachingOfUACPrompt(); @@ -238,13 +240,13 @@ protected override void PrepareProcessStartInfo() admin = true; process.StartInfo.FileName = CoreData.ElevatorPath; process.StartInfo.Arguments = - $"\"{exePath}\" " + ($"{CoreData.ElevatorArgs} \"{exePath}\" " + Source.Manager.Status.ExecutableCallArgs + " " + string.Join( " ", Source.Manager.SourcesHelper.GetRemoveSourceParameters(Source) - ); + )).TrimStart(); } else { diff --git a/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs b/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs index dbe0d19ef9..cfb219ce8e 100644 --- a/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs +++ b/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs @@ -15,7 +15,10 @@ using UniGetUI.PackageEngine.Managers.ScoopManager; using UniGetUI.PackageEngine.Managers.WingetManager; #else +using UniGetUI.PackageEngine.Managers.AptManager; +using UniGetUI.PackageEngine.Managers.DnfManager; using UniGetUI.PackageEngine.Managers.HomebrewManager; +using UniGetUI.PackageEngine.Managers.PacmanManager; #endif namespace UniGetUI.PackageEngine @@ -41,6 +44,9 @@ public static class PEInterface public static readonly Cargo Cargo = new(); public static readonly Vcpkg Vcpkg = new(); #if !WINDOWS + public static readonly Apt Apt = new(); + public static readonly Dnf Dnf = new(); + public static readonly Pacman Pacman = new(); public static readonly Homebrew Homebrew = new(); #endif @@ -54,6 +60,18 @@ private static IPackageManager[] CreateManagers() managers.Add(PowerShell); #else managers.Insert(0, Homebrew); + if (OperatingSystem.IsLinux()) + { + var families = ReadLinuxDistroFamilies(); + // If /etc/os-release is unreadable, include both as a safe fallback. + bool unknown = families.Count == 0; + if (unknown || families.Contains("debian") || families.Contains("ubuntu")) + managers.Add(Apt); + if (unknown || families.Contains("fedora") || families.Contains("rhel") || families.Contains("centos")) + managers.Add(Dnf); + if (unknown || families.Contains("arch")) + managers.Add(Pacman); + } #endif return [.. managers]; } @@ -97,6 +115,28 @@ public static void LoadManagers() Logger.Error(ex); } } + +#if !WINDOWS + [System.Runtime.Versioning.SupportedOSPlatform("linux")] + private static HashSet ReadLinuxDistroFamilies() + { + var families = new HashSet(StringComparer.OrdinalIgnoreCase); + try + { + foreach (var line in File.ReadLines("/etc/os-release")) + { + if (!line.StartsWith("ID=", StringComparison.Ordinal) && + !line.StartsWith("ID_LIKE=", StringComparison.Ordinal)) + continue; + var value = line[(line.IndexOf('=') + 1)..].Trim('"'); + foreach (var token in value.Split(' ', StringSplitOptions.RemoveEmptyEntries)) + families.Add(token); + } + } + catch { /* /etc/os-release not readable — caller will use fallback */ } + return families; + } +#endif } public class PackageBundlesLoader_I : PackageBundlesLoader diff --git a/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj b/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj index dc9978ea57..102f07b342 100644 --- a/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj +++ b/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj @@ -31,6 +31,9 @@ + + + diff --git a/src/UniGetUI.sln b/src/UniGetUI.sln index d64b90697c..a49ec88962 100644 --- a/src/UniGetUI.sln +++ b/src/UniGetUI.sln @@ -59,6 +59,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UniGetUI.PackageEngine.Mana EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UniGetUI.PackageEngine.Managers.Homebrew", "UniGetUI.PackageEngine.Managers.Homebrew\UniGetUI.PackageEngine.Managers.Homebrew.csproj", "{A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniGetUI.PackageEngine.Managers.Apt", "UniGetUI.PackageEngine.Managers.Apt\UniGetUI.PackageEngine.Managers.Apt.csproj", "{D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniGetUI.PackageEngine.Managers.Dnf", "UniGetUI.PackageEngine.Managers.Dnf\UniGetUI.PackageEngine.Managers.Dnf.csproj", "{E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniGetUI.PackageEngine.Managers.Pacman", "UniGetUI.PackageEngine.Managers.Pacman\UniGetUI.PackageEngine.Managers.Pacman.csproj", "{F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UniGetUI.PackageEngine.Managers.PowerShell", "UniGetUI.PackageEngine.Managers.PowerShell\UniGetUI.PackageEngine.Managers.PowerShell.csproj", "{E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UniGetUI.PackageEngine.Managers.Chocolatey", "UniGetUI.PackageEngine.Managers.Chocolatey\UniGetUI.PackageEngine.Managers.Chocolatey.csproj", "{57D094C1-6913-46BF-A657-84A5F46D4EE7}" @@ -481,6 +487,54 @@ Global {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Release|Any CPU.Build.0 = Release|Any CPU {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Release|x86.ActiveCfg = Release|Any CPU {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Release|x86.Build.0 = Release|Any CPU + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Debug|x64.ActiveCfg = Debug|x64 + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Debug|x64.Build.0 = Debug|x64 + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Debug|arm64.ActiveCfg = Debug|arm64 + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Debug|arm64.Build.0 = Debug|arm64 + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Debug|x86.ActiveCfg = Debug|Any CPU + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Debug|x86.Build.0 = Debug|Any CPU + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Release|x64.ActiveCfg = Release|x64 + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Release|x64.Build.0 = Release|x64 + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Release|arm64.ActiveCfg = Release|arm64 + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Release|arm64.Build.0 = Release|arm64 + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Release|Any CPU.Build.0 = Release|Any CPU + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Release|x86.ActiveCfg = Release|Any CPU + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A}.Release|x86.Build.0 = Release|Any CPU + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Debug|x64.ActiveCfg = Debug|x64 + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Debug|x64.Build.0 = Debug|x64 + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Debug|arm64.ActiveCfg = Debug|arm64 + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Debug|arm64.Build.0 = Debug|arm64 + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Debug|x86.ActiveCfg = Debug|Any CPU + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Debug|x86.Build.0 = Debug|Any CPU + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Release|x64.ActiveCfg = Release|x64 + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Release|x64.Build.0 = Release|x64 + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Release|arm64.ActiveCfg = Release|arm64 + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Release|arm64.Build.0 = Release|arm64 + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Release|Any CPU.Build.0 = Release|Any CPU + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Release|x86.ActiveCfg = Release|Any CPU + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B}.Release|x86.Build.0 = Release|Any CPU + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Debug|x64.ActiveCfg = Debug|x64 + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Debug|x64.Build.0 = Debug|x64 + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Debug|arm64.ActiveCfg = Debug|arm64 + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Debug|arm64.Build.0 = Debug|arm64 + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Debug|x86.ActiveCfg = Debug|Any CPU + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Debug|x86.Build.0 = Debug|Any CPU + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Release|x64.ActiveCfg = Release|x64 + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Release|x64.Build.0 = Release|x64 + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Release|arm64.ActiveCfg = Release|arm64 + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Release|arm64.Build.0 = Release|arm64 + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Release|Any CPU.Build.0 = Release|Any CPU + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Release|x86.ActiveCfg = Release|Any CPU + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C}.Release|x86.Build.0 = Release|Any CPU {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Debug|x64.ActiveCfg = Debug|x64 {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Debug|x64.Build.0 = Debug|x64 {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Debug|arm64.ActiveCfg = Debug|arm64 @@ -877,6 +931,9 @@ Global {D47CC16E-466B-4D58-A8FC-ECAE5C9606FC} = {95168D0B-1B2C-4295-B6D4-D5BAF781B9FA} {1143176D-B7F0-477C-90BB-72289068D927} = {95168D0B-1B2C-4295-B6D4-D5BAF781B9FA} {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6} = {7940E867-EEBA-4AFD-9904-1536F003239C} + {D1E2F3A4-B5C6-7D8E-9F0A-1B2C3D4E5F6A} = {95168D0B-1B2C-4295-B6D4-D5BAF781B9FA} + {E2F3A4B5-C6D7-8E9F-0A1B-2C3D4E5F6A7B} = {95168D0B-1B2C-4295-B6D4-D5BAF781B9FA} + {F3A4B5C6-D7E8-9F0A-1B2C-3D4E5F6A7B8C} = {95168D0B-1B2C-4295-B6D4-D5BAF781B9FA} {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD} = {95168D0B-1B2C-4295-B6D4-D5BAF781B9FA} {57D094C1-6913-46BF-A657-84A5F46D4EE7} = {95168D0B-1B2C-4295-B6D4-D5BAF781B9FA} {740E2894-903D-4B94-9C32-B630593BEB16} = {95168D0B-1B2C-4295-B6D4-D5BAF781B9FA} diff --git a/src/UniGetUI/Assets/Symbols/apt.svg b/src/UniGetUI/Assets/Symbols/apt.svg new file mode 100644 index 0000000000..bc759bbaca --- /dev/null +++ b/src/UniGetUI/Assets/Symbols/apt.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/UniGetUI/Assets/Symbols/dnf.svg b/src/UniGetUI/Assets/Symbols/dnf.svg new file mode 100644 index 0000000000..105386fc9e --- /dev/null +++ b/src/UniGetUI/Assets/Symbols/dnf.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/UniGetUI/Assets/Symbols/pacman.svg b/src/UniGetUI/Assets/Symbols/pacman.svg new file mode 100644 index 0000000000..c5ab7ad8da --- /dev/null +++ b/src/UniGetUI/Assets/Symbols/pacman.svg @@ -0,0 +1,4 @@ + + + +