Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
76 changes: 43 additions & 33 deletions src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@ public partial class Cargo : PackageManager
[GeneratedRegex(@"([\w-]+)\s=\s""(\d+\.\d+\.\d+)""\s*#\s(.*)")]
private static partial Regex SearchLineRegex();

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

// Matches "ripgrep v15.1.0:" lines from `cargo install --list`
[GeneratedRegex(@"^([\w-]+)\s+v(\d+\.\d+\.\d+):")]
private static partial Regex InstallListLineRegex();

public Cargo()
{
string cargoCommand = OperatingSystem.IsWindows() ? "cargo.exe" : "cargo";
Expand Down Expand Up @@ -193,28 +186,44 @@ protected override void _loadManagerVersion(out string version)
}

public void InvalidateInstalledCache() =>
TaskRecycler<List<Match>>.RemoveFromCache(GetInstalledCommandOutput);
TaskRecycler<List<CargoListEntry>>.RemoveFromCache(GetInstalledCommandOutput);

private IReadOnlyList<Package> GetPackages(LoggableTaskType taskType)
{
List<Package> Packages = [];
foreach (var match in TaskRecycler<List<Match>>.RunOrAttach(GetInstalledCommandOutput, 15))
var entries = TaskRecycler<List<CargoListEntry>>.RunOrAttach(GetInstalledCommandOutput, 15);
foreach (var entry in entries)
{
var id = match.Groups[1]?.Value?.Trim() ?? "";
var name = CoreTools.FormatAsName(id);
var oldVersion = match.Groups[2]?.Value?.Trim() ?? "";
var newVersion = match.Groups[3]?.Value?.Trim() ?? "";
if (taskType is LoggableTaskType.ListUpdates && oldVersion != newVersion)
Packages.Add(new Package(name, id, oldVersion, newVersion, DefaultSource, this));
var name = CoreTools.FormatAsName(entry.Id);
if (taskType is LoggableTaskType.ListUpdates)
{
if (
entry.NeedsUpdate
&& entry.LatestVersion is { Length: > 0 } latestVersion
&& latestVersion != entry.InstalledVersion
)
Packages.Add(
new Package(
name,
entry.Id,
entry.InstalledVersion,
latestVersion,
DefaultSource,
this
)
);
}
else if (taskType is LoggableTaskType.ListInstalledPackages)
Packages.Add(new Package(name, id, oldVersion, DefaultSource, this));
Packages.Add(
new Package(name, entry.Id, entry.InstalledVersion, DefaultSource, this)
);
}
return Packages;
}

private List<Match> GetInstalledCommandOutput()
private List<CargoListEntry> GetInstalledCommandOutput()
{
List<Match> output = [];
List<string> stdout = [];
using Process p = GetProcess(Status.ExecutablePath, "install-update --list");
IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.OtherTask, p);
logger.AddToStdOut("Other task: Call the install-update command");
Expand All @@ -224,38 +233,39 @@ private List<Match> GetInstalledCommandOutput()
while ((line = p.StandardOutput.ReadLine()) is not null)
{
logger.AddToStdOut(line);
var match = UpdateLineRegex().Match(line);
if (match.Success)
output.Add(match);
stdout.Add(line);
}
logger.AddToStdErr(p.StandardError.ReadToEnd());
p.WaitForExit();

List<string> skippedRows = [];
var output = ParseInstallUpdateList(stdout, skippedRows);
foreach (var skippedRow in skippedRows)
logger.AddToStdErr($"Ignored unrecognized `install-update --list` row: {skippedRow}");
logger.Close(p.ExitCode);

if (output.Count > 0)
return output;

// Fallback: cargo-update is not installed, use the built-in `cargo install --list`.
// No latest-version info is available, so updates won't be detected, but the installed
// packages list will be populated correctly.
List<string> fallbackStdout = [];
using Process fallback = GetProcess(Status.ExecutablePath, "install --list");
IProcessTaskLogger fallbackLogger = TaskLogger.CreateNew(LoggableTaskType.OtherTask, fallback);
fallbackLogger.AddToStdOut("Falling back to `cargo install --list` (cargo-update not available)");
IProcessTaskLogger fallbackLogger = TaskLogger.CreateNew(
LoggableTaskType.OtherTask,
fallback
);
fallbackLogger.AddToStdOut(
"Falling back to `cargo install --list` (cargo-update reported no packages)"
);
fallback.Start();
while ((line = fallback.StandardOutput.ReadLine()) is not null)
{
fallbackLogger.AddToStdOut(line);
var m = InstallListLineRegex().Match(line);
if (!m.Success) continue;
// Synthesise a match compatible with UpdateLineRegex (same installed and latest version → no update)
var fake = UpdateLineRegex().Match($"{m.Groups[1].Value} v{m.Groups[2].Value} v{m.Groups[2].Value} No");
if (fake.Success)
output.Add(fake);
fallbackStdout.Add(line);
}
fallbackLogger.AddToStdErr(fallback.StandardError.ReadToEnd());
fallback.WaitForExit();
fallbackLogger.Close(fallback.ExitCode);
return output;
return ParseInstallList(fallbackStdout);
}

