Skip to content

Commit ffc2ece

Browse files
committed
allow the same package with multiple versions
This corresponds to a project with multiple TFMs where the same package is imported in each case, but with a different version each time.
1 parent aa9d767 commit ffc2ece

File tree

3 files changed

+69
-14
lines changed

3 files changed

+69
-14
lines changed

src/Microsoft.ComponentDetection.Detectors/nuget/NuGetMSBuildBinaryLogComponentDetector.cs

+21-12
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public NuGetMSBuildBinaryLogComponentDetector(
5858

5959
public override int Version { get; } = 1;
6060

61-
private static void ProcessResolvedPackageReference(Dictionary<string, HashSet<string>> topLevelDependencies, Dictionary<string, Dictionary<string, string>> projectResolvedDependencies, NamedNode node)
61+
private static void ProcessResolvedPackageReference(Dictionary<string, HashSet<string>> topLevelDependencies, Dictionary<string, Dictionary<string, HashSet<string>>> projectResolvedDependencies, NamedNode node)
6262
{
6363
var doRemoveOperation = node is RemoveItem;
6464
var doAddOperation = node is AddItem;
@@ -110,14 +110,20 @@ private static void ProcessResolvedPackageReference(Dictionary<string, HashSet<s
110110
projectResolvedDependencies[project.ProjectFile] = projectDependencies;
111111
}
112112

113+
if (!projectDependencies.TryGetValue(packageName, out var packageVersions))
114+
{
115+
packageVersions = new(StringComparer.OrdinalIgnoreCase);
116+
projectDependencies[packageName] = packageVersions;
117+
}
118+
113119
if (doRemoveOperation)
114120
{
115-
projectDependencies.Remove(packageName);
121+
packageVersions.Remove(packageVersion);
116122
}
117123

118124
if (doAddOperation)
119125
{
120-
projectDependencies[packageName] = packageVersion;
126+
packageVersions.Add(packageVersion);
121127
}
122128

123129
project = project.GetNearestParent<Project>();
@@ -183,7 +189,7 @@ private void ProcessBinLog(Build buildRoot, ISingleFileComponentRecorder compone
183189
{
184190
// maps a project path to a set of resolved dependencies
185191
var projectTopLevelDependencies = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
186-
var projectResolvedDependencies = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
192+
var projectResolvedDependencies = new Dictionary<string, Dictionary<string, HashSet<string>>>(StringComparer.OrdinalIgnoreCase);
187193
buildRoot.VisitAllChildren<BaseNode>(node =>
188194
{
189195
switch (node)
@@ -198,7 +204,7 @@ private void ProcessBinLog(Build buildRoot, ISingleFileComponentRecorder compone
198204

199205
// dependencies were resolved per project, we need to re-arrange them to be per package/version
200206
var projectsPerPackage = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
201-
foreach (var projectPath in projectResolvedDependencies.Keys)
207+
foreach (var projectPath in projectResolvedDependencies.Keys.OrderBy(p => p))
202208
{
203209
if (Path.GetExtension(projectPath).Equals(".sln", StringComparison.OrdinalIgnoreCase))
204210
{
@@ -207,16 +213,19 @@ private void ProcessBinLog(Build buildRoot, ISingleFileComponentRecorder compone
207213
}
208214

209215
var projectDependencies = projectResolvedDependencies[projectPath];
210-
foreach (var (packageName, packageVersion) in projectDependencies)
216+
foreach (var (packageName, packageVersions) in projectDependencies.OrderBy(p => p.Key))
211217
{
212-
var key = $"{packageName}/{packageVersion}";
213-
if (!projectsPerPackage.TryGetValue(key, out var projectPaths))
218+
foreach (var packageVersion in packageVersions.OrderBy(v => v))
214219
{
215-
projectPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
216-
projectsPerPackage[key] = projectPaths;
217-
}
220+
var key = $"{packageName}/{packageVersion}";
221+
if (!projectsPerPackage.TryGetValue(key, out var projectPaths))
222+
{
223+
projectPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
224+
projectsPerPackage[key] = projectPaths;
225+
}
218226

219-
projectPaths.Add(projectPath);
227+
projectPaths.Add(projectPath);
228+
}
220229
}
221230
}
222231

test/Microsoft.ComponentDetection.Detectors.Tests/MSBuildTestUtilities.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public static class MSBuildTestUtilities
5858
public static async Task<Stream> GetBinLogStreamFromFileContentsAsync(
5959
string defaultFilePath,
6060
string defaultFileContents,
61+
string targetName = null,
6162
(string FileName, string Contents)[] additionalFiles = null,
6263
(string Name, string Version, string TargetFramework, string AdditionalMetadataXml)[] mockedPackages = null)
6364
{
@@ -79,7 +80,8 @@ public static async Task<Stream> GetBinLogStreamFromFileContentsAsync(
7980
await MockNuGetPackagesInDirectoryAsync(tempDir, mockedPackages);
8081

8182
// generate the binlog
82-
var (exitCode, stdOut, stdErr) = await RunProcessAsync("dotnet", $"build \"{fullDefaultFilePath}\" /t:GenerateBuildDependencyFile /bl:msbuild.binlog", workingDirectory: tempDir.DirectoryPath);
83+
targetName ??= "GenerateBuildDependencyFile";
84+
var (exitCode, stdOut, stdErr) = await RunProcessAsync("dotnet", $"build \"{fullDefaultFilePath}\" /t:{targetName} /bl:msbuild.binlog", workingDirectory: tempDir.DirectoryPath);
8385
exitCode.Should().Be(0, $"STDOUT:\n{stdOut}\n\nSTDERR:\n{stdErr}");
8486

8587
// copy it to memory so the temporary directory can be cleaned up

test/Microsoft.ComponentDetection.Detectors.Tests/NuGetMSBuildBinaryLogComponentDetectorTests.cs

+45-1
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,49 @@ public async Task PackagesReportedFromSeparateProjectsDoNotOverlap()
214214
solutionComponents.Should().BeEmpty();
215215
}
216216

217+
[TestMethod]
218+
public async Task PackagesAreReportedWhenConditionedOnTargetFramework()
219+
{
220+
// This test simulates a project with multiple TFMs where the same package is imported in each, but with a
221+
// different version. To avoid building the entire project, we can fake what MSBuild does by resolving
222+
// packages with each TFM. I manually verified that a "real" invocation of MSBuild with the "Build" target
223+
// produces the same shape of the binlog as this test generates.
224+
225+
// The end result is that _all_ packages are reported, regardless of the TFM invokation they came from, and in
226+
// this case that is good, because we really only care about what packages were used in the build and what
227+
// project file they came from.
228+
var (scanResult, componentRecorder) = await this.ExecuteDetectorAndGetBinLogAsync(
229+
projectContents: $@"
230+
<Project Sdk=""Microsoft.NET.Sdk"">
231+
<PropertyGroup>
232+
<TargetFrameworks>netstandard2.0;{MSBuildTestUtilities.TestTargetFramework}</TargetFrameworks>
233+
</PropertyGroup>
234+
<ItemGroup>
235+
<PackageReference Include=""Some.Package"" Version=""1.2.3"" Condition=""'$(TargetFramework)' == 'netstandard2.0'"" />
236+
<PackageReference Include=""Some.Package"" Version=""4.5.6"" Condition=""'$(TargetFramework)' == '{MSBuildTestUtilities.TestTargetFramework}'"" />
237+
</ItemGroup>
238+
<Target Name=""TEST_GenerateBuildDependencyFileForTargetFrameworks"">
239+
<MSBuild Projects=""$(MSBuildThisFile)"" Properties=""TargetFramework=netstandard2.0"" Targets=""GenerateBuildDependencyFile"" />
240+
<MSBuild Projects=""$(MSBuildThisFile)"" Properties=""TargetFramework={MSBuildTestUtilities.TestTargetFramework}"" Targets=""GenerateBuildDependencyFile"" />
241+
</Target>
242+
</Project>
243+
",
244+
targetName: "TEST_GenerateBuildDependencyFileForTargetFrameworks",
245+
mockedPackages: new[]
246+
{
247+
("NETStandard.Library", "2.0.3", "netstandard2.0", "<dependencies />"),
248+
("Some.Package", "1.2.3", "netstandard2.0", null),
249+
("Some.Package", "4.5.6", MSBuildTestUtilities.TestTargetFramework, null),
250+
});
251+
252+
var detectedComponents = componentRecorder.GetDetectedComponents();
253+
254+
var packages = detectedComponents
255+
.Where(d => d.FilePaths.Any(p => p.Replace("\\", "/").EndsWith("/project.csproj")))
256+
.Select(d => d.Component).Cast<NuGetComponent>().OrderBy(c => c.Name).ThenBy(c => c.Version).Select(c => $"{c.Name}/{c.Version}");
257+
packages.Should().Equal("NETStandard.Library/2.0.3", "Some.Package/1.2.3", "Some.Package/4.5.6");
258+
}
259+
217260
[TestMethod]
218261
public async Task PackagesImplicitlyAddedBySdkDuringPublishAreAdded()
219262
{
@@ -428,10 +471,11 @@ public async Task PackagesImplicitlyAddedBySdkDuringPublishAreAdded()
428471

429472
private async Task<(IndividualDetectorScanResult ScanResult, IComponentRecorder ComponentRecorder)> ExecuteDetectorAndGetBinLogAsync(
430473
string projectContents,
474+
string targetName = null,
431475
(string FileName, string Content)[] additionalFiles = null,
432476
(string Name, string Version, string TargetFramework, string DependenciesXml)[] mockedPackages = null)
433477
{
434-
using var binLogStream = await MSBuildTestUtilities.GetBinLogStreamFromFileContentsAsync("project.csproj", projectContents, additionalFiles: additionalFiles, mockedPackages: mockedPackages);
478+
using var binLogStream = await MSBuildTestUtilities.GetBinLogStreamFromFileContentsAsync("project.csproj", projectContents, targetName: targetName, additionalFiles: additionalFiles, mockedPackages: mockedPackages);
435479
var (scanResult, componentRecorder) = await this.DetectorTestUtility
436480
.WithFile("msbuild.binlog", binLogStream)
437481
.ExecuteDetectorAsync();

0 commit comments

Comments
 (0)