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
32 changes: 32 additions & 0 deletions dotnet/src/Devolutions.Pinget.Cli/CliJsonContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Text.Json.Serialization;

namespace Devolutions.Pinget.Cli;

internal sealed record SourceExportEntry(
string Name,
string Type,
string Arg,
string Data,
string Identifier,
string TrustLevel,
bool Explicit,
int Priority);

internal sealed record SourceExportPayload(List<SourceExportEntry> Sources);

internal sealed record PackageExportEntry(
string PackageIdentifier,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? Version);

internal sealed record PackageExportSourceDetails(string Name, string Argument, string Type);

internal sealed record PackageExportSource(
PackageExportSourceDetails SourceDetails,
List<PackageExportEntry> Packages);

internal sealed record PackagesExportPayload(string Schema, List<PackageExportSource> Sources);

[JsonSerializable(typeof(SourceExportPayload))]
[JsonSerializable(typeof(PackagesExportPayload))]
[JsonSourceGenerationOptions(WriteIndented = true)]
internal partial class CliJsonContext : JsonSerializerContext;
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Devolutions.Pinget.Core.Tests" />
</ItemGroup>

<Target Name="UseStaticWindowsRuntimeForNativeAot"
AfterTargets="SetupOSSpecificProps"
Condition="'$(PublishAot)' == 'true'
Expand Down
127 changes: 68 additions & 59 deletions dotnet/src/Devolutions.Pinget.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization.Metadata;
using Devolutions.Pinget.Cli;
using Devolutions.Pinget.Core;
using YamlDotNet.Serialization;

const string Version = "0.6.0";
const string Version = "0.7.0";
const string UpgradeUnsupportedWarning = "Upgrading packages is not supported on this platform; no changes were made.";

if (args.Length == 1 && (string.Equals(args[0], "--version", StringComparison.OrdinalIgnoreCase) || string.Equals(args[0], "-v", StringComparison.OrdinalIgnoreCase)))
Expand Down Expand Up @@ -77,18 +77,18 @@
if (ctx.ParseResult.GetValueForOption(sVersionsOpt))
throw new InvalidOperationException("--manifests cannot be combined with --versions");

WriteStructuredOutput(repo.SearchManifests(query), output);
WriteDynamicStructuredOutput(repo.SearchManifests(query), output);
}
else if (ctx.ParseResult.GetValueForOption(sVersionsOpt))
{
var result = repo.SearchVersions(query);
if (output != OutputFormat.Text) WriteStructuredOutput(result, output);
if (output != OutputFormat.Text) WriteStructuredOutput(result, output, PingetJsonContext.Default.VersionsResult);
else PrintVersions(result);
}
else
{
var result = repo.Search(query);
if (output != OutputFormat.Text) WriteStructuredOutput(result, output);
if (output != OutputFormat.Text) WriteStructuredOutput(result, output, PingetJsonContext.Default.SearchResponse);
else PrintSearch(result);
}
});
Expand Down Expand Up @@ -129,7 +129,7 @@
if (ctx.ParseResult.GetValueForOption(shVerOpt))
{
var result = repo.ShowVersions(query);
if (output != OutputFormat.Text) WriteStructuredOutput(result, output);
if (output != OutputFormat.Text) WriteStructuredOutput(result, output, PingetJsonContext.Default.VersionsResult);
else PrintVersions(result);
}
else
Expand Down Expand Up @@ -181,7 +181,7 @@

using var repo = Repository.Open();
var result = repo.List(query);
if (output != OutputFormat.Text) WriteStructuredOutput(result, output);
if (output != OutputFormat.Text) WriteStructuredOutput(result, output, PingetJsonContext.Default.ListResponse);
else PrintListResult(result, details, upgrade);
});

Expand Down Expand Up @@ -302,7 +302,7 @@

