Skip to content

Commit

Permalink
[WIP] nugraph
Browse files Browse the repository at this point in the history
  • Loading branch information
0xced committed May 5, 2024
1 parent 8b379dc commit 8052000
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 0 deletions.
7 changes: 7 additions & 0 deletions Chisel.sln
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlClientSample", "samples\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Client", "samples\Microsoft.Identity.Client\Microsoft.Identity.Client.csproj", "{F40FA01A-EB18-4785-9A3C-379F2E7A7A02}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "nugraph", "src\nugraph\nugraph.csproj", "{7E6E162B-49CA-4D32-8DD9-EAD5E6FE253C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -60,6 +62,10 @@ Global
{F40FA01A-EB18-4785-9A3C-379F2E7A7A02}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F40FA01A-EB18-4785-9A3C-379F2E7A7A02}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F40FA01A-EB18-4785-9A3C-379F2E7A7A02}.Release|Any CPU.Build.0 = Release|Any CPU
{7E6E162B-49CA-4D32-8DD9-EAD5E6FE253C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7E6E162B-49CA-4D32-8DD9-EAD5E6FE253C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7E6E162B-49CA-4D32-8DD9-EAD5E6FE253C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7E6E162B-49CA-4D32-8DD9-EAD5E6FE253C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{845EDA2A-5207-4C6D-ABE9-9635F4630D90} = {0CC84E67-19D2-480B-B36A-6BB15A9109E7}
Expand All @@ -68,5 +74,6 @@ Global
{8B1B3D6A-7100-4DFB-97C9-CF5ACF1A3B08} = {AC8C6685-EDF9-443A-BAF6-A5E7CF777B2A}
{611D4DE0-F729-48A6-A496-2EA3B5DF8EC6} = {0CC84E67-19D2-480B-B36A-6BB15A9109E7}
{F40FA01A-EB18-4785-9A3C-379F2E7A7A02} = {0CC84E67-19D2-480B-B36A-6BB15A9109E7}
{7E6E162B-49CA-4D32-8DD9-EAD5E6FE253C} = {89268D80-B21D-4C76-AF7F-796AAD1E00D9}
EndGlobalSection
EndGlobal
61 changes: 61 additions & 0 deletions src/nugraph/Dotnet.cs
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);
}
124 changes: 124 additions & 0 deletions src/nugraph/GraphCommand.cs
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();
}
}
41 changes: 41 additions & 0 deletions src/nugraph/JsonPipeTarget.cs
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;
}
}
}
30 changes: 30 additions & 0 deletions src/nugraph/Mermaid.cs
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}");
}
}
5 changes: 5 additions & 0 deletions src/nugraph/Program.cs
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);
36 changes: 36 additions & 0 deletions src/nugraph/nugraph.csproj
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>

0 comments on commit 8052000

Please sign in to comment.