-
Notifications
You must be signed in to change notification settings - Fork 94
/
Copy pathNuGetMSBuildBinaryLogComponentDetector.cs
224 lines (196 loc) · 9.46 KB
/
NuGetMSBuildBinaryLogComponentDetector.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
namespace Microsoft.ComponentDetection.Detectors.NuGet;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.Build.Locator;
using Microsoft.Build.Logging.StructuredLogger;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.Internal;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.Extensions.Logging;
using Task = System.Threading.Tasks.Task;
public class NuGetMSBuildBinaryLogComponentDetector : FileComponentDetector
{
private static readonly HashSet<string> TopLevelPackageItemNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"PackageReference",
};
// the items listed below represent collection names that NuGet will resolve a package into, along with the metadata value names to get the package name and version
private static readonly Dictionary<string, (string NameMetadata, string VersionMetadata)> ResolvedPackageItemNames = new Dictionary<string, (string, string)>(StringComparer.OrdinalIgnoreCase)
{
["NativeCopyLocalItems"] = ("NuGetPackageId", "NuGetPackageVersion"),
["ResourceCopyLocalItems"] = ("NuGetPackageId", "NuGetPackageVersion"),
["RuntimeCopyLocalItems"] = ("NuGetPackageId", "NuGetPackageVersion"),
["ResolvedAnalyzers"] = ("NuGetPackageId", "NuGetPackageVersion"),
["_PackageDependenciesDesignTime"] = ("Name", "Version"),
};
private static readonly object MSBuildRegistrationGate = new();
private static bool isMSBuildRegistered;
public NuGetMSBuildBinaryLogComponentDetector(
IObservableDirectoryWalkerFactory walkerFactory,
ILogger<NuGetMSBuildBinaryLogComponentDetector> logger)
{
this.Scanner = walkerFactory;
this.Logger = logger;
}
public override string Id { get; } = "NuGetMSBuildBinaryLog";
public override IEnumerable<string> Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.NuGet) };
public override IList<string> SearchPatterns { get; } = new List<string> { "*.binlog" };
public override IEnumerable<ComponentType> SupportedComponentTypes { get; } = new[] { ComponentType.NuGet };
public override int Version { get; } = 1;
private static void ProcessResolvedPackageReference(Dictionary<string, HashSet<string>> topLevelDependencies, Dictionary<string, Dictionary<string, string>> projectResolvedDependencies, NamedNode node)
{
var doRemoveOperation = node is RemoveItem;
var doAddOperation = node is AddItem;
if (TopLevelPackageItemNames.Contains(node.Name))
{
var projectEvaluation = node.GetNearestParent<ProjectEvaluation>();
if (projectEvaluation is not null)
{
foreach (var child in node.Children.OfType<Item>())
{
var packageName = child.Name;
if (!topLevelDependencies.TryGetValue(projectEvaluation.ProjectFile, out var topLevel))
{
topLevel = new(StringComparer.OrdinalIgnoreCase);
topLevelDependencies[projectEvaluation.ProjectFile] = topLevel;
}
if (doRemoveOperation)
{
topLevel.Remove(packageName);
}
if (doAddOperation)
{
topLevel.Add(packageName);
}
}
}
}
else if (ResolvedPackageItemNames.TryGetValue(node.Name, out var metadataNames))
{
var nameMetadata = metadataNames.NameMetadata;
var versionMetadata = metadataNames.VersionMetadata;
var originalProject = node.GetNearestParent<Project>();
if (originalProject is not null)
{
foreach (var child in node.Children.OfType<Item>())
{
var packageName = GetChildMetadataValue(child, nameMetadata);
var packageVersion = GetChildMetadataValue(child, versionMetadata);
if (packageName is not null && packageVersion is not null)
{
var project = originalProject;
while (project is not null)
{
if (!projectResolvedDependencies.TryGetValue(project.ProjectFile, out var projectDependencies))
{
projectDependencies = new(StringComparer.OrdinalIgnoreCase);
projectResolvedDependencies[project.ProjectFile] = projectDependencies;
}
if (doRemoveOperation)
{
projectDependencies.Remove(packageName);
}
if (doAddOperation)
{
projectDependencies[packageName] = packageVersion;
}
project = project.GetNearestParent<Project>();
}
}
}
}
}
}
private static string GetChildMetadataValue(TreeNode node, string metadataItemName)
{
var metadata = node.Children.OfType<Metadata>();
var metadataValue = metadata.FirstOrDefault(m => m.Name.Equals(metadataItemName, StringComparison.OrdinalIgnoreCase))?.Value;
return metadataValue;
}
protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary<string, string> detectorArgs, CancellationToken cancellationToken = default)
{
try
{
lock (MSBuildRegistrationGate)
{
if (!isMSBuildRegistered)
{
// this must happen once per process, and never again
var defaultInstance = MSBuildLocator.QueryVisualStudioInstances().First();
MSBuildLocator.RegisterInstance(defaultInstance);
isMSBuildRegistered = true;
}
}
var singleFileComponentRecorder = this.ComponentRecorder.CreateSingleFileComponentRecorder(processRequest.ComponentStream.Location);
var buildRoot = BinaryLog.ReadBuild(processRequest.ComponentStream.Stream);
this.RecordLockfileVersion(buildRoot.FileFormatVersion);
this.ProcessBinLog(buildRoot, singleFileComponentRecorder);
}
catch (Exception e)
{
// If something went wrong, just ignore the package
this.Logger.LogError(e, "Failed to process MSBuild binary log {BinLogFile}", processRequest.ComponentStream.Location);
}
return Task.CompletedTask;
}
protected override Task OnDetectionFinishedAsync()
{
return Task.CompletedTask;
}
private void ProcessBinLog(Build buildRoot, ISingleFileComponentRecorder componentRecorder)
{
// maps a project path to a set of resolved dependencies
var projectTopLevelDependencies = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
var projectResolvedDependencies = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
buildRoot.VisitAllChildren<BaseNode>(node =>
{
switch (node)
{
case NamedNode namedNode when namedNode is AddItem or RemoveItem:
ProcessResolvedPackageReference(projectTopLevelDependencies, projectResolvedDependencies, namedNode);
break;
default:
break;
}
});
// dependencies were resolved per project, we need to re-arrange them to be per package/version
var projectsPerPackage = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var projectPath in projectResolvedDependencies.Keys)
{
if (Path.GetExtension(projectPath).Equals(".sln", StringComparison.OrdinalIgnoreCase))
{
// don't report solution files
continue;
}
var projectDependencies = projectResolvedDependencies[projectPath];
foreach (var (packageName, packageVersion) in projectDependencies)
{
var key = $"{packageName}/{packageVersion}";
if (!projectsPerPackage.TryGetValue(key, out var projectPaths))
{
projectPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
projectsPerPackage[key] = projectPaths;
}
projectPaths.Add(projectPath);
}
}
// report it all
foreach (var packageNameAndVersion in projectsPerPackage.Keys.OrderBy(p => p))
{
var projectPaths = projectsPerPackage[packageNameAndVersion];
var parts = packageNameAndVersion.Split('/', 2);
var packageName = parts[0];
var packageVersion = parts[1];
var component = new NuGetComponent(packageName, packageVersion);
var libraryComponent = new DetectedComponent(component);
foreach (var projectPath in projectPaths)
{
libraryComponent.FilePaths.Add(projectPath);
}
componentRecorder.RegisterUsage(libraryComponent);
}
}
}