if (!doInstall)
{
if (output != OutputFormat.Text) WriteStructuredOutput(result, output);
if (output != OutputFormat.Text) WriteStructuredOutput(result, output, PingetJsonContext.Default.ListResponse);
else PrintListResult(result, false, true);
}
else if (!string.IsNullOrWhiteSpace(manifestPath))
Expand All @@ -315,6 +315,7 @@
var upgradeable = result.Matches.Where(m => m.AvailableVersion is not null).ToList();
var pins = repo.ListPins();
var upgradedCount = 0;
var failureCount = 0;
if (upgradeable.Count == 0)
{
Console.WriteLine("No applicable upgrade found.");
Expand All @@ -330,6 +331,7 @@
if (pin?.PinType == PinType.Blocking)
{
Console.WriteLine($" Package is blocked by pin {pin.Version}; remove the pin before upgrading.");
failureCount++;
continue;
}

Expand Down Expand Up @@ -358,13 +360,23 @@
: $" Failed to upgrade {m.Id} (exit code: {r.ExitCode})");
if (r.Success && !r.NoOp)
upgradedCount++;
else if (!r.Success)
failureCount++;
}
catch (Exception ex)
{
Console.Error.WriteLine($" Error upgrading {m.Id}: {ex.Message}");
failureCount++;
}
}
Console.WriteLine($"{upgradedCount} package(s) upgraded.");
Console.WriteLine($"{upgradedCount} of {upgradeable.Count} package(s) upgraded.");
if (failureCount > 0)
{
// Surface the failure to the caller (UniGetUI, scripts, etc.) so
// they don't treat a partial failure as success.
Console.Error.WriteLine($"{failureCount} package(s) failed to upgrade during upgrade --all");
ctx.ExitCode = 1;
}
}
}
});
Expand Down Expand Up @@ -425,18 +437,16 @@
sourceExportCmd.SetHandler(() =>
{
using var repo = Repository.Open();
var sources = repo.ListSources().Select(s => new
{
Name = s.Name,
Type = FormatSourceType(s.Kind),
Arg = s.Arg,
Data = s.Identifier,
Identifier = s.Identifier,
TrustLevel = s.TrustLevel,
Explicit = s.Explicit,
Priority = s.Priority,
});
Console.WriteLine(StructuredOutputSerializer.SerializeJson(new { Sources = sources }));
var sources = repo.ListSources().Select(s => new SourceExportEntry(
Name: s.Name,
Type: FormatSourceType(s.Kind),
Arg: s.Arg,
Data: s.Identifier,
Identifier: s.Identifier,
TrustLevel: s.TrustLevel,
Explicit: s.Explicit,
Priority: s.Priority)).ToList();
Console.WriteLine(StructuredOutputSerializer.SerializeJson(new SourceExportPayload(sources), CliJsonContext.Default.SourceExportPayload));
});

sourceAddCmd.SetHandler((ctx) =>
Expand Down Expand Up @@ -511,7 +521,7 @@
};
using var repo = Repository.Open();
var result = repo.WarmCache(query);
if (output != OutputFormat.Text) WriteStructuredOutput(result, output);
if (output != OutputFormat.Text) WriteStructuredOutput(result, output, PingetJsonContext.Default.CacheWarmResult);
else
{
Console.WriteLine($"Warmed cache for {result.Package.Name} [{result.Package.Id}]");
Expand Down Expand Up @@ -560,26 +570,22 @@
{
using var repo = Repository.Open();
var listResult = repo.List(new ListQuery { Source = source is not null ? source : null, Query = source is not null ? " " : null });
var packages = listResult.Matches.Select(m =>
{
var pkg = new Dictionary<string, object> { ["PackageIdentifier"] = m.Id };
if (includeVersions && m.InstalledVersion is not null) pkg["Version"] = m.InstalledVersion;
return pkg;
}).ToList();

var export = new
{
Schema = "https://aka.ms/winget-packages.schema.2.0.json",
Sources = new[]
{
new
{
SourceDetails = new { Name = source ?? "winget", Argument = "https://cdn.winget.microsoft.com/cache", Type = "Microsoft.PreIndexed" },
Packages = packages
}
}
};
File.WriteAllText(output, StructuredOutputSerializer.SerializeJson(export));
var packages = listResult.Matches.Select(m => new PackageExportEntry(
PackageIdentifier: m.Id,
Version: includeVersions ? m.InstalledVersion : null)).ToList();

var export = new PackagesExportPayload(
Schema: "https://aka.ms/winget-packages.schema.2.0.json",
Sources:
[
new PackageExportSource(
SourceDetails: new PackageExportSourceDetails(
Name: source ?? "winget",
Argument: "https://cdn.winget.microsoft.com/cache",
Type: "Microsoft.PreIndexed"),
Packages: packages)
]);
File.WriteAllText(output, StructuredOutputSerializer.SerializeJson(export, CliJsonContext.Default.PackagesExportPayload));
Console.WriteLine($"Exported {packages.Count} packages to {output}");
}, exOutputOpt, exSourceOpt, exVersionsOpt);

Expand Down Expand Up @@ -1128,8 +1134,8 @@

if (!File.Exists(file)) { Console.Error.WriteLine($"error: File not found: {file}"); return; }
var jsonText = File.ReadAllText(file);
var doc = JsonSerializer.Deserialize<JsonElement>(jsonText);
var sources = doc.GetProperty("Sources").EnumerateArray().ToList();
using var doc = JsonDocument.Parse(jsonText);
var sources = doc.RootElement.GetProperty("Sources").EnumerateArray().ToList();

