Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cargo package manager #2662

Merged
merged 20 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
af38487
Add Cargo
wilt00 Aug 24, 2024
8e3fbc5
Merge branch 'main' into cargo-manager
marticliment Aug 29, 2024
e390e4b
Merge branch 'main' into cargo-manager
marticliment Sep 2, 2024
e2d5155
LiveOutputView will capture STDERR as well
marticliment Sep 2, 2024
502371b
Implement missing GetPackageInstallLocation_Unsafe method
marticliment Sep 2, 2024
4735a4d
Fix installed packages being shown as ugradable when they are not
marticliment Sep 2, 2024
9bbe8a2
Improvements to operation logging
marticliment Sep 2, 2024
1c24d9e
Add cargo icon, change node icon in favor of NPM icon
marticliment Sep 2, 2024
509eaf3
Hide non-binary packages from search
wilt00 Sep 3, 2024
9f73b2b
Merge branch 'main' into cargo-manager
marticliment Sep 3, 2024
538d6ab
Merge branch 'main' into cargo-manager
marticliment Sep 3, 2024
2fb6db6
Merge branch 'main' into cargo-manager
marticliment Sep 3, 2024
84c835c
Merge branch 'main' into cargo-manager
marticliment Sep 4, 2024
2d00884
Merge branch 'main' into pr/2662
marticliment Sep 15, 2024
3f98e8c
Merge branch 'cargo-manager' of https://github.com/wilt00/UniGetUI in…
marticliment Sep 15, 2024
0a5761b
Check cargo packages against the crates.io website instead of using t…
marticliment Sep 15, 2024
14f4e2c
Check cargo packages against the crates.io website instead of using t…
marticliment Sep 15, 2024
39c8ab8
Merge branch 'cargo-manager' of https://github.com/wilt00/UniGetUI in…
marticliment Sep 15, 2024
228dc58
Revert "Check cargo packages against the crates.io website instead of…
marticliment Sep 15, 2024
f87852e
Better timing
marticliment Sep 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/UniGetUI.Interface.Enums/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public enum IconType
Warning_Filled = '\uE93E',
Warning_Round = '\uE93F',
WinGet = '\uE940',
Rust = '\uE941',
}

public class NotificationArguments
Expand Down
190 changes: 190 additions & 0 deletions src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using UniGetUI.Core.Logging;
using UniGetUI.Core.Tools;
using UniGetUI.PackageEngine.Classes.Manager;
using UniGetUI.Interface.Enums;
using UniGetUI.PackageEngine.Classes.Manager.Classes;
using UniGetUI.PackageEngine.Classes.Manager.ManagerHelpers;
using UniGetUI.PackageEngine.Enums;
using UniGetUI.PackageEngine.ManagerClasses.Manager;
using UniGetUI.PackageEngine.PackageClasses;
using UniGetUI.PackageEngine.ManagerClasses.Classes;

namespace UniGetUI.PackageEngine.Managers.CargoManager;

