-
Notifications
You must be signed in to change notification settings - Fork 4
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
9 changed files
with
731 additions
and
13 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,108 @@ | ||
using System; | ||
using System.IO; | ||
using System.Linq; | ||
using Microsoft.Build.Framework; | ||
using Microsoft.Build.Utilities; | ||
|
||
namespace Chisel; | ||
|
||
/// <summary> | ||
/// Task that determines which package to remove from the build. | ||
/// </summary> | ||
public class ChiselTask : Task | ||
{ | ||
/// <summary> | ||
/// The project assets file path (project.assets.json). | ||
/// </summary> | ||
[Required] | ||
public string ProjectAssetsFile { get; set; } = ""; | ||
|
||
/// <summary> | ||
/// The target framework. | ||
/// </summary> | ||
[Required] | ||
public string TargetFramework { get; set; } = ""; | ||
|
||
/// <summary> | ||
/// The runtime identifier (rid). | ||
/// </summary> | ||
[Required] | ||
public string RuntimeIdentifier { get; set; } = ""; | ||
|
||
/// <summary> | ||
/// The package references to remove from the build. | ||
/// </summary> | ||
[Required] | ||
public ITaskItem[] ChiselPackages { get; set; } = []; | ||
|
||
/// <summary> | ||
/// | ||
/// </summary> | ||
[Required] | ||
public ITaskItem[] RuntimeAssemblies { get; set; } = []; | ||
|
||
/// <summary> | ||
/// | ||
/// </summary> | ||
[Required] | ||
public ITaskItem[] NativeLibraries { get; set; } = []; | ||
|
||
/// <summary> | ||
/// The intermediate output path where the <see cref="Graph"/> is saved. | ||
/// </summary> | ||
public string IntermediateOutputPath { get; set; } = ""; | ||
|
||
/// <summary> | ||
/// The optional dependency graph file name. | ||
/// If the file name ends with <c>.svg</c> then Graphviz <c>dot</c> command line is used to produce a SVG file. | ||
/// Otherwise, a Graphviz <a href="https://graphviz.org/doc/info/lang.html">dot file</a> is written. | ||
/// Use <c>false</c> to disable writing the dependency graph. | ||
/// </summary> | ||
public string Graph { get; set; } = ""; | ||
|
||
/// <summary> | ||
/// The <c>RuntimeCopyLocalItems</c> to remove from the build. | ||
/// </summary> | ||
[Output] | ||
public ITaskItem[] RemoveRuntimeAssemblies { get; private set; } = []; | ||
|
||
/// <summary> | ||
/// The <c>NativeCopyLocalItems</c> to remove from the build. | ||
/// </summary> | ||
[Output] | ||
public ITaskItem[] RemoveNativeLibraries { get; private set; } = []; | ||
|
||
/// <inheritdoc /> | ||
public override bool Execute() | ||
{ | ||
try | ||
{ | ||
var graph = new DependencyGraph(ProjectAssetsFile, TargetFramework, RuntimeIdentifier); | ||
var removed = graph.Remove(ChiselPackages.Select(e => e.ItemSpec)); | ||
|
||
RemoveRuntimeAssemblies = RuntimeAssemblies.Where(item => removed.Contains(item.GetMetadata("NuGetPackageId"))).ToArray(); | ||
RemoveNativeLibraries = NativeLibraries.Where(item => removed.Contains(item.GetMetadata("NuGetPackageId"))).ToArray(); | ||
|
||
if (!string.IsNullOrEmpty(Graph) && !Graph.Equals("false", StringComparison.OrdinalIgnoreCase) && Graph == Path.GetFileName(Graph)) | ||
{ | ||
try | ||
{ | ||
using var output = new FileStream(Path.Combine(IntermediateOutputPath, Graph), FileMode.Create); | ||
var format = Path.GetExtension(output.Name).Equals(".svg", StringComparison.OrdinalIgnoreCase) ? GraphFormat.Svg : GraphFormat.Dot; | ||
graph.WriteAsync(output, format).GetAwaiter().GetResult(); | ||
} | ||
catch (Exception exception) | ||
{ | ||
Log.LogWarningFromException(exception, showStackTrace: true); | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
catch (Exception exception) | ||
{ | ||
Log.LogErrorFromException(exception, showStackTrace: true, showDetail: true, null); | ||
return false; | ||
} | ||
} | ||
} |
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,180 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Drawing; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using CliWrap; | ||
using GiGraph.Dot.Entities.Edges; | ||
using GiGraph.Dot.Entities.Graphs; | ||
using GiGraph.Dot.Extensions; | ||
using GiGraph.Dot.Types.Nodes; | ||
using NuGet.ProjectModel; | ||
|
||
namespace Chisel; | ||
|
||
internal class DependencyGraph | ||
{ | ||
private readonly HashSet<Package> _roots; | ||
private readonly Dictionary<Package, HashSet<Package>> _graph = new(); | ||
private readonly Dictionary<Package, HashSet<Package>> _reverseGraph = new(); | ||
|
||
private static Package CreatePackage(LockFileTargetLibrary library) | ||
{ | ||
var name = library.Name ?? throw new ArgumentException("The library must have a name", nameof(library)); | ||
var version = library.Version?.ToString() ?? throw new ArgumentException("The library must have a version", nameof(library)); | ||
var dependencies = library.Dependencies.Select(e => e.Id).ToList(); | ||
return new Package(name, version, dependencies); | ||
} | ||
|
||
public DependencyGraph(string projectAssetsFile, string targetFramework, string runtimeIdentifier) | ||
{ | ||
var assetsLockFile = new LockFileFormat().Read(projectAssetsFile); | ||
var targetFrameworks = assetsLockFile.PackageSpec.TargetFrameworks.Where(e => e.TargetAlias == targetFramework).ToList(); | ||
var targetFrameworkInformation = targetFrameworks.Count switch | ||
{ | ||
0 => throw new ArgumentException($"Target framework \"{targetFramework}\" is not available in \"{projectAssetsFile}\"", targetFramework), | ||
1 => targetFrameworks[0], | ||
_ => throw new ArgumentException($"Multiple target frameworks match \"{targetFramework}\" in \"{projectAssetsFile}\"", targetFramework), | ||
}; | ||
var target = assetsLockFile.Targets.Single(e => e.TargetFramework == targetFrameworkInformation.FrameworkName && e.RuntimeIdentifier == runtimeIdentifier); | ||
var packages = target.Libraries.ToDictionary(e => e.Name ?? "", CreatePackage); | ||
|
||
_roots = new HashSet<Package>(targetFrameworkInformation.Dependencies.Select(e => packages[e.Name])); | ||
|
||
foreach (var package in packages.Values) | ||
{ | ||
var dependencies = new HashSet<Package>(package.Dependencies.Select(e => packages[e])); | ||
|
||
if (dependencies.Count > 0) | ||
{ | ||
_graph.Add(package, dependencies); | ||
} | ||
|
||
foreach (var dependency in dependencies) | ||
{ | ||
if (_reverseGraph.TryGetValue(dependency, out var reverseDependencies)) | ||
{ | ||
reverseDependencies.Add(package); | ||
} | ||
else | ||
{ | ||
_reverseGraph[dependency] = [package]; | ||
} | ||
} | ||
} | ||
} | ||
|
||
internal HashSet<string> Remove(IEnumerable<string> packages) | ||
{ | ||
var notFound = new List<string>(); | ||
var dependencies = new HashSet<Package>(); | ||
foreach (var packageName in packages) | ||
{ | ||
var packageDependency = _reverseGraph.Keys.SingleOrDefault(e => e.Name == packageName); | ||
if (packageDependency == null) | ||
{ | ||
notFound.Add(packageName); | ||
} | ||
else | ||
{ | ||
dependencies.Add(packageDependency); | ||
} | ||
} | ||
|
||
_ = notFound.Count switch | ||
{ | ||
0 => 0, | ||
1 => throw new ArgumentException($"\"{notFound[0]}\" was not found in the dependency graph.", nameof(packages)), | ||
2 => throw new ArgumentException($"\"{notFound[0]}\" and \"{notFound[1]}\" were not found in the dependency graph.", nameof(packages)), | ||
_ => throw new ArgumentException($"{string.Join(", ", notFound.Take(notFound.Count - 1).Select(e => $"\"{e}\""))} and \"{notFound.Last()}\" were not found in the dependency graph.", nameof(packages)), | ||
}; | ||
|
||
foreach (var dependency in dependencies) | ||
{ | ||
Remove(dependency); | ||
Restore(dependency, dependencies); | ||
} | ||
|
||
return [.._reverseGraph.Keys.Where(e => !e.Keep).Select(e => e.Name)]; | ||
} | ||
|
||
private void Remove(Package package) | ||
{ | ||
package.Keep = false; | ||
if (_graph.TryGetValue(package, out var dependencies)) | ||
{ | ||
foreach (var dependency in dependencies) | ||
{ | ||
Remove(dependency); | ||
} | ||
} | ||
} | ||
|
||
private void Restore(Package package, ICollection<Package> removedPackages) | ||
{ | ||
if ((_reverseGraph[package].Any(e => e.Keep) && !removedPackages.Contains(package)) || _roots.Contains(package)) | ||
{ | ||
package.Keep = true; | ||
} | ||
|
||
if (_graph.TryGetValue(package, out var dependencies)) | ||
{ | ||
foreach (var dependency in dependencies) | ||
{ | ||
Restore(dependency, removedPackages); | ||
} | ||
} | ||
} | ||
|
||
public async Task WriteAsync(Stream output, GraphFormat format) | ||
{ | ||
var dotGraph = new DotGraph | ||
{ | ||
Nodes = | ||
{ | ||
Shape = DotNodeShape.Box, | ||
Style = { FillStyle = DotNodeFillStyle.Normal }, | ||
Font = { Name = "Segoe UI, sans-serif" }, | ||
}, | ||
}; | ||
|
||
foreach (var package in _reverseGraph.Keys.Union(_roots).OrderBy(e => e.Name)) | ||
{ | ||
dotGraph.Nodes.Add(package.Id, node => | ||
{ | ||
node.Color = package.Keep ? Color.Aquamarine : Color.LightCoral; | ||
node.Attributes.Collection.Set("URL", $"https://www.nuget.org/packages/{package.Name}/{package.Version}"); | ||
node.Attributes.Collection.Set("target", "_blank"); | ||
}); | ||
} | ||
|
||
foreach (var (package, dependencies) in _graph.Select(e => (e.Key, e.Value))) | ||
{ | ||
foreach (var dependency in dependencies) | ||
{ | ||
dotGraph.Edges.Add(new DotEdge(package.Id, dependency.Id)); | ||
} | ||
} | ||
|
||
void WriteGraph(Stream stream) | ||
{ | ||
using var writer = new StreamWriter(stream); | ||
dotGraph.Build(writer); | ||
} | ||
|
||
if (format == GraphFormat.Dot) | ||
{ | ||
WriteGraph(output); | ||
return; | ||
} | ||
|
||
var input = PipeSource.Create(WriteGraph); | ||
|
||
var dot = Cli.Wrap("dot") | ||
.WithArguments([ $"-T{format.ToString().ToLowerInvariant()}" ]) | ||
.WithStandardErrorPipe(PipeTarget.ToDelegate(Console.Error.WriteLine)); | ||
|
||
await (input | dot | output).ExecuteAsync(); | ||
} | ||
} |
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,7 @@ | ||
namespace Chisel; | ||
|
||
internal enum GraphFormat | ||
{ | ||
Dot, | ||
Svg, | ||
} |
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,42 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
|
||
namespace Chisel; | ||
|
||
[DebuggerDisplay("{Name}/{Version}")] | ||
internal class Package : IEquatable<Package> | ||
{ | ||
public Package(string name, string version, IReadOnlyCollection<string> dependencies) | ||
{ | ||
Name = name; | ||
Version = version; | ||
Dependencies = dependencies; | ||
} | ||
|
||
public string Name { get; } | ||
public string Version { get; } | ||
public IReadOnlyCollection<string> Dependencies { get; } | ||
|
||
public string Id => $"{Name}/{Version}"; | ||
|
||
public bool Keep { get; set; } = true; | ||
|
||
public override string ToString() => Name; | ||
|
||
public bool Equals(Package? other) | ||
{ | ||
if (ReferenceEquals(null, other)) return false; | ||
if (ReferenceEquals(this, other)) return true; | ||
return Name == other.Name; | ||
} | ||
|
||
public override bool Equals(object? obj) | ||
{ | ||
if (ReferenceEquals(null, obj)) return false; | ||
if (ReferenceEquals(this, obj)) return true; | ||
return obj.GetType() == GetType() && Equals((Package)obj); | ||
} | ||
|
||
public override int GetHashCode() => Name.GetHashCode(); | ||
} |
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,7 @@ | ||
<Project> | ||
|
||
<PropertyGroup> | ||
<ChiselEnabled Condition="$(ChiselEnabled) == ''">true</ChiselEnabled> | ||
</PropertyGroup> | ||
|
||
</Project> |
Oops, something went wrong.