-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
304 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using CliWrap; | ||
|
||
namespace nugraph; | ||
|
||
internal static class Dotnet | ||
{ | ||
public static async Task<ProjectInfo> ResolvePackageAssetsAsync(FileSystemInfo? source) | ||
{ | ||
var arguments = new List<string> { "build" }; | ||
if (source != null) | ||
{ | ||
arguments.Add(source.FullName); | ||
} | ||
arguments.Add("--target:ResolvePackageAssets"); | ||
arguments.Add($"--getProperty:{nameof(Property.ProjectAssetsFile)}"); | ||
arguments.Add($"--getProperty:{nameof(Property.TargetFramework)}"); | ||
arguments.Add($"--getProperty:{nameof(Property.TargetFrameworks)}"); | ||
arguments.Add($"--getItem:{nameof(Item.RuntimeCopyLocalItems)}"); | ||
|
||
var dotnet = Cli.Wrap("dotnet").WithArguments(arguments).WithValidation(CommandResultValidation.None); | ||
var jsonPipe = new JsonPipeTarget<Result>(dotnet, c => new Exception($"Running \"{c}\" in \"{c.WorkingDirPath}\" returned a literal 'null' JSON payload")); | ||
var stdout = new StringBuilder(); | ||
var stderr = new StringBuilder(); | ||
var commandResult = await dotnet | ||
.WithStandardOutputPipe(PipeTarget.Merge(jsonPipe, PipeTarget.ToStringBuilder(stdout))) | ||
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(stderr)).ExecuteAsync(); | ||
|
||
if (!commandResult.IsSuccess) | ||
{ | ||
var message = stderr.Length > 0 ? stderr.ToString() : stdout.ToString(); | ||
throw new Exception($"Running \"{dotnet}\" in \"{dotnet.WorkingDirPath}\" failed with exit code {commandResult.ExitCode}.{Environment.NewLine}{message}"); | ||
} | ||
|
||
var properties = jsonPipe.Result.Properties; | ||
var copyLocalPackages = jsonPipe.Result.Items.RuntimeCopyLocalItems.Select(e => e.NuGetPackageId).ToArray(); | ||
return new ProjectInfo(properties.ProjectAssetsFile, properties.GetTargetFrameworks(), copyLocalPackages); | ||
} | ||
|
||
public record ProjectInfo(string ProjectAssetsFile, string[] TargetFrameworks, string[] CopyLocalPackages); | ||
|
||
private record Result(Property Properties, Item Items); | ||
|
||
private record Property(string ProjectAssetsFile, string TargetFramework, string TargetFrameworks) | ||
{ | ||
public string[] GetTargetFrameworks() | ||
{ | ||
var targetFrameworks = TargetFrameworks.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); | ||
return targetFrameworks.Length > 0 ? targetFrameworks : [TargetFramework]; | ||
} | ||
} | ||
|
||
private record Item(RuntimeCopyLocalItem[] RuntimeCopyLocalItems); | ||
|
||
private record RuntimeCopyLocalItem(string NuGetPackageId); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
using System; | ||
using System.ComponentModel; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Chisel; | ||
using NuGet.ProjectModel; | ||
using OneOf; | ||
using Spectre.Console; | ||
using Spectre.Console.Cli; | ||
|
||
namespace nugraph; | ||
|
||
[GenerateOneOf] | ||
public partial class FileOrPackage : OneOfBase<FileSystemInfo?, string> | ||
{ | ||
public override string ToString() => Match(file => file?.FullName ?? Environment.CurrentDirectory, id => id); | ||
} | ||
|
||
public class GraphCommand(IAnsiConsole console) : AsyncCommand<GraphCommand.GraphSettings> | ||
{ | ||
public class GraphSettings : CommandSettings | ||
{ | ||
[CommandArgument(0, "[SOURCE]")] | ||
[Description("The source of the graph. Can be either an existing directory, an existing file or the name of a NuGet package.")] | ||
public string? Source { get; init; } | ||
|
||
internal FileOrPackage GetSource() | ||
{ | ||
if (Source == null) | ||
return (FileSystemInfo?)null; | ||
|
||
var file = new FileInfo(Source); | ||
if (file.Exists) | ||
{ | ||
return file; | ||
} | ||
|
||
var directory = new DirectoryInfo(Source); | ||
if (directory.Exists) | ||
{ | ||
return directory; | ||
} | ||
|
||
return Source; | ||
} | ||
|
||
[CommandOption("-o|--output <OUTPUT>")] | ||
//[DefaultValue("graph.mermaid")] | ||
public FileInfo? OutputFile { get; init; } | ||
|
||
[CommandOption("-f|--framework <FRAMEWORK>")] | ||
[Description("The target framework to build for. The target framework must also be specified in the project file.")] | ||
public string? Framework { get; init; } | ||
|
||
[CommandOption("-r|--runtime <RUNTIME_IDENTIFIER>")] | ||
[Description("The target runtime to build for.")] | ||
public string? RuntimeIdentifier { get; init; } | ||
} | ||
|
||
public override async Task<int> ExecuteAsync(CommandContext _, GraphSettings settings) | ||
{ | ||
var source = settings.GetSource(); | ||
var url = await console.Status().StartAsync($"Creating graph for {source}", async _ => | ||
{ | ||
var graph = await source.Match( | ||
f => ComputeDependencyGraphAsync(f, settings), | ||
f => ComputeDependencyGraphAsync(f, settings) | ||
); | ||
var mermaidData = await WriteGraphAsync(graph, settings.OutputFile); | ||
return mermaidData != null ? Mermaid.GetLiveEditorUri(mermaidData, fullScreen: true) : null; | ||
}); | ||
|
||
if (url != null) | ||
{ | ||
var fileName = url.ToString(); | ||
console.WriteLine(fileName); | ||
Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); | ||
} | ||
else | ||
{ | ||
console.MarkupLineInterpolated($"Wrote {source} dependency graph to [lime]{new Uri(settings.OutputFile!.FullName)}[/]"); | ||
} | ||
|
||
return 0; | ||
} | ||
|
||
private static async Task<DependencyGraph> ComputeDependencyGraphAsync(FileSystemInfo? source, GraphSettings settings) | ||
{ | ||
var projectInfo = await Dotnet.ResolvePackageAssetsAsync(source); | ||
var targetFramework = settings.Framework ?? projectInfo.TargetFrameworks.First(); | ||
var lockFile = new LockFileFormat().Read(projectInfo.ProjectAssetsFile); | ||
var (packages, roots) = lockFile.ReadPackages(targetFramework, settings.RuntimeIdentifier); | ||
return new DependencyGraph(packages, roots, projectInfo.CopyLocalPackages, []); | ||
} | ||
|
||
private static async Task<DependencyGraph> ComputeDependencyGraphAsync(string packageId, GraphSettings settings) | ||
{ | ||
await Task.Delay(0); | ||
throw new NotImplementedException($"TODO: Interpret {packageId} as a NuGet package id"); | ||
} | ||
|
||
private static async Task<byte[]?> WriteGraphAsync(DependencyGraph graph, FileInfo? outputFile) | ||
{ | ||
var fileStream = outputFile?.OpenWrite(); | ||
var memoryStream = fileStream == null ? new MemoryStream() : null; | ||
var stream = (fileStream ?? memoryStream as Stream)!; | ||
await using (var streamWriter = new StreamWriter(stream)) | ||
{ | ||
var isMermaid = fileStream == null || Path.GetExtension(fileStream.Name) is ".mmd" or ".mermaid"; | ||
var graphWriter = isMermaid ? GraphWriter.Mermaid(streamWriter) : GraphWriter.Graphviz(streamWriter); | ||
var graphOptions = new GraphOptions | ||
{ | ||
Direction = GraphDirection.LeftToRight, | ||
IncludeVersions = false, | ||
WriteIgnoredPackages = false, | ||
}; | ||
graphWriter.Write(graph, graphOptions); | ||
} | ||
|
||
return memoryStream?.ToArray(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
using System; | ||
using System.IO; | ||
using System.Text.Json; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using CliWrap; | ||
|
||
namespace nugraph; | ||
|
||
internal class JsonPipeTarget<T>(Command command, Func<Command, Exception> exception) : PipeTarget | ||
{ | ||
private T? _result; | ||
private JsonException? _exception; | ||
|
||
public override async Task CopyFromAsync(Stream stream, CancellationToken cancellationToken = default) | ||
{ | ||
try | ||
{ | ||
_result = await JsonSerializer.DeserializeAsync<T>(stream, cancellationToken: cancellationToken) ?? throw exception(command); | ||
} | ||
catch (JsonException jsonException) | ||
{ | ||
_exception = jsonException; | ||
} | ||
} | ||
|
||
public T Result | ||
{ | ||
get | ||
{ | ||
if (_result == null) | ||
{ | ||
if (_exception != null) | ||
throw _exception; | ||
|
||
throw new InvalidOperationException($"Result is only available after {nameof(CopyFromAsync)} has executed."); | ||
} | ||
return _result; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
using System; | ||
using System.IO; | ||
using System.IO.Compression; | ||
using System.Text.Json; | ||
|
||
namespace nugraph; | ||
|
||
internal static class Mermaid | ||
{ | ||
public static Uri GetLiveEditorUri(ReadOnlySpan<byte> data, bool fullScreen) | ||
{ | ||
using var memoryStream = new MemoryStream(); | ||
using (var zlibStream = new ZLibStream(memoryStream, CompressionLevel.SmallestSize)) | ||
using (var writer = new Utf8JsonWriter(zlibStream)) | ||
{ | ||
// See https://github.com/mermaid-js/mermaid-live-editor/blob/dc72838036719637f3947a7c16c0cbbdeba0d73b/src/lib/types.d.ts#L21-L31 | ||
// And https://github.com/mermaid-js/mermaid-live-editor/blob/dc72838036719637f3947a7c16c0cbbdeba0d73b/src/lib/util/state.ts#L10-L23 | ||
writer.WriteStartObject(); | ||
writer.WriteString("code"u8, data); | ||
writer.WriteString("mermaid"u8, """{"theme":"default"}"""u8); | ||
writer.WriteBoolean("panZoom"u8, true); | ||
writer.WriteEndObject(); | ||
} | ||
|
||
// See https://github.com/mermaid-js/mermaid-live-editor/discussions/1291 | ||
var payload = Convert.ToBase64String(memoryStream.ToArray()).Replace("/", "_").Replace("+", "-"); | ||
var mode = fullScreen ? "view" : "edit"; | ||
return new Uri($"https://mermaid.live/{mode}#pako:{payload}"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
using nugraph; | ||
using Spectre.Console.Cli; | ||
|
||
var app = new CommandApp<GraphCommand>(); | ||
app.Run(args); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<PropertyGroup> | ||
<PackAsTool>true</PackAsTool> | ||
<ToolCommandName>nugraph</ToolCommandName> | ||
<RollForward>LatestMajor</RollForward> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="CliWrap" Version="3.6.6" /> | ||
<PackageReference Include="Dunet" Version="1.11.2" PrivateAssets="all" /> | ||
<PackageReference Include="NuGet.ProjectModel" Version="6.9.1" /> | ||
<PackageReference Include="OneOf" Version="3.0.263" /> | ||
<PackageReference Include="OneOf.SourceGenerator" Version="3.0.263" /> | ||
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<Compile Include="..\Chisel\DependencyGraph.cs" LinkBase="Chisel" /> | ||
<Compile Include="..\Chisel\GraphDirection.cs" LinkBase="Chisel" /> | ||
<Compile Include="..\Chisel\GraphOptions.cs" LinkBase="Chisel" /> | ||
<Compile Include="..\Chisel\GraphWriter.cs" LinkBase="Chisel" /> | ||
<Compile Include="..\Chisel\GraphWriter.Graphviz.cs" LinkBase="Chisel" /> | ||
<Compile Include="..\Chisel\GraphWriter.Mermaid.cs" LinkBase="Chisel" /> | ||
<Compile Include="..\Chisel\LockFileExtensions.cs" LinkBase="Chisel" /> | ||
<Compile Include="..\Chisel\Package.cs" LinkBase="Chisel" /> | ||
<Compile Include="..\Chisel\PackageState.cs" LinkBase="Chisel" /> | ||
</ItemGroup> | ||
|
||
</Project> |