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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ src/UniGetU*/outpublish
src/UniGetUI.Pinget.Cli/bin
src/UniGetUI.Pinget.Cli/obj

scripts/CargoSmoke/bin/
scripts/CargoSmoke/obj/

src/ExternalLibraries.*/bin
src/ExternalLibraries.*/obj
src/ExternalLibraries.*/out
Expand Down
51 changes: 51 additions & 0 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,57 @@ Related environment variables:

## Command reference

### Cargo prerequisites and installation behavior

The Cargo manager requires Cargo, not `cargo-binstall` or `cargo-update`. UniGetUI never
installs or invokes those extensions. Existing installations of those tools remain ordinary
Cargo packages and are not removed.

Installed packages are listed with `cargo install --list`, independently of network access.
UniGetUI checks crates.io versions itself, excluding yanked and prerelease updates by default.
Git, local-path, and other-registry packages remain listed with their source identity; they
are not matched against crates.io for updates. Their details, version catalogs, and source
downloads are not substituted with potentially unrelated same-name crates.io packages.
Named-registry and directory-source display labels remain listed even when Cargo does
not provide a source URL; they do not prevent other packages in the same root from loading.

Local, non-elevated installations first attempt a publisher-provided binary for the exact
selected version and Cargo host target. The C# installer supports ZIP, TAR, TAR.GZ/TGZ, and
explicitly configured raw executables. It reads supported publisher manifest metadata or
unambiguous version-specific GitHub release assets. It requires a supported SHA-256/SHA-512
checksum (including GitHub's published asset digest); unsupported signature-only metadata
uses source compilation. No cargo-quickinstall or other third-party build host is used.
Checksums of crates.io source archives are never reused as binary checksums.

When a compatible verified binary is unavailable, the operation log explains the fallback
to `cargo install --locked` for the same version. Custom Cargo/build options and elevated
operations also use Cargo. Compilation still requires a working Rust compiler, linker,
and any crate-specific native dependencies. A checksum mismatch, unsafe archive, or
cancellation is a failure/cancellation, not permission to silently choose another installer.
Integrity skipping and binstall-specific custom arguments are not supported.

Custom installation locations use Cargo's **install-root** semantics: executables go into
`<root>\bin`, with Cargo's tracking files beside that directory. This differs from binstall's
former `--install-path`, which selected the binary directory directly; review saved custom
locations when migrating. Without a custom root, `CARGO_INSTALL_ROOT`, `install.root` in the
Cargo home configuration, `CARGO_HOME`, and the user-default Cargo home determine the root
in that order. Like Cargo itself, the default installed listing does not scan arbitrary
other custom roots.

Uninstallation remains `cargo uninstall`. Command previews, manual-install commands, and
exported install scripts use ordinary Cargo source commands; the managed binary optimization
runs only inside UniGetUI's operation pipeline. Broker-owned operations stay with the Agent:
they never fall back to a local installer after policy denial or broker failure. The Agent's
own Cargo implementation determines its execution prerequisites and binary/source behavior;
client-side helper removal does not change an externally installed Agent.

The standalone NativeAOT interoperability check is
`dotnet publish scripts\CargoSmoke\CargoSmoke.csproj -c Release -r win-x64 --self-contained true /p:Platform=x64`,
followed by `.\scripts\CargoSmoke\bin\x64\Release\net10.0\win-x64\publish\UniGetUI.Cargo.Smoke.exe`.
It uses fixture downloads and an isolated temporary install root, verifies real Cargo
listing/uninstallation, and treats trim/AOT warnings as errors. Use the matching RID on
other hosts; this does not substitute for their native lock-contention tests.

### Core

| Command | Required options | Optional options | Notes |
Expand Down
19 changes: 19 additions & 0 deletions scripts/CargoSmoke/CargoSmoke.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<AssemblyName>UniGetUI.Cargo.Smoke</AssemblyName>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<IsAotCompatible>true</IsAotCompatible>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<IlcTreatWarningsAsErrors>true</IlcTreatWarningsAsErrors>
<TrimmerTreatWarningsAsErrors>true</TrimmerTreatWarningsAsErrors>
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
<TomlSerializerIsReflectionEnabledByDefault>false</TomlSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\UniGetUI.PackageEngine.Managers.Cargo\UniGetUI.PackageEngine.Managers.Cargo.csproj" />
</ItemGroup>
</Project>
164 changes: 164 additions & 0 deletions scripts/CargoSmoke/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
using System.Buffers.Binary;
using System.Diagnostics;
using System.Formats.Tar;
using System.IO.Compression;
using System.Net;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using UniGetUI.PackageEngine.Managers.CargoManager;

string root = Path.Join(Path.GetTempPath(), "unigetui-cargo-smoke-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
Directory.CreateDirectory(Path.Join(root, "cargo-home"));
try
{
string architecture = RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "aarch64" : "x86_64";
string target = architecture + (OperatingSystem.IsWindows() ? "-pc-windows-msvc"
: OperatingSystem.IsMacOS() ? "-apple-darwin" : "-unknown-linux-gnu");
byte[] executable = new byte[128];
if (OperatingSystem.IsWindows())
{
executable[0] = (byte)'M';
executable[1] = (byte)'Z';
BinaryPrimitives.WriteInt32LittleEndian(executable.AsSpan(0x3C), 64);
BinaryPrimitives.WriteUInt32LittleEndian(executable.AsSpan(64), 0x4550);
BinaryPrimitives.WriteUInt16LittleEndian(executable.AsSpan(68), architecture == "aarch64" ? (ushort)0xAA64 : (ushort)0x8664);
}
else if (OperatingSystem.IsLinux())
{
executable[0] = 0x7F;
executable[1] = (byte)'E';
executable[2] = (byte)'L';
executable[3] = (byte)'F';
executable[4] = 2;
executable[5] = 1;
BinaryPrimitives.WriteUInt16LittleEndian(executable.AsSpan(18), architecture == "aarch64" ? (ushort)183 : (ushort)62);
}
else
{
BinaryPrimitives.WriteUInt32LittleEndian(executable, 0xFEEDFACF);
BinaryPrimitives.WriteUInt32LittleEndian(executable.AsSpan(4), architecture == "aarch64" ? 0x100000CU : 0x1000007U);
}
byte[] binaryArchive;
using (MemoryStream output = new())
{
using (ZipArchive zip = new(output, ZipArchiveMode.Create, leaveOpen: true))
using (Stream entry = zip.CreateEntry("smoke" + (OperatingSystem.IsWindows() ? ".exe" : "")).Open())
entry.Write(executable);
binaryArchive = output.ToArray();
}
byte[] sourceArchive;
using (MemoryStream output = new())
{
using (GZipStream gzip = new(output, CompressionLevel.SmallestSize, leaveOpen: true))
using (TarWriter tar = new(gzip))
{
var manifestEntry = new PaxTarEntry(TarEntryType.RegularFile, "unigetui-smoke/ignored");
manifestEntry.Name = "unigetui-smoke-1.0.0/Cargo.toml";
manifestEntry.DataStream = new MemoryStream(Encoding.UTF8.GetBytes("""
[package]
name = "unigetui-smoke"
version = "1.0.0"
[package.metadata.binstall]
pkg-url = "https://publisher.example/{version}/smoke.zip"
pkg-fmt = "zip"
"""));
tar.WriteEntry(manifestEntry);
}
sourceArchive = output.ToArray();
}
var version = new CargoManifestVersion
{
num = "1.0.0",
checksum = Convert.ToHexString(SHA256.HashData(sourceArchive)),
dl_path = "/crates/unigetui-smoke/1.0.0/download",
bin_names = ["smoke"],
};
var manifest = new CargoManifest
{
crate = new CargoManifestCrate
{
name = "unigetui-smoke",
max_stable_version = "1.0.0",
max_version = "1.0.0",
newest_version = "1.0.0",
},
versions = [version],
};
CargoHttp.TestHandlerFactory = () => new FixtureHandler(sourceArchive, binaryArchive);
using CargoHttp http = new();
CargoBinaryPlan plan = await new CargoBinaryResolver(http).ResolveAsync(
"unigetui-smoke", "1.0.0", target, manifest, Console.WriteLine, CancellationToken.None)
?? throw new InvalidOperationException("Published binary resolution unexpectedly selected fallback.");
CargoBinaryResolver.VerifyHash(binaryArchive, plan.Hash);
var binaries = CargoManagedInstaller.ExtractBinaries(binaryArchive, plan, CancellationToken.None);
await CargoInstallTracker.InstallAsync(root, plan, binaries, CancellationToken.None);
string listing = await Cargo("install", "--list", "--root", root);
if (!listing.Contains("unigetui-smoke v1.0.0:", StringComparison.Ordinal))
throw new InvalidOperationException("Real Cargo did not recognize native installation tracking.");
string fixture = Path.Join(root, "source");
Directory.CreateDirectory(Path.Join(fixture, "src"));
File.WriteAllText(Path.Join(fixture, "Cargo.toml"), """
[package]
name = "unigetui-smoke"
version = "1.0.1"
edition = "2021"
[[bin]]
name = "smoke"
path = "src/main.rs"
""");
File.WriteAllText(Path.Join(fixture, "src", "main.rs"), """fn main() { println!("cargo-smoke"); }""");
await Cargo("generate-lockfile", "--manifest-path", Path.Join(fixture, "Cargo.toml"), "--offline");
await Cargo("install", "--path", fixture, "--root", root, "--target", target, "--locked", "--offline");
if (!(await Cargo("install", "--list", "--root", root)).Contains("unigetui-smoke v1.0.1", StringComparison.Ordinal))
throw new InvalidOperationException("Cargo could not upgrade the native-installed package from source.");
await Cargo("uninstall", "--root", root, "unigetui-smoke");
if ((await Cargo("install", "--list", "--root", root)).Contains("unigetui-smoke", StringComparison.Ordinal))
throw new InvalidOperationException("Real Cargo did not remove the native-installed package.");
Console.WriteLine("PASS: published native resolver, TOML/JSON, integrity, extraction, tracking, Cargo list/source upgrade/uninstall.");
}
finally
{
CargoHttp.TestHandlerFactory = null;
Directory.Delete(root, recursive: true);
}

async Task<string> Cargo(params string[] arguments)
{
using Process process = new();
process.StartInfo = new ProcessStartInfo("cargo")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
};
process.StartInfo.Environment["CARGO_HOME"] = Path.Join(root, "cargo-home");
foreach (string argument in arguments)
process.StartInfo.ArgumentList.Add(argument);
process.Start();
Task<string> stdout = process.StandardOutput.ReadToEndAsync();
Task<string> stderr = process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
if (process.ExitCode != 0)
throw new InvalidOperationException("Cargo smoke operation failed: " + await stderr);
return await stdout;
}

sealed class FixtureHandler(byte[] source, byte[] binary) : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
byte[]? content = request.RequestUri?.AbsoluteUri switch
{
"https://crates.io/api/v1/crates/unigetui-smoke/1.0.0/download" => source,
"https://publisher.example/1.0.0/smoke.zip.sha256" =>
Encoding.ASCII.GetBytes(Convert.ToHexString(SHA256.HashData(binary)) + " smoke.zip"),
_ => null,
};
return Task.FromResult(new HttpResponseMessage(content is null ? HttpStatusCode.NotFound : HttpStatusCode.OK)
{
Content = new ByteArrayContent(content ?? []),
});
}
}
12 changes: 3 additions & 9 deletions src/UniGetUI.Core.Data/Licenses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public static class LicenseData
{ "Windows App Sdk", "MIT" },
{ "PhotoSauce.MagicScaler", "MIT" },
{ "YamlDotNet", "MIT" },
{ "Tomlyn", "BSD-2-Clause" },
{ "InnoDependencyInstaller", "CPOL 1.02" },
// Package managers and related
{ "WinGet", "MIT" },
Expand All @@ -24,8 +25,6 @@ public static class LicenseData
{ "PowerShell Gallery", "Unknown" },
{ ".NET SDK", "MIT" },
{ "Cargo", "MIT" },
{ "cargo-binstall", "GPL-3.0-only" },
{ "cargo-update", "MIT" },
{ "vcpkg", "MIT" },
// Other
{ "GSudo", "MIT" },
Expand Down Expand Up @@ -59,6 +58,7 @@ public static class LicenseData
"YamlDotNet",
new Uri("https://github.com/aaubry/YamlDotNet/blob/master/LICENSE.txt")
},
{ "Tomlyn", new Uri("https://github.com/xoofx/tomlyn/blob/main/license.txt") },
{
"InnoDependencyInstaller",
new Uri(
Expand All @@ -84,11 +84,6 @@ public static class LicenseData
{ ".NET SDK", new Uri("https://github.com/dotnet/sdk/blob/main/LICENSE.TXT") },
{ "PowerShell Gallery", new Uri("https://www.powershellgallery.com/") },
{ "Cargo", new Uri("https://github.com/rust-lang/cargo/blob/master/LICENSE-MIT") },
{ "cargo-binstall", new Uri("https://spdx.org/licenses/GPL-3.0-only.html") },
{
"cargo-update",
new Uri("https://github.com/nabijaczleweli/cargo-update/blob/master/LICENSE")
},
{ "vcpkg", new Uri("https://github.com/microsoft/vcpkg/blob/master/LICENSE.txt") },
// Other
{ "GSudo", new Uri("https://github.com/gerardog/gsudo/blob/master/LICENSE.txt") },
Expand All @@ -110,6 +105,7 @@ public static class LicenseData
{ "Windows App Sdk", new Uri("https://github.com/microsoft/WindowsAppSDK/") },
{ "PhotoSauce.MagicScaler", new Uri("https://github.com/saucecontrol/PhotoSauce/") },
{ "YamlDotNet", new Uri("https://github.com/aaubry/YamlDotNet/") },
{ "Tomlyn", new Uri("https://github.com/xoofx/tomlyn/") },
{
"InnoDependencyInstaller",
new Uri("https://github.com/DomGries/InnoDependencyInstaller")
Expand All @@ -128,8 +124,6 @@ public static class LicenseData
{ ".NET SDK", new Uri("https://dotnet.microsoft.com/") },
{ "PowerShell Gallery", new Uri("https://www.powershellgallery.com/") },
{ "Cargo", new Uri("https://github.com/rust-lang/cargo") },
{ "cargo-binstall", new Uri("https://github.com/cargo-bins/cargo-binstall") },
{ "cargo-update", new Uri("https://github.com/nabijaczleweli/cargo-update/") },
{ "vcpkg", new Uri("https://github.com/microsoft/vcpkg") },
// Other
{ "GSudo", new Uri("https://github.com/gerardog/gsudo/") },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ public struct OverridenInstallationOptions
public bool Pip_BreakSystemPackages = false;
public bool WinGet_DropArchAndScope = false;
public bool WinGet_UseLocalIdentifier = false;
public bool Cargo_DoNotUseBinstall = false;
public bool Cargo_CustomInstallPathRequested = false;

public OverridenInstallationOptions(string? scope = null, bool? runAsAdministrator = null)
{
Expand All @@ -22,6 +20,6 @@ public OverridenInstallationOptions(string? scope = null, bool? runAsAdministrat

public override string ToString()
{
return $"<Scope={Scope};RunAsAdministrator={RunAsAdministrator};WG_SpecifyVersion={WinGet_SpecifyVersion};PS_NoScope={PowerShell_DoNotSetScopeParameter};PS_AllowClobber={PowerShell_AllowClobber};PS_UpdateThroughInstall={PowerShell_UpdateThroughInstall};Pip_BreakSystemPackages={Pip_BreakSystemPackages};WG_DropArchAndScope={WinGet_DropArchAndScope};WG_UseLocalId={WinGet_UseLocalIdentifier};Cargo_DoNotUseBinstall={Cargo_DoNotUseBinstall};Cargo_CustomInstallPath={Cargo_CustomInstallPathRequested}>";
return $"<Scope={Scope};RunAsAdministrator={RunAsAdministrator};WG_SpecifyVersion={WinGet_SpecifyVersion};PS_NoScope={PowerShell_DoNotSetScopeParameter};PS_AllowClobber={PowerShell_AllowClobber};PS_UpdateThroughInstall={PowerShell_UpdateThroughInstall};Pip_BreakSystemPackages={Pip_BreakSystemPackages};WG_DropArchAndScope={WinGet_DropArchAndScope};WG_UseLocalId={WinGet_UseLocalIdentifier}>";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using UniGetUI.PackageEngine.Enums;
using UniGetUI.PackageEngine.Serializable;

namespace UniGetUI.PackageEngine.Interfaces.ManagerProviders;

/// <summary>An optional local execution path; null selects the ordinary manager process.</summary>
public interface IManagedPackageOperationHelper
{
Task<OperationVeredict?> TryPerformAsync(
IPackage package,
InstallOptions options,
OperationType operation,
Action<string> log,
Action<OperationProgress> progress,
CancellationToken cancellationToken);
}
Loading
Loading