Skip to content

Commit 499cc03

Browse files
authored
Add APT, DNF, and Pacman package managers for Linux (#4606)
* Add "apt" and "dnf" for linux * added pacman for arch linux
1 parent 1dfb890 commit 499cc03

File tree

28 files changed

+1843
-37
lines changed

28 files changed

+1843
-37
lines changed

src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ private static async Task LoadElevatorAsync()
140140
return;
141141
}
142142

143+
if (OperatingSystem.IsLinux())
144+
{
145+
await LoadLinuxElevatorAsync();
146+
return;
147+
}
148+
143149
if (SecureSettings.Get(SecureSettings.K.ForceUserGSudo))
144150
{
145151
var res = await CoreTools.WhichAsync("gsudo.exe");
@@ -171,6 +177,72 @@ private static async Task LoadElevatorAsync()
171177
}
172178
}
173179

180+
[System.Runtime.Versioning.SupportedOSPlatform("linux")]
181+
private static async Task LoadLinuxElevatorAsync()
182+
{
183+
// Prefer sudo over pkexec: sudo caches credentials on disk (per user, not per
184+
// process), so the user is only prompted once per ~15-minute window regardless
185+
// of how many packages are installed. pkexec prompts on every single invocation
186+
// because polkit ties its authorization cache to the calling process PID.
187+
var results = await Task.WhenAll(
188+
CoreTools.WhichAsync("sudo"),
189+
CoreTools.WhichAsync("pkexec"),
190+
CoreTools.WhichAsync("zenity"));
191+
var (sudoFound, sudoPath) = results[0];
192+
var (pkexecFound, pkexecPath) = results[1];
193+
var (zenityFound, zenityPath) = results[2];
194+
195+
if (sudoFound)
196+
{
197+
// Find a graphical askpass helper so sudo can prompt without a terminal.
198+
// Most DEs (KDE, XFCE, ...) pre-set SSH_ASKPASS to their native tool;
199+
// GNOME doesn't, so we fall back to zenity with a small wrapper script
200+
// (zenity --password ignores positional args, so it needs the wrapper
201+
// to forward the prompt text via --text="$1").
202+
string? askpass = null;
203+
var envAskpass = Environment.GetEnvironmentVariable("SSH_ASKPASS");
204+
if (!string.IsNullOrEmpty(envAskpass) && File.Exists(envAskpass))
205+
askpass = envAskpass;
206+
else if (zenityFound)
207+
{
208+
askpass = Path.Join(CoreData.UniGetUIDataDirectory, "linux-askpass.sh");
209+
await File.WriteAllTextAsync(askpass,
210+
$"#!/bin/sh\n\"{zenityPath}\" --password --title=\"UniGetUI\" --text=\"$1\"\n");
211+
File.SetUnixFileMode(askpass,
212+
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
213+
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
214+
UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
215+
}
216+
217+
if (askpass != null)
218+
{
219+
Environment.SetEnvironmentVariable("SUDO_ASKPASS", askpass);
220+
CoreData.ElevatorPath = sudoPath;
221+
CoreData.ElevatorArgs = "-A";
222+
Logger.Debug($"Using sudo -A with askpass '{askpass}'");
223+
return;
224+
}
225+
}
226+
227+
// Fall back to pkexec when no usable sudo+askpass combination is found.
228+
// pkexec handles its own graphical prompt via polkit but prompts every invocation.
229+
if (pkexecFound)
230+
{
231+
CoreData.ElevatorPath = pkexecPath;
232+
Logger.Warn($"Using pkexec at {pkexecPath} (prompts on every operation)");
233+
return;
234+
}
235+
236+
if (sudoFound)
237+
{
238+
CoreData.ElevatorPath = sudoPath;
239+
Logger.Warn($"Falling back to sudo without graphical askpass at {sudoPath}");
240+
return;
241+
}
242+
243+
Logger.Warn("No elevation tool found (pkexec/sudo). Admin operations will fail.");
244+
}
245+
174246
/// <summary>
175247
/// Checks all ready package managers for missing dependencies.
176248
/// Returns the list of dependencies whose installation was not skipped by the user.

src/UniGetUI.Avalonia/ViewModels/DialogPages/ManageIgnoredUpdatesViewModel.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,9 @@ private async Task ResetAll()
108108
await entry.RemoveAsync();
109109
}
110110

