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
36 changes: 36 additions & 0 deletions maintenance/backports.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,42 @@ upstream_base:
commit: 68411409d001b9f34cfb896af1543a1d7067ca5e

backports:
- commit: b0d8bcb11569f5b3eb007fe2315c782e6ce97f5d
upstream_pr: 5312
status: applied
class: backend
reason: Replaces fragile Cargo output regexes with column-aware parsing, safe install-list fallback, and tolerant crate metadata lookup.

- commit: ac366036a875f37e2d7b1977623a5cdd8f2d3f4d
upstream_pr: 4974
status: applied
class: backend
reason: Kills registered manager listing processes on timeout and avoids retrying Chocolatey listings that cannot be repaired safely.

- commit: 4108308bf6c0f67754da25c47135ba55fd8beee1
upstream_pr: 5321
status: applied
class: backend
reason: Drains Scoop stdout and stderr concurrently, closes command input, handles listing timeouts, and reports unknown versions safely.

- commit: 20f3296c9e5fde85201aa1cba4c897c848b3f724
upstream_pr: 5322
status: partial
class: backend-plus-winui
reason: Detects Cargo binaries under CARGO_HOME, adds dynamic dependency commands, and verifies Classic WinUI dependency installs; the upstream Avalonia dialog rewrite is intentionally excluded.

- commit: 357e1b122cc22277feeb3da146fa8430a3b2b716
upstream_pr: 5309
status: applied
class: backend
reason: Propagates non-terminating PowerShell 5.x module errors through the operation exit code while respecting caller-owned error variables.

- commit: c683fa77961b1cdf72a41599f5a91ab5699db8e6
upstream_pr: 5244
status: applied-semantic
class: backend
reason: Treats Pinget's no-applicable-upgrade message as not applicable even when the process exits with code zero, preserving Classic's existing retry and phantom-suppression guards.

- commit: 21116375c8299d1db38a3c3b4c2eb7e18bc97c4e
upstream_pr: 5072
status: applied
Expand Down
4 changes: 3 additions & 1 deletion src/Languages/lang_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -843,5 +843,7 @@
"Error": "Error",
"Log in failed: ": "Log in failed: ",
"Log out failed: ": "Log out failed: ",
"Package backup settings": "Package backup settings"
"Package backup settings": "Package backup settings",
"Please wait while {0} is being installed. This may take several minutes.": "Please wait while {0} is being installed. This may take several minutes.",
"The installer finished, but {0} could not be found on your system.": "The installer finished, but {0} could not be found on your system."
}
9 changes: 8 additions & 1 deletion src/UniGetUI.PackageEngine.Interfaces/ManagerDependency.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,26 @@ public readonly struct ManagerDependency
public readonly Func<Task<bool>> IsInstalled;
public readonly string FancyInstallCommand;

private readonly Func<(string FileName, string Arguments)>? _resolveCommand;

public ManagerDependency(
string name,
string installFileName,
string installArguments,
string fancyInstallCommand,
Func<Task<bool>> isInstalled
Func<Task<bool>> isInstalled,
Func<(string FileName, string Arguments)>? resolveCommand = null
)
{
Name = name;
InstallFileName = installFileName;
InstallArguments = installArguments;
IsInstalled = isInstalled;
FancyInstallCommand = fancyInstallCommand;
_resolveCommand = resolveCommand;
}

public (string FileName, string Arguments) GetInstallCommand() =>
_resolveCommand?.Invoke() ?? (InstallFileName, InstallArguments);
}
}
153 changes: 111 additions & 42 deletions src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,57 @@ 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();
internal static IReadOnlyList<string> GetCargoBinDirectories(
Func<string, string?> readEnvironmentVariable,
string userProfileDirectory
)
{
List<string> directories = [];

if (readEnvironmentVariable("CARGO_HOME")?.Trim() is { Length: > 0 } cargoHome)
directories.Add(Path.Join(cargoHome, "bin"));
else if (userProfileDirectory.Trim() is { Length: > 0 } userProfile)
directories.Add(Path.Join(userProfile, ".cargo", "bin"));

return directories;
}

internal static bool IsCargoBinaryPresent(string binaryName) =>
CoreTools.Which(binaryName).Item1
|| GetCargoBinDirectories(
Environment.GetEnvironmentVariable,
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
)
.Any(directory => IsExecutableFile(Path.Join(directory, binaryName)));

private static bool IsExecutableFile(string path)
{
if (!File.Exists(path))
return false;

if (OperatingSystem.IsWindows())
return true;

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

try
{
return (File.GetUnixFileMode(path) & ExecutableBits) is not 0;
}
catch (IOException)
{
return false;
}
catch (UnauthorizedAccessException)
{
return false;
}
catch (NotSupportedException)
{
return false;
}
}

public Cargo()
{
Expand All @@ -38,23 +83,30 @@ public Cargo()
? "cargo-binstall.exe"
: "cargo-binstall";

string CargoPath() =>
Status.ExecutablePath is { Length: > 0 } path ? path : cargoCommand;

Dependencies =
[
// cargo-update is required to check for installed and upgradable packages
// Cargo-binstall is required to install and update cargo binaries
new ManagerDependency(
"cargo-update",
"cargo-binstall",
cargoCommand,
"install cargo-update",
"cargo install cargo-update",
async () => (await CoreTools.WhichAsync(cargoUpdateBinary)).Item1
"install cargo-binstall --locked",
"cargo install cargo-binstall --locked",
async () => await Task.Run(() => IsCargoBinaryPresent(cargoBinstallBinary)),
() => (CargoPath(), "install cargo-binstall --locked")
),
// Cargo-binstall is required to install and update cargo binaries
// cargo-update is required to check for installed and upgradable packages
new ManagerDependency(
"cargo-binstall",
"cargo-update",
cargoCommand,
"install cargo-binstall",
"cargo install cargo-binstall",
async () => (await CoreTools.WhichAsync(cargoBinstallBinary)).Item1
"install cargo-update --locked",
"cargo install cargo-update --locked",
async () => await Task.Run(() => IsCargoBinaryPresent(cargoUpdateBinary)),
() => IsCargoBinaryPresent(cargoBinstallBinary)
? (CargoPath(), "binstall --no-confirm cargo-update")
: (CargoPath(), "install cargo-update --locked")
),
];

Expand Down Expand Up @@ -165,7 +217,7 @@ protected override IReadOnlyList<Package> GetInstalledPackages_UnSafe()
}

public readonly bool HasBinstall =
CoreTools.Which(OperatingSystem.IsWindows() ? "cargo-binstall.exe" : "cargo-binstall").Item1;
IsCargoBinaryPresent(OperatingSystem.IsWindows() ? "cargo-binstall.exe" : "cargo-binstall");

public override IReadOnlyList<string> FindCandidateExecutableFiles() =>
CoreTools.WhichMultiple(OperatingSystem.IsWindows() ? "cargo.exe" : "cargo");
Expand Down Expand Up @@ -193,28 +245,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 +292,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
Loading
Loading