public partial class Cargo : PackageManager
{
public static new string[] FALSE_PACKAGE_NAMES = [""];
public static new string[] FALSE_PACKAGE_IDS = [""];
public static new string[] FALSE_PACKAGE_VERSIONS = [""];

[GeneratedRegex(@"(\w+)\s=\s""(\d+\.\d+\.\d+)""\s*#\s(.*)")]
private static partial Regex SearchLineRegex();

[GeneratedRegex(@"(.+)v(\d+\.\d+\.\d+)\s*v(\d+\.\d+\.\d+)\s*(Yes|No)")]
private static partial Regex UpdateLineRegex();

public Cargo()
{
Dependencies = [
// cargo-update is required to check for and update installed packages
new ManagerDependency(
"cargo-update",
Path.Join(Environment.SystemDirectory, "windowspowershell\\v1.0\\powershell.exe"),
"-ExecutionPolicy Bypass -NoLogo -NoProfile -Command \"& {cargo install cargo-update; if($error.count -ne 0){pause}}\"",
"cargo install cargo-update",
async () => (await CoreTools.Which("cargo-install-update.exe")).Item1),
];

Capabilities = new ManagerCapabilities { };

var cratesIo = new ManagerSource(this, "crates.io", new Uri("https://index.crates.io/"));

Properties = new ManagerProperties
{
Name = "Cargo",
Description = CoreTools.Translate("The Rust package manager.<br>Contains: <b>Rust libraries and programs written in Rust</b>"),
IconId = IconType.Rust,
ColorIconId = "cargo_color",
ExecutableFriendlyName = "cargo.exe",
InstallVerb = "install",
UninstallVerb = "uninstall",
UpdateVerb = "install-update",
ExecutableCallArgs = "",
DefaultSource = cratesIo,
KnownSources = [cratesIo]
};

PackageDetailsProvider = new CargoPackageDetailsProvider(this);
OperationProvider = new CargoOperationProvider(this);
}

protected override async Task<Package[]> FindPackages_UnSafe(string query)
{
Process p = GetProcess(Status.ExecutablePath, "search -q --color=never " + query);
IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages, p);
p.Start();

string? line;
List<Package> Packages = [];
while ((line = await p.StandardOutput.ReadLineAsync()) != null)
{
logger.AddToStdOut(line);
var match = SearchLineRegex().Match(line);
if (match.Success)
{
var id = match.Groups[1].Value;
var version = match.Groups[2].Value;
Packages.Add(new Package(CoreTools.FormatAsName(id), id, version, DefaultSource, this));
}
}

logger.AddToStdErr(await p.StandardError.ReadToEndAsync());
await p.WaitForExitAsync();

List<Package> BinPackages = [];

for (int i = 0; i < Packages.Count; i++)
{
DateTime startTime = DateTime.Now;

var package = Packages[i];
try
{
var versionInfo = await CratesIOClient.GetManifestVersion(package.Id, package.Version);
if (versionInfo.bin_names?.Length > 0)
{
BinPackages.Add(package);
}
}
catch (Exception ex)
{
logger.AddToStdErr($"{ex.Message}");
}

if (i + 1 == Packages.Count) break;
// Crates.io api requests that we send no more than one request per second
await Task.Delay(Math.Max(0, 1000 - (int)((DateTime.Now - startTime).TotalMilliseconds)));
}

logger.Close(p.ExitCode);

return [.. BinPackages];
}

protected override async Task<Package[]> GetAvailableUpdates_UnSafe()
{
return await GetPackages(LoggableTaskType.ListUpdates);
}

protected override async Task<Package[]> GetInstalledPackages_UnSafe()
{
return await GetPackages(LoggableTaskType.ListInstalledPackages);
}

protected override async Task<ManagerStatus> LoadManager()
{
var (found, executablePath) = await CoreTools.Which("cargo");
Process p = GetProcess(executablePath, "--version");
p.Start();
string version = (await p.StandardOutput.ReadToEndAsync()).Trim();
string error = await p.StandardError.ReadToEndAsync();
if (!string.IsNullOrEmpty(error))
{
Logger.Error("cargo version error: " + error);
}

return new() { ExecutablePath = executablePath, Found = found, Version = version };
}

private async Task<Package[]> GetPackages(LoggableTaskType taskType)
{
List<Package> Packages = [];

Process p = GetProcess(Status.ExecutablePath, "install-update --list");
IProcessTaskLogger logger = TaskLogger.CreateNew(taskType, p);
p.Start();

string? line;
while ((line = await p.StandardOutput.ReadLineAsync()) != null)
{
logger.AddToStdOut(line);
var match = UpdateLineRegex().Match(line);
if (match.Success)
{
var id = match.Groups[1].Value.Trim();
var name = CoreTools.FormatAsName(id);
var oldVersion = match.Groups[2].Value;
var newVersion = match.Groups[3].Value;
if(taskType is LoggableTaskType.ListUpdates && oldVersion != newVersion)
Packages.Add(new Package(name, id, oldVersion, newVersion, DefaultSource, this));
else if(taskType is LoggableTaskType.ListInstalledPackages)
Packages.Add(new Package(name, id, oldVersion, DefaultSource, this));
}
}
logger.AddToStdErr(await p.StandardError.ReadToEndAsync());
await p.WaitForExitAsync();
logger.Close(p.ExitCode);
return Packages.ToArray();
}

