diff --git a/src/UniGetUI.Avalonia.slnx b/src/UniGetUI.Avalonia.slnx index 42647b832..e65b0bdb5 100644 --- a/src/UniGetUI.Avalonia.slnx +++ b/src/UniGetUI.Avalonia.slnx @@ -177,6 +177,12 @@ + + + + + + diff --git a/src/UniGetUI.Interface.Enums/Enums.cs b/src/UniGetUI.Interface.Enums/Enums.cs index 0ca9570f7..7d813080c 100644 --- a/src/UniGetUI.Interface.Enums/Enums.cs +++ b/src/UniGetUI.Interface.Enums/Enums.cs @@ -89,6 +89,7 @@ public enum IconType Dnf = '\uE945', Pacman = '\uE946', Snap = '\uE947', + Flatpak = '\uE948', } public class NotificationArguments diff --git a/src/UniGetUI.PackageEngine.Managers.Flatpak/Flatpak.cs b/src/UniGetUI.PackageEngine.Managers.Flatpak/Flatpak.cs new file mode 100644 index 000000000..39c4fdea8 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Flatpak/Flatpak.cs @@ -0,0 +1,224 @@ +using System.Diagnostics; +using UniGetUI.Core.Tools; +using UniGetUI.Interface.Enums; +using UniGetUI.PackageEngine.Classes.Manager; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.ManagerClasses.Classes; +using UniGetUI.PackageEngine.ManagerClasses.Manager; +using UniGetUI.PackageEngine.PackageClasses; + +namespace UniGetUI.PackageEngine.Managers.FlatpakManager; + +public partial class Flatpak : PackageManager +{ + private static readonly string[] FLATPAK_PATHS = + [ + "/usr/bin/flatpak", + "/usr/local/bin/flatpak", + ]; + + public Flatpak() + { + Dependencies = []; + + var flathubSource = new ManagerSource(this, "flathub", new Uri("https://dl.flathub.org/repo/")); + + Capabilities = new ManagerCapabilities + { + CanRunAsAdmin = true, + CanSkipIntegrityChecks = true, + SupportsCustomSources = true, + Sources = new SourceCapabilities + { + KnowsPackageCount = false, + KnowsUpdateDate = false, + }, + SupportsProxy = ProxySupport.No, + SupportsProxyAuth = false, + }; + + Properties = new ManagerProperties + { + Name = "Flatpak", + Description = CoreTools.Translate( + "The universal Linux package manager for desktop applications.
Contains: Flatpak applications from configured remotes" + ), + IconId = IconType.Flatpak, + ColorIconId = "flatpak", + ExecutableFriendlyName = "flatpak", + InstallVerb = "install", + UpdateVerb = "update", + UninstallVerb = "uninstall", + DefaultSource = flathubSource, + KnownSources = [flathubSource], + }; + + SourcesHelper = new FlatpakSourceHelper(this); + DetailsHelper = new FlatpakPkgDetailsHelper(this); + OperationHelper = new FlatpakPkgOperationHelper(this); + } + + public override IReadOnlyList FindCandidateExecutableFiles() + { + var candidates = new List(CoreTools.WhichMultiple("flatpak")); + foreach (var path in FLATPAK_PATHS) + { + 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(); + var line = p.StandardOutput.ReadLine()?.Trim() ?? ""; + version = line.Replace("Flatpak ", "").Trim(); + p.StandardError.ReadToEnd(); + p.WaitForExit(); + } + + public override void RefreshPackageIndexes() + { + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = "update --appstream", + 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); + } + + protected override IReadOnlyList FindPackages_UnSafe(string query) + { + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = $"search {CoreTools.EnsureSafeQueryString(query)}", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + p.StartInfo.Environment["LANG"] = "C"; + p.StartInfo.Environment["LC_ALL"] = "C"; + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages, p); + p.Start(); + + List outputLines = []; + while (p.StandardOutput.ReadLine() is { } line) + { + logger.AddToStdOut(line); + outputLines.Add(line); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + + return ParseSearchResults(outputLines, DefaultSource, this); + } + + protected override IReadOnlyList GetInstalledPackages_UnSafe() + { + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = "list --app --columns=application,version,branch,origin,name", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + p.StartInfo.Environment["LANG"] = "C"; + p.StartInfo.Environment["LC_ALL"] = "C"; + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.ListInstalledPackages, p); + p.Start(); + + List outputLines = []; + while (p.StandardOutput.ReadLine() is { } line) + { + logger.AddToStdOut(line); + outputLines.Add(line); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + + return ParseInstalledPackages(outputLines, DefaultSource, this); + } + + protected override IReadOnlyList GetAvailableUpdates_UnSafe() + { + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = "remote-ls --updates --columns=application,version,branch,origin,name", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + + p.StartInfo.Environment["LANG"] = "C"; + p.StartInfo.Environment["LC_ALL"] = "C"; + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.ListUpdates, p); + + p.Start(); + + List outputLines = []; + while (p.StandardOutput.ReadLine() is { } line) + { + logger.AddToStdOut(line); + outputLines.Add(line); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + + return ParseAvailableUpdates(outputLines, DefaultSource, this, GetInstalledPackages_UnSafe()); + } +} diff --git a/src/UniGetUI.PackageEngine.Managers.Flatpak/FlatpakParsing.cs b/src/UniGetUI.PackageEngine.Managers.Flatpak/FlatpakParsing.cs new file mode 100644 index 000000000..dc2e6db33 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Flatpak/FlatpakParsing.cs @@ -0,0 +1,173 @@ +using System.Text.RegularExpressions; +using UniGetUI.Core.Tools; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.PackageClasses; + +namespace UniGetUI.PackageEngine.Managers.FlatpakManager; + +public partial class Flatpak +{ + [GeneratedRegex(@"[a-zA-Z0-9][-a-zA-Z0-9]*(\.[a-zA-Z0-9][-a-zA-Z0-9]*)+")] + private static partial Regex _searchAppIdRegex(); + + [GeneratedRegex(@"\s{2,}")] + private static partial Regex _multiSpaceRegex(); + + private static void ParseColumns(IReadOnlyList parts, out string appId, out string version, out string name) + { + appId = parts[0]; + version = parts[1]; + if (string.IsNullOrEmpty(version) && parts.Count > 2) + version = parts[2]; + name = parts[4]; + } + + public static IReadOnlyList ParseInstalledPackages( + IEnumerable outputLines, + IManagerSource source, + IPackageManager manager) + { + var packages = new List(); + + foreach (var line in outputLines) + { + var trimmed = line.Trim(); + if (string.IsNullOrEmpty(trimmed)) + { + continue; + } + + var parts = trimmed.Split('\t'); + if (parts.Length < 5) + { + continue; + } + + var appId = parts[0]; + var version = parts[1]; + if (string.IsNullOrEmpty(version)) + { + version = parts[2]; + } + + var name = parts[4]; + + packages.Add(new Package( + CoreTools.FormatAsName(name), + appId, + version, + source, + manager)); + } + + return packages; + } + + public static IReadOnlyList ParseAvailableUpdates( + IEnumerable outputLines, + IManagerSource source, + IPackageManager manager, + IReadOnlyList installedPackages) + { + var installedPackageMap = new Dictionary(); + foreach (var installedPackage in installedPackages) + { + installedPackageMap.TryAdd(installedPackage.Id, installedPackage); + } + + var packages = new List(); + + foreach (var line in outputLines) + { + var trimmed = line.Trim(); + if (string.IsNullOrEmpty(trimmed)) + { + continue; + } + + var parts = trimmed.Split('\t'); + if (parts.Length < 5) + { + continue; + } + + ParseColumns(parts, out var appId, out var newVersion, out var name); + + if (installedPackageMap.TryGetValue(appId, out var installedPackage)) + { + packages.Add(new Package( + CoreTools.FormatAsName(name), + appId, + installedPackage.VersionString, + newVersion, + source, + manager)); + } + } + + return packages; + } + + public static IReadOnlyList ParseSearchResults( + IEnumerable outputLines, + IManagerSource source, + IPackageManager manager) + { + var lines = outputLines.ToArray(); + if (lines.Length == 0) + { + return []; + } + + var packages = new List(); + + // Detect format: tab-separated (no header) vs space-padded (with header) + int startIndex = lines[0].Contains('\t') ? 0 : 1; + + for (int i = startIndex; i < lines.Length; i++) + { + var trimmed = lines[i].Trim(); + if (string.IsNullOrEmpty(trimmed)) + { + continue; + } + + string name; + string appId; + string version; + + var tabParts = trimmed.Split('\t'); + if (tabParts.Length >= 4) + { + name = tabParts[0]; + appId = tabParts[2]; + version = tabParts[3]; + } + else + { + var appIdMatch = _searchAppIdRegex().Match(trimmed); + if (!appIdMatch.Success) + { + continue; + } + + appId = appIdMatch.Value; + var before = trimmed[..appIdMatch.Index].TrimEnd(); + var after = trimmed[(appIdMatch.Index + appIdMatch.Length)..].Trim(); + + name = _multiSpaceRegex().Split(before)[0]; + var parts = after.Split(' ', StringSplitOptions.RemoveEmptyEntries); + version = parts.Length > 0 ? parts[0] : ""; + } + + packages.Add(new Package( + CoreTools.FormatAsName(name), + appId, + version, + source, + manager)); + } + + return packages; + } +} diff --git a/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakPkgDetailsHelper.cs b/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakPkgDetailsHelper.cs new file mode 100644 index 000000000..337d590b5 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakPkgDetailsHelper.cs @@ -0,0 +1,86 @@ +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.FlatpakManager; + +internal sealed class FlatpakPkgDetailsHelper : BasePkgDetailsHelper +{ + public FlatpakPkgDetailsHelper(Flatpak 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, + }, + }; + p.StartInfo.Environment["LANG"] = "C"; + p.StartInfo.Environment["LC_ALL"] = "C"; + + IProcessTaskLogger logger = Manager.TaskLogger.CreateNew( + LoggableTaskType.LoadPackageDetails, p); + p.Start(); + + while (p.StandardOutput.ReadLine() is { } line) + { + logger.AddToStdOut(line); + + var colonIdx = line.IndexOf(':', StringComparison.Ordinal); + if (colonIdx <= 0) continue; + + var key = line[..colonIdx].Trim().ToLowerInvariant(); + var value = line[(colonIdx + 1)..].Trim(); + + switch (key) + { + case "name": + break; + case "summary": + details.Description = value; + break; + case "license": + details.License = value; + break; + case "homepage": + if (Uri.TryCreate(value, UriKind.Absolute, out var homepage)) + details.HomepageUrl = homepage; + break; + case "origin": + details.Publisher = value; + break; + case "url": + if (Uri.TryCreate(value, UriKind.Absolute, out var url)) + details.ManifestUrl = url; + 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) + => Array.Empty(); + + protected override string? GetInstallLocation_UnSafe(IPackage package) + => $"/var/lib/flatpak/app/{package.Id}"; + + protected override IReadOnlyList GetInstallableVersions_UnSafe(IPackage package) + => throw new InvalidOperationException("Flatpak does not support installing arbitrary versions"); +} diff --git a/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakPkgOperationHelper.cs new file mode 100644 index 000000000..a60e7d7ab --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakPkgOperationHelper.cs @@ -0,0 +1,54 @@ +using UniGetUI.PackageEngine.Classes.Manager.BaseProviders; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.Serializable; + +namespace UniGetUI.PackageEngine.Managers.FlatpakManager; + +internal sealed class FlatpakPkgOperationHelper : BasePkgOperationHelper +{ + public FlatpakPkgOperationHelper(Flatpak manager) + : base(manager) { } + + protected override IReadOnlyList _getOperationParameters( + IPackage package, + InstallOptions options, + OperationType operation) + { + 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"), + }, + ]; + + parameters.Add("--noninteractive"); + parameters.Add("-y"); + parameters.Add(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.Flatpak/Helpers/FlatpakSourceHelper.cs b/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakSourceHelper.cs new file mode 100644 index 000000000..7336d4c2d --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakSourceHelper.cs @@ -0,0 +1,84 @@ +using System.Diagnostics; +using System.Text.RegularExpressions; +using UniGetUI.Core.Logging; +using UniGetUI.PackageEngine.Classes.Manager; +using UniGetUI.PackageEngine.Classes.Manager.Providers; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.ManagerClasses.Classes; + +namespace UniGetUI.PackageEngine.Managers.FlatpakManager; + +public partial class Flatpak +{ + [GeneratedRegex(@"^(\S+)\s+(https?://\S+)$")] + internal static partial Regex RemoteListLineRegex(); +} + +internal sealed class FlatpakSourceHelper : BaseSourceHelper +{ + public FlatpakSourceHelper(Flatpak manager) + : base(manager) { } + + protected override IReadOnlyList GetSources_UnSafe() + { + var sources = new List(); + + using var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Manager.Status.ExecutablePath, + Arguments = "remote-list --columns=name,url", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }, + }; + IProcessTaskLogger logger = Manager.TaskLogger.CreateNew(LoggableTaskType.ListSources, p); + p.Start(); + + string? line; + while ((line = p.StandardOutput.ReadLine()) is not null) + { + logger.AddToStdOut(line); + var match = Flatpak.RemoteListLineRegex().Match(line.Trim()); + if (!match.Success) + { + continue; + } + + var name = match.Groups[1].Value; + var url = match.Groups[2].Value; + + try + { + sources.Add(new ManagerSource(Manager, name, new Uri(url))); + } + catch (Exception ex) + { + Logger.Warn($"FlatpakSourceHelper: could not add remote '{name}': {ex.Message}"); + } + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + return sources; + } + + public override string[] GetAddSourceParameters(IManagerSource source) + => ["remote-add", "--if-not-exists", source.Name, source.Url.ToString()]; + + public override string[] GetRemoveSourceParameters(IManagerSource source) + => ["remote-delete", source.Name]; + + protected override OperationVeredict _getAddSourceOperationVeredict( + IManagerSource source, int ReturnCode, string[] Output) + => ReturnCode == 0 ? OperationVeredict.Success : OperationVeredict.Failure; + + protected override OperationVeredict _getRemoveSourceOperationVeredict( + IManagerSource source, int ReturnCode, string[] Output) + => ReturnCode == 0 ? OperationVeredict.Success : OperationVeredict.Failure; +} diff --git a/src/UniGetUI.PackageEngine.Managers.Flatpak/UniGetUI.PackageEngine.Managers.Flatpak.csproj b/src/UniGetUI.PackageEngine.Managers.Flatpak/UniGetUI.PackageEngine.Managers.Flatpak.csproj new file mode 100644 index 000000000..00d99aa0e --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Flatpak/UniGetUI.PackageEngine.Managers.Flatpak.csproj @@ -0,0 +1,23 @@ + + + $(SharedTargetFrameworks) + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs b/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs index fdf99b3ea..16af27c48 100644 --- a/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs +++ b/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs @@ -20,6 +20,7 @@ using UniGetUI.PackageEngine.Managers.HomebrewManager; using UniGetUI.PackageEngine.Managers.PacmanManager; using UniGetUI.PackageEngine.Managers.SnapManager; +using UniGetUI.PackageEngine.Managers.FlatpakManager; #endif namespace UniGetUI.PackageEngine @@ -50,6 +51,7 @@ public static class PEInterface public static readonly Pacman Pacman = new(); public static readonly Homebrew Homebrew = new(); public static readonly Snap Snap = new(); + public static readonly Flatpak Flatpak = new(); #endif public static readonly IPackageManager[] Managers = CreateManagers(); @@ -74,7 +76,10 @@ private static IPackageManager[] CreateManagers() if (unknown || families.Contains("arch")) managers.Add(Pacman); if (unknown || families.Contains("ubuntu") || families.Contains("debian") || families.Contains("fedora") || families.Contains("arch")) + { managers.Add(Snap); + managers.Add(Flatpak); + } } #endif return [.. managers]; diff --git a/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj b/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj index f77f54607..9a8df247c 100644 --- a/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj +++ b/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj @@ -36,6 +36,7 @@ + diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/installed-list.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/installed-list.txt new file mode 100644 index 000000000..31db72bb8 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/installed-list.txt @@ -0,0 +1,3 @@ +com.github.tchx84.Flatseal 2.4.0 stable flathub Flatseal +com.heroicgameslauncher.hgl v2.21.0 stable flathub Heroic +io.github.nokse22.high-tide 1.3.1 stable flathub High Tide diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/search-sqlitebrowser-tab.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/search-sqlitebrowser-tab.txt new file mode 100644 index 000000000..c4f1bf21d --- /dev/null +++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/search-sqlitebrowser-tab.txt @@ -0,0 +1 @@ +DB Browser for SQLite light GUI editor for SQLite databases org.sqlitebrowser.sqlitebrowser 3.13.1 stable flathub diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/search-sqlitebrowser.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/search-sqlitebrowser.txt new file mode 100644 index 000000000..68239b385 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/search-sqlitebrowser.txt @@ -0,0 +1,2 @@ +Name Description Application ID Version Branch Remotes +DB Browser for SQLite light GUI editor for SQLite databases org.sqlitebrowser.sqlitebrowser 3.13.1 stable flathub diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/updates-list.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/updates-list.txt new file mode 100644 index 000000000..130275f2e --- /dev/null +++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/updates-list.txt @@ -0,0 +1,3 @@ +io.podman_desktop.PodmanDesktop 1.27.1 stable flathub Podman Desktop +org.freedesktop.Platform.Compat.i386 25.08 flathub i386 +org.freedesktop.Platform.GL.default 26.0.4 24.08 flathub Mesa diff --git a/src/UniGetUI.PackageEngine.Tests/FlatpakManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/FlatpakManagerTests.cs new file mode 100644 index 000000000..e0feee003 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Tests/FlatpakManagerTests.cs @@ -0,0 +1,385 @@ +using UniGetUI.Core.Data; +using UniGetUI.Core.SettingsEngine; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Managers.FlatpakManager; +using UniGetUI.PackageEngine.PackageClasses; +using UniGetUI.PackageEngine.Serializable; +using UniGetUI.PackageEngine.Tests.Infrastructure.Assertions; +using UniGetUI.PackageEngine.Tests.Infrastructure.Builders; +using UniGetUI.PackageEngine.Tests.Infrastructure.Helpers; + +namespace UniGetUI.PackageEngine.Tests; + +[CollectionDefinition("Flatpak manager tests", DisableParallelization = true)] +public sealed class FlatpakManagerTestCollection +{ + public const string Name = "Flatpak manager tests"; +} + +[Collection(FlatpakManagerTestCollection.Name)] +public sealed class FlatpakManagerTests : IDisposable +{ + private readonly string _testRoot = Path.Combine( + AppContext.BaseDirectory, + nameof(FlatpakManagerTests), + Guid.NewGuid().ToString("N") + ); + + public FlatpakManagerTests() + { + Directory.CreateDirectory(_testRoot); + CoreData.TEST_DataDirectoryOverride = Path.Combine(_testRoot, "Data"); + Directory.CreateDirectory(CoreData.UniGetUIUserConfigurationDirectory); + Settings.ResetSettings(); + } + + public void Dispose() + { + Settings.ResetSettings(); + CoreData.TEST_DataDirectoryOverride = null; + if (Directory.Exists(_testRoot)) + { + Directory.Delete(_testRoot, recursive: true); + } + } + + [Fact] + public void ParseInstalledPackagesBuildsPackagesFromFixture() + { + var manager = new Flatpak(); + + var packages = Flatpak.ParseInstalledPackages( + File.ReadLines(PackageEngineFixtureFiles.GetPath(Path.Combine("Flatpak", "installed-list.txt"))), + manager.DefaultSource, + manager + ); + + Assert.Collection( + packages, + package => + { + PackageAssert.Matches(package, "Flatseal", "com.github.tchx84.Flatseal", "2.4.0"); + PackageAssert.BelongsTo(package, manager, manager.DefaultSource); + }, + package => + { + PackageAssert.Matches(package, "Heroic", "com.heroicgameslauncher.hgl", "v2.21.0"); + PackageAssert.BelongsTo(package, manager, manager.DefaultSource); + }, + package => + { + PackageAssert.Matches(package, "High Tide", "io.github.nokse22.high-tide", "1.3.1"); + PackageAssert.BelongsTo(package, manager, manager.DefaultSource); + } + ); + } + + [Fact] + public void ParseAvailableUpdatesBuildsPackagesFromFixture() + { + var manager = new Flatpak(); + + var installedPackages = new List + { + new("Podman Desktop", "io.podman_desktop.PodmanDesktop", "1.26.0", manager.DefaultSource, manager), + new("Mesa", "org.freedesktop.Platform.GL.default", "26.0.3", manager.DefaultSource, manager), + }; + + var packages = Flatpak.ParseAvailableUpdates( + File.ReadLines(PackageEngineFixtureFiles.GetPath(Path.Combine("Flatpak", "updates-list.txt"))), + manager.DefaultSource, + manager, + installedPackages + ); + + Assert.Collection( + packages, + package => + { + PackageAssert.Matches(package, "Podman Desktop", "io.podman_desktop.PodmanDesktop", "1.26.0", "1.27.1"); + Assert.True(package.IsUpgradable); + PackageAssert.BelongsTo(package, manager, manager.DefaultSource); + }, + package => + { + PackageAssert.Matches(package, "Mesa", "org.freedesktop.Platform.GL.default", "26.0.3", "26.0.4"); + Assert.True(package.IsUpgradable); + PackageAssert.BelongsTo(package, manager, manager.DefaultSource); + } + ); + } + + [Fact] + public void ParseSearchResultsBuildsPackagesFromFixture() + { + var manager = new Flatpak(); + + var packages = Flatpak.ParseSearchResults( + File.ReadLines(PackageEngineFixtureFiles.GetPath(Path.Combine("Flatpak", "search-sqlitebrowser.txt"))), + manager.DefaultSource, + manager + ); + + Assert.Collection( + packages, + package => + { + PackageAssert.Matches(package, "DB Browser For SQLite", "org.sqlitebrowser.sqlitebrowser", "3.13.1"); + PackageAssert.BelongsTo(package, manager, manager.DefaultSource); + } + ); + } + + [Fact] + public void ParseInstalledPackagesFiltersEmptyLines() + { + var manager = new Flatpak(); + var lines = new[] + { + "", + " ", + "com.github.tchx84.Flatseal\t2.4.0\tstable\tflathub\tFlatseal", + }; + + var packages = Flatpak.ParseInstalledPackages(lines, manager.DefaultSource, manager); + + Assert.Single(packages); + Assert.Equal("com.github.tchx84.Flatseal", packages[0].Id); + } + + [Fact] + public void ParseAvailableUpdatesFiltersEmptyLines() + { + var manager = new Flatpak(); + var lines = new[] + { + "", + "io.podman_desktop.PodmanDesktop\t1.27.1\tstable\tflathub\tPodman Desktop", + }; + + var installedPackages = new List + { + new Package("Podman Desktop", "io.podman_desktop.PodmanDesktop", "1.26.0", manager.DefaultSource, manager), + }; + + var packages = Flatpak.ParseAvailableUpdates(lines, manager.DefaultSource, manager, installedPackages); + + Assert.Single(packages); + Assert.Equal("io.podman_desktop.PodmanDesktop", packages[0].Id); + Assert.Equal("1.26.0", packages[0].VersionString); + Assert.Equal("1.27.1", packages[0].NewVersionString); + } + + [Fact] + public void ParseSearchResultsHandlesTabSeparatedOutputWithoutHeader() + { + var manager = new Flatpak(); + + var packages = Flatpak.ParseSearchResults( + File.ReadLines(PackageEngineFixtureFiles.GetPath(Path.Combine("Flatpak", "search-sqlitebrowser-tab.txt"))), + manager.DefaultSource, + manager + ); + + Assert.Collection( + packages, + package => + { + PackageAssert.Matches(package, "DB Browser For SQLite", "org.sqlitebrowser.sqlitebrowser", "3.13.1"); + PackageAssert.BelongsTo(package, manager, manager.DefaultSource); + } + ); + } + + [Fact] + public void ParseSearchResultsSkipsHeaderAndEmptyLines() + { + var manager = new Flatpak(); + var lines = new[] + { + "Name Description Application ID Version Branch Remotes", + "", + "DB Browser for SQLite light GUI editor for SQLite databases org.sqlitebrowser.sqlitebrowser 3.13.1 stable flathub", + }; + + var packages = Flatpak.ParseSearchResults(lines, manager.DefaultSource, manager); + + Assert.Single(packages); + Assert.Equal("org.sqlitebrowser.sqlitebrowser", packages[0].Id); + } + + [Fact] + public void ParseInstalledPackagesReturnsEmptyForNoResults() + { + var manager = new Flatpak(); + + var packages = Flatpak.ParseInstalledPackages([], manager.DefaultSource, manager); + + Assert.Empty(packages); + } + + [Fact] + public void ParseAvailableUpdatesReturnsEmptyForNoResults() + { + var manager = new Flatpak(); + + var packages = Flatpak.ParseAvailableUpdates([], manager.DefaultSource, manager, []); + Assert.Empty(packages); + } + + [Fact] + public void ParseSearchResultsReturnsEmptyForNoResults() + { + var manager = new Flatpak(); + var lines = new[] + { + "Name Description Application ID Version Branch Remotes", + }; + + var packages = Flatpak.ParseSearchResults(lines, manager.DefaultSource, manager); + + Assert.Empty(packages); + } + + [Fact] + public void OperationHelperBuildsInstallAndUninstallParameters() + { + var manager = new Flatpak(); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("org.sqlitebrowser.sqlitebrowser") + .Build(); + var installOptions = new InstallOptions + { + CustomParameters_Install = [], + }; + var uninstallOptions = new InstallOptions(); + + var installParameters = manager.OperationHelper.GetParameters( + package, + installOptions, + OperationType.Install + ); + var uninstallParameters = manager.OperationHelper.GetParameters( + package, + uninstallOptions, + OperationType.Uninstall + ); + + Assert.Equal( + [ + "install", + "--noninteractive", + "-y", + "org.sqlitebrowser.sqlitebrowser", + ], + installParameters + ); + Assert.Equal( + [ + "uninstall", + "--noninteractive", + "-y", + "org.sqlitebrowser.sqlitebrowser", + ], + uninstallParameters + ); + } + + [Fact] + public void OperationHelperForcesAdministratorForAllOperations() + { + var manager = new Flatpak(); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("com.github.tchx84.Flatseal") + .Build(); + var options = new InstallOptions(); + + _ = manager.OperationHelper.GetParameters(package, options, OperationType.Install); + Assert.True(options.RunAsAdministrator); + + options = new InstallOptions(); + _ = manager.OperationHelper.GetParameters(package, options, OperationType.Update); + Assert.True(options.RunAsAdministrator); + + options = new InstallOptions(); + _ = manager.OperationHelper.GetParameters(package, options, OperationType.Uninstall); + Assert.True(options.RunAsAdministrator); + } + + [Fact] + public void OperationHelperReturnsSuccessOnZeroExitCode() + { + var manager = new Flatpak(); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("com.github.tchx84.Flatseal") + .Build(); + + var result = manager.OperationHelper.GetResult( + package, + OperationType.Install, + ["Installing..."], + 0 + ); + + Assert.Equal(OperationVeredict.Success, result); + } + + [Fact] + public void OperationHelperReturnsFailureOnNonZeroExitCode() + { + var manager = new Flatpak(); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("com.github.tchx84.Flatseal") + .Build(); + + var result = manager.OperationHelper.GetResult( + package, + OperationType.Install, + ["error: something went wrong"], + 1 + ); + + Assert.Equal(OperationVeredict.Failure, result); + } + + [Fact] + public void ManagerHasCorrectCapabilities() + { + var manager = new Flatpak(); + + Assert.True(manager.Capabilities.CanRunAsAdmin); + Assert.True(manager.Capabilities.CanSkipIntegrityChecks); + Assert.True(manager.Capabilities.SupportsCustomSources); + } + + [Fact] + public void ManagerHasCorrectProperties() + { + var manager = new Flatpak(); + + Assert.Equal("Flatpak", manager.Name); + Assert.Equal("flatpak", manager.Properties.ExecutableFriendlyName); + Assert.Equal("install", manager.Properties.InstallVerb); + Assert.Equal("update", manager.Properties.UpdateVerb); + Assert.Equal("uninstall", manager.Properties.UninstallVerb); + Assert.Equal("flathub", manager.DefaultSource.Name); + Assert.Equal("https://dl.flathub.org/repo/", manager.DefaultSource.Url.ToString()); + } + + [Fact] + public void GetInstallableVersionsReturnsEmptyList() + { + var manager = new Flatpak(); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("org.sqlitebrowser.sqlitebrowser") + .Build(); + + var versions = manager.DetailsHelper.GetVersions(package); + Assert.Empty(versions); + } +} diff --git a/src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj b/src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj index 01d61ac4d..97f08a85f 100644 --- a/src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj +++ b/src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj @@ -29,6 +29,7 @@ + diff --git a/src/UniGetUI.Windows.slnx b/src/UniGetUI.Windows.slnx index 8f1754aeb..d3292646f 100644 --- a/src/UniGetUI.Windows.slnx +++ b/src/UniGetUI.Windows.slnx @@ -184,6 +184,10 @@
+ + + + diff --git a/src/UniGetUI/Assets/Symbols/flatpak.svg b/src/UniGetUI/Assets/Symbols/flatpak.svg new file mode 100644 index 000000000..49b91308d --- /dev/null +++ b/src/UniGetUI/Assets/Symbols/flatpak.svg @@ -0,0 +1 @@ + \ No newline at end of file