private Process GetProcess(string fileName, string extraArguments)
Expand Down
117 changes: 117 additions & 0 deletions src/UniGetUI.PackageEngine.Managers.Cargo/CargoListParsing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System.Text.RegularExpressions;

namespace UniGetUI.PackageEngine.Managers.CargoManager;

internal sealed record CargoListEntry(
string Id,
string InstalledVersion,
string? LatestVersion,
bool NeedsUpdate
);

public partial class Cargo
{
[GeneratedRegex(@"^[A-Za-z0-9_][A-Za-z0-9_-]*$")]
private static partial Regex CrateNameRegex();

[GeneratedRegex(@"[ \t]{2,}|\t")]
private static partial Regex ColumnSeparatorRegex();

[GeneratedRegex(@"^v(?<version>[0-9][A-Za-z0-9.+-]*)(?:\s+\(v[^)]*\))?$")]
private static partial Regex VersionCellRegex();

[GeneratedRegex(@"^(?<id>[A-Za-z0-9_][A-Za-z0-9_-]*)\s+v(?<version>[0-9][A-Za-z0-9.+-]*)(?:\s+\(.*\))?:$")]
private static partial Regex InstallListLineRegex();

internal static List<CargoListEntry> ParseInstallUpdateList(
IEnumerable<string> lines,
List<string>? skippedRows = null
)
{
List<CargoListEntry> entries = [];
bool insideTable = false;

foreach (var rawLine in lines)
{
var line = rawLine.Trim();

if (line.Length is 0)
{
insideTable = false;
continue;
}

if (IsTableHeader(line))
{
insideTable = true;
continue;
}

if (!insideTable)
continue;

var entry = ParseInstallUpdateRow(line);
if (entry is null)
skippedRows?.Add(line);
else
entries.Add(entry);
}

return entries;
}

internal static List<CargoListEntry> ParseInstallList(IEnumerable<string> lines)
{
List<CargoListEntry> entries = [];

foreach (var rawLine in lines)
{
var match = InstallListLineRegex().Match(rawLine.TrimEnd());
if (match.Success)
entries.Add(
new CargoListEntry(
match.Groups["id"].Value,
match.Groups["version"].Value,
null,
false
)
);
}

return entries;
}

private static bool IsTableHeader(string line) =>
line.StartsWith("Package", StringComparison.Ordinal)
&& line.Contains("Installed", StringComparison.Ordinal)
&& line.Contains("Latest", StringComparison.Ordinal)
&& line.Contains("Needs update", StringComparison.Ordinal);

private static CargoListEntry? ParseInstallUpdateRow(string line)
{
var cells = ColumnSeparatorRegex().Split(line);
if (cells.Length < 4)
return null;

var id = cells[0].Trim();
if (!CrateNameRegex().IsMatch(id))
return null;

var installedVersion = ParseVersionCell(cells[1]);
if (installedVersion is null)
return null;

return new CargoListEntry(
id,
installedVersion,
ParseVersionCell(cells[2]),
cells[3].Trim().Equals("Yes", StringComparison.OrdinalIgnoreCase)
);
}

private static string? ParseVersionCell(string cell)
{
var match = VersionCellRegex().Match(cell.Trim());
return match.Success ? match.Groups["version"].Value : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,22 @@ protected override void GetDetails_UnSafe(IPackageDetails details)
var categories = manifest.categories?.Select(c => c.category) ?? [];
details.Tags = [.. keywords, .. categories];

var versionData = manifest
.versions.Where((v) => v.num == details.Package.VersionString)
.First();

details.Author = versionData.published_by?.name;
details.License = versionData.license;
details.InstallerUrl = new Uri(
(CratesIOClient.ApiUrl + versionData.dl_path).Replace("/api/v1/api/v1", "/api/v1")
var versionData = manifest.versions.FirstOrDefault(v =>
v.num == details.Package.VersionString
);
details.InstallerSize = versionData.crate_size ?? 0;
details.InstallerHash = versionData.checksum;
details.Publisher = versionData.published_by?.name;
details.UpdateDate = versionData.updated_at;

if (versionData is not null)
{
details.Author = versionData.published_by?.name;
details.License = versionData.license;
details.InstallerUrl = new Uri(
(CratesIOClient.ApiUrl + versionData.dl_path).Replace("/api/v1/api/v1", "/api/v1")
);
details.InstallerSize = versionData.crate_size ?? 0;
details.InstallerHash = versionData.checksum;
details.Publisher = versionData.published_by?.name;
details.UpdateDate = versionData.updated_at;
}

// TODO: most packages are hosted on Github; see if there's a way to use the repository
// info to extract release notes
Expand Down
Loading
Loading