using var repo = Repository.Open();
int total = 0;
Expand Down Expand Up @@ -1260,35 +1266,39 @@ static OutputFormat GetOutputFormat(string? value) =>
_ => OutputFormat.Text,
};

void WriteStructuredOutput(object value, OutputFormat output)
void WriteStructuredOutput<T>(T value, OutputFormat output, JsonTypeInfo<T> typeInfo)
{
switch (output)
{
case OutputFormat.Json:
Console.WriteLine(StructuredOutputSerializer.SerializeJson(value));
Console.WriteLine(StructuredOutputSerializer.SerializeJson(value, typeInfo));
break;
case OutputFormat.Yaml:
Console.Write(StructuredOutputSerializer.SerializeYaml(value));
Console.Write(StructuredOutputSerializer.SerializeYaml(value, typeInfo));
break;
default:
throw new InvalidOperationException("Text output should be handled separately.");
}
}

void WriteManifestStructuredOutput(object value, OutputFormat output)
void WriteDynamicStructuredOutput(object value, OutputFormat output)
{
if (output == OutputFormat.Yaml && value is List<Dictionary<string, object?>> documents)
switch (output)
{
var serializer = new SerializerBuilder().Build();
foreach (var document in documents)
{
Console.Write("---\n");
Console.Write(serializer.Serialize(document));
}
return;
case OutputFormat.Json:
Console.WriteLine(StructuredOutputSerializer.SerializeJson(StructuredOutputSerializer.DynamicToJsonNode(value)));
break;
case OutputFormat.Yaml:
Console.Write(StructuredOutputSerializer.SerializeYaml(value));
break;
default:
throw new InvalidOperationException("Text output should be handled separately.");
}
}

WriteStructuredOutput(value, output);
void WriteManifestStructuredOutput(SerializableShowManifest value, OutputFormat output)
{
WriteStructuredOutput(value, output, PingetJsonContext.Default.SerializableShowManifest);
}

static void PrintSearch(SearchResponse result)
Expand Down Expand Up @@ -1781,8 +1791,7 @@ void WriteJsonNode(JsonNode value, OutputFormat output)
switch (output)
{
case OutputFormat.Yaml:
var structured = JsonSerializer.Deserialize<object>(value.ToJsonString()) ?? new object();
Console.Write(new SerializerBuilder().Build().Serialize(structured));
Console.Write(StructuredOutputSerializer.SerializeYaml(value));
break;
default:
Console.WriteLine(value.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));
Expand Down
68 changes: 60 additions & 8 deletions dotnet/src/Devolutions.Pinget.Cli/StructuredOutputSerializer.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,70 @@
using System.Collections;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization.Metadata;
using Devolutions.Pinget.Core;
using YamlDotNet.Serialization;

namespace Devolutions.Pinget.Cli;

public static class StructuredOutputSerializer
{
public static JsonSerializerOptions JsonOptions { get; } = new() { WriteIndented = true };

public static string SerializeJson(object value) =>
value is SerializableShowManifest showManifest
? JsonSerializer.Serialize(showManifest, PingetJsonContext.Default.SerializableShowManifest)
: JsonSerializer.Serialize(value, JsonOptions);
public static string SerializeJson<T>(T value, JsonTypeInfo<T> typeInfo) =>
JsonSerializer.Serialize(value, typeInfo);

public static string SerializeYaml(object value) =>
new SerializerBuilder().Build().Serialize(value);
}
public static string SerializeJson(JsonNode? node) =>
node?.ToJsonString(JsonOptions) ?? "null";

public static string SerializeYaml(object? value) =>
YamlEmitter.EmitDocument(value);

public static string SerializeYaml<T>(T value, JsonTypeInfo<T> typeInfo) =>
YamlEmitter.EmitDocument(JsonSerializer.SerializeToNode(value, typeInfo));

internal static JsonNode? DynamicToJsonNode(object? value)
{
switch (value)
{
case null:
return null;
case JsonNode node:
return node.DeepClone();
case string s:
return JsonValue.Create(s);
case bool b:
return JsonValue.Create(b);
case int i:
return JsonValue.Create(i);
case long l:
return JsonValue.Create(l);
case double d:
return JsonValue.Create(d);
case decimal m:
return JsonValue.Create(m);
case DateTime dt:
return JsonValue.Create(dt);
case DateTimeOffset dto:
return JsonValue.Create(dto);
case Guid g:
return JsonValue.Create(g);
case IDictionary<string, object?> dict:
{
var obj = new JsonObject();
foreach (var (key, val) in dict)
obj[key] = DynamicToJsonNode(val);
return obj;
}
case IEnumerable enumerable:
{
var array = new JsonArray();
foreach (var item in enumerable)
array.Add(DynamicToJsonNode(item));
return array;
}
default:
return JsonValue.Create(value.ToString());
}
}

}
Loading
Loading