private Process GetProcess(string fileName, string extraArguments)
{
return new()
{
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = Properties.ExecutableCallArgs + " " + extraArguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
}
};
}
}
100 changes: 100 additions & 0 deletions src/UniGetUI.PackageEngine.Managers.Cargo/CratesIOClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System.Text.Json;
using UniGetUI.Core.Data;

namespace UniGetUI.PackageEngine.Managers.CargoManager;

record CargoManifest
{
public CargoManifestCategory[]? categories { get; init; }
public required CargoManifestCrate crate { get; init; }
public required CargoManifestVersion[] versions { get; init; }
}

record CargoManifestCategory
{
public required string category { get; init; }
public required string description { get; init; }
public required string id { get; init; }
}

record CargoManifestCrate
{
public string[]? categories { get; init; }
public string? description { get; init; }
public string? documentation { get; init; }
public double? downloads { get; init; }
public string? homepage { get; init; }
public string[]? keywords { get; init; }
public required string max_stable_version { get; init; }
public required string max_version { get; init; }
public required string name { get; init; }
public required string newest_version { get; init; }
public string? repository { get; init; }
public string? updated_at { get; init; }
}

record CargoManifestVersion
{
public string[]? bin_names { get; init; }
public required string checksum { get; init; }
public double? crate_size { get; init; }
public string? created_at { get; init; }
public required string dl_path { get; init; }
public string? license { get; init; }
public required string num { get; init; }
public CargoManifestPublisher? published_by { get; init; }
public string? updated_at { get; init; }
public bool yanked { get; init; }
}

record CargoManifestVersionWrapper
{
public required CargoManifestVersion version { get; init; }
}

class CargoManifestPublisher
{
public string? avatar { get; init; }
public required string name { get; init; }
public string? url { get; init; }
}

class CratesIOClient
{
public const string ApiUrl = "https://crates.io/api/v1";

public static async Task<Tuple<Uri, CargoManifest>> GetManifest(string packageId)
{
var manifestUrl = new Uri($"{ApiUrl}/crates/{packageId}");
var manifest = await Fetch<CargoManifest>(manifestUrl);
if (manifest.crate == null)
{
throw new NullResponseException($"Null response for package {packageId}");
}
return Tuple.Create(manifestUrl, manifest);
}

public static async Task<CargoManifestVersion> GetManifestVersion(string packageId, string version)
{
var manifestUrl = new Uri($"{ApiUrl}/crates/{packageId}/{version}");
var manifest = await Fetch<CargoManifestVersionWrapper>(manifestUrl);
if (manifest.version == null)
{
throw new NullResponseException($"Null response for package {packageId}");
}
return manifest.version;
}

private static async Task<T> Fetch<T>(Uri url)
{
HttpClient client = new(CoreData.GenericHttpClientParameters);
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString);

var manifestStr = await client.GetStringAsync(url);

var manifest = JsonSerializer.Deserialize<T>(manifestStr) ?? throw new NullResponseException($"Null response for request to {url}");
return manifest;
}
}

public class NullResponseException(string message) : Exception(message);
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using UniGetUI.PackageEngine.Classes.Manager.BaseProviders;
using UniGetUI.PackageEngine.Enums;
using UniGetUI.PackageEngine.Interfaces;

namespace UniGetUI.PackageEngine.Managers.CargoManager;

internal sealed class CargoOperationProvider(Cargo cargo) : BaseOperationProvider<Cargo>(cargo)
{
public override IEnumerable<string> GetOperationParameters(IPackage package, IInstallationOptions options, OperationType operation)
{
var version = options.Version == string.Empty ? package.Version : options.Version;
List<string> parameters = operation switch
{
OperationType.Install => [Manager.Properties.InstallVerb, "--version", version, package.Id],
OperationType.Update => [Manager.Properties.UpdateVerb, package.Id],
OperationType.Uninstall => [Manager.Properties.UninstallVerb, package.Id],
_ => throw new InvalidDataException("Invalid package operation"),
};

return parameters;
}

public override OperationVeredict GetOperationResult(IPackage package, OperationType operation, IEnumerable<string> processOutput, int returnCode)
{
return returnCode == 0 ? OperationVeredict.Succeeded : OperationVeredict.Failed;
}
}
Loading
Loading