111-
private static string ResolveManagerIcon(string managerKey) =>
112-
(managerKey switch
111+
private static string ResolveManagerIcon(string managerKey)
112+
{
113+
string name = managerKey switch
113114
{
114115
"winget" => "winget",
115116
"scoop" => "scoop",
@@ -123,10 +124,13 @@ private static string ResolveManagerIcon(string managerKey) =>
123124
"steam" => "steam",
124125
"gog" => "gog",
125126
"uplay" => "uplay",
127+
"apt" => "apt",
128+
"dnf" => "dnf",
129+
"pacman" => "pacman",
126130
_ => "ms_store",
127-
}) is var name
128-
? $"avares://UniGetUI.Avalonia/Assets/Symbols/{name}.svg"
129-
: $"avares://UniGetUI.Avalonia/Assets/Symbols/ms_store.svg";
131+
};
132+
return $"avares://UniGetUI.Avalonia/Assets/Symbols/{name}.svg";
133+
}
130134
}
131135

132136
public partial class IgnoredPackageEntryViewModel : ObservableObject

src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsPageButton.cs

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public IconType Icon
2828
{
2929
set => HeaderIcon = new SvgIcon
3030
{
31-
Path = $"avares://UniGetUI.Avalonia/Assets/Symbols/{IconTypeToName(value)}.svg",
31+
Path = IconTypeToPath(value),
3232
Width = 24,
3333
Height = 24,
3434
};
@@ -40,21 +40,25 @@ public SettingsPageButton()
4040
IsClickEnabled = true;
4141
}
4242

43-
private static string IconTypeToName(IconType icon) => icon switch
43+
private static string IconTypeToPath(IconType icon)
4444
{
45-
IconType.Chocolatey => "choco",
46-
IconType.Package => "package",
47-
IconType.UAC => "uac",
48-
IconType.Update => "update",
49-
IconType.Help => "help",
50-
IconType.Console => "console",
51-
IconType.Checksum => "checksum",
52-
IconType.Download => "download",
53-
IconType.Settings => "settings",
54-
IconType.SaveAs => "save_as",
55-
IconType.OpenFolder => "open_folder",
56-
IconType.Experimental => "experimental",
57-
IconType.ClipboardList => "clipboard_list",
58-
_ => icon.ToString().ToLower(),
59-
};
45+
string name = icon switch
46+
{
47+
IconType.Chocolatey => "choco",
48+
IconType.Package => "package",
49+
IconType.UAC => "uac",
50+
IconType.Update => "update",
51+
IconType.Help => "help",
52+
IconType.Console => "console",
53+
IconType.Checksum => "checksum",
54+
IconType.Download => "download",
55+
IconType.Settings => "settings",
56+
IconType.SaveAs => "save_as",
57+
IconType.OpenFolder => "open_folder",
58+
IconType.Experimental => "experimental",
59+
IconType.ClipboardList => "clipboard_list",
60+
_ => icon.ToString().ToLower(),
61+
};
62+
return $"avares://UniGetUI.Avalonia/Assets/Symbols/{name}.svg";
63+
}
6064
}

src/UniGetUI.Core.Data/CoreData.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,12 @@ public static string UniGetUIExecutableFile
370370

371371
public static string ElevatorPath = "";
372372

373+
/// <summary>
374+
/// Extra arguments to insert between the elevator binary and the elevated command.
375+
/// For example, "-A" when using sudo with an askpass helper on Linux.
376+
/// </summary>
377+
public static string ElevatorArgs = "";
378+
373379
/// <summary>
374380
/// This method will return the most appropriate data directory.
375381
/// If the new directory exists, it will be used.

src/UniGetUI.Core.LanguageEngine/Assets/Data/TranslatedPercentages.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
"bn": "100%",
77
"da": "100%",
88
"el": "100%",
9-
"eo": "100%",
9+
"eo": "99%",
1010
"es": "100%",
1111
"es-MX": "100%",
1212
"et": "100%",
1313
"fa": "100%",
1414
"fil": "100%",
15-
"gl": "100%",
15+
"gl": "99%",
1616
"gu": "100%",
1717
"he": "100%",
1818
"hi": "100%",
@@ -23,10 +23,10 @@
2323
"ka": "100%",
2424
"kn": "100%",
2525
"ko": "100%",
26-
"ku": "100%",
26+
"ku": "99%",
2727
"lt": "100%",
2828
"mk": "100%",
29-
"mr": "100%",
29+
"mr": "99%",
3030
"nb": "100%",
3131
"nn": "100%",
3232
"pt_PT": "100%",
@@ -58,4 +58,4 @@
5858
"zh_CN": "100%",
5959
"zh_TW": "100%",
6060
"en": "100%"
61-
}
61+
}

src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,9 @@
502502
"A repository full of tools and executables designed with Microsoft's .NET ecosystem in mind.<br>Contains: <b>.NET related tools and scripts</b>": "A repository full of tools and executables designed with Microsoft's .NET ecosystem in mind.<br>Contains: <b>.NET related tools and scripts</b>",
503503
"NuPkg (zipped manifest)": "NuPkg (zipped manifest)",
504504
"The Missing Package Manager for macOS (or Linux).<br>Contains: <b>Formulae, Casks</b>": "The Missing Package Manager for macOS (or Linux).<br>Contains: <b>Formulae, Casks</b>",
505+
"The default package manager for Debian/Ubuntu-based Linux distributions.<br>Contains: <b>Debian/Ubuntu packages</b>": "The default package manager for Debian/Ubuntu-based Linux distributions.<br>Contains: <b>Debian/Ubuntu packages</b>",
506+
"The default package manager for RHEL/Fedora-based Linux distributions.<br>Contains: <b>RPM packages</b>": "The default package manager for RHEL/Fedora-based Linux distributions.<br>Contains: <b>RPM packages</b>",
507+
"The default package manager for Arch Linux and its derivatives.<br>Contains: <b>Arch Linux packages</b>": "The default package manager for Arch Linux and its derivatives.<br>Contains: <b>Arch Linux packages</b>",
505508
"Node JS's package manager. Full of libraries and other utilities that orbit the javascript world<br>Contains: <b>Node javascript libraries and other related utilities</b>": "Node JS's package manager. Full of libraries and other utilities that orbit the javascript world<br>Contains: <b>Node javascript libraries and other related utilities</b>",
506509
"Python's library manager. Full of python libraries and other python-related utilities<br>Contains: <b>Python libraries and related utilities</b>": "Python's library manager. Full of python libraries and other python-related utilities<br>Contains: <b>Python libraries and related utilities</b>",
507510
"PowerShell's package manager. Find libraries and scripts to expand PowerShell capabilities<br>Contains: <b>Modules, Scripts, Cmdlets</b>": "PowerShell's package manager. Find libraries and scripts to expand PowerShell capabilities<br>Contains: <b>Modules, Scripts, Cmdlets</b>",

src/UniGetUI.Core.Tools/Tools.cs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -505,12 +505,29 @@ public static async Task CacheUACForCurrentProcess()
505505
{
506506
_isCaching = true;
507507
Logger.Info("Caching admin rights for process id " + Environment.ProcessId);
508+
509+
var elevatorName = Path.GetFileName(CoreData.ElevatorPath);
510+
511+
// pkexec prompts on every invocation and has no caching protocol.
512+
if (elevatorName == "pkexec")
513+
{
514+
_isCaching = false;
515+
return;
516+
}
517+
518+
// sudo: -v validates/extends the cached timestamp.
519+
// Prepend -A only when the SUDO_ASKPASS helper is configured.
520+
// gsudo / UniGetUI Elevator.exe: use the gsudo cache protocol.
521+
string cacheArgs = elevatorName == "sudo"
522+
? (CoreData.ElevatorArgs.Contains("-A") ? "-Av" : "-v")
523+
: "cache on --pid " + Environment.ProcessId + " -d 1";
524+
508525
using Process p = new()
509526
{
510527
StartInfo = new ProcessStartInfo
511528
{
512529
FileName = CoreData.ElevatorPath,
513-
Arguments = "cache on --pid " + Environment.ProcessId + " -d 1",
530+
Arguments = cacheArgs,
514531
UseShellExecute = false,
515532
RedirectStandardOutput = true,
516533
RedirectStandardError = true,
@@ -546,12 +563,27 @@ public static async Task ResetUACForCurrentProcess()
546563
Logger.Info(
547564
"Resetting administrator rights cache for process id " + Environment.ProcessId
548565
);
566+
567+
var elevatorName = Path.GetFileName(CoreData.ElevatorPath);
568+
569+
// pkexec prompts on every invocation and has no caching protocol.
570+
if (elevatorName == "pkexec")
571+
{
572+
return;
573+
}
574+
575+
// sudo: -K removes all cached timestamps.
576+
// gsudo / UniGetUI Elevator.exe: use the gsudo cache protocol.
577+
string resetArgs = elevatorName == "sudo"
578+
? "-K"
579+
: "cache off --pid " + Environment.ProcessId;
580+
549581
using Process p = new()
550582
{
551583
StartInfo = new ProcessStartInfo
552584
{
553585
FileName = CoreData.ElevatorPath,
554-
Arguments = "cache off --pid " + Environment.ProcessId,
586+
Arguments = resetArgs,
555587
UseShellExecute = false,
556588
RedirectStandardOutput = true,
557589
RedirectStandardError = true,

src/UniGetUI.Interface.Enums/Enums.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ public enum IconType
8585
Rust = '\uE941',
8686
Vcpkg = '\uE942',
8787
Homebrew = '\uE943',
88+
Apt = '\uE944',
89+
Dnf = '\uE945',
90+
Pacman = '\uE946',
8891
}
8992

9093
public class NotificationArguments

0 commit comments

Comments
 (0)