Skip to content

Commit ee30155

Browse files
jdotson166Jacob Dotsongrvillic
authored
Updates to the VCPKG detector to use manifest-info.json for resolving vcpkg.json location. (#1436)
* Vcpkg detection enhancement to report vcpkg.json file #1408 --------- Co-authored-by: Jacob Dotson <[email protected]> Co-authored-by: Greg Villicana <[email protected]>
1 parent 5289cf1 commit ee30155

File tree

3 files changed

+172
-3
lines changed

3 files changed

+172
-3
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Microsoft.ComponentDetection.Detectors.Vcpkg.Contracts;
2+
3+
using Newtonsoft.Json;
4+
5+
public class ManifestInfo
6+
{
7+
[JsonProperty("manifest-path")]
8+
public string ManifestPath { get; set; }
9+
}

src/Microsoft.ComponentDetection.Detectors/vcpkg/VcpkgComponentDetector.cs

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
namespace Microsoft.ComponentDetection.Detectors.Vcpkg;
22

33
using System;
4+
using System.Collections.Concurrent;
45
using System.Collections.Generic;
56
using System.IO;
67
using System.Linq;
8+
using System.Reactive.Linq;
79
using System.Threading;
810
using System.Threading.Tasks;
911
using Microsoft.ComponentDetection.Contracts;
@@ -15,7 +17,11 @@ namespace Microsoft.ComponentDetection.Detectors.Vcpkg;
1517

1618
public class VcpkgComponentDetector : FileComponentDetector
1719
{
20+
private const string VcpkgInstalledFolder = "vcpkg_installed";
21+
private const string ManifestInfoFile = "manifest-info.json";
22+
1823
private readonly HashSet<string> projectRoots = [];
24+
private readonly ConcurrentDictionary<string, string> manifestMappings = new(StringComparer.OrdinalIgnoreCase);
1925

2026
private readonly ICommandLineInvocationService commandLineInvocationService;
2127
private readonly IEnvironmentVariableService envVarService;
@@ -38,11 +44,11 @@ public VcpkgComponentDetector(
3844

3945
public override IEnumerable<string> Categories => [Enum.GetName(typeof(DetectorClass), DetectorClass.Vcpkg)];
4046

41-
public override IList<string> SearchPatterns { get; } = ["vcpkg.spdx.json"];
47+
public override IList<string> SearchPatterns { get; } = ["vcpkg.spdx.json", ManifestInfoFile];
4248

4349
public override IEnumerable<ComponentType> SupportedComponentTypes { get; } = [ComponentType.Vcpkg];
4450

45-
public override int Version => 2;
51+
public override int Version => 3;
4652

4753
protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary<string, string> detectorArgs, CancellationToken cancellationToken = default)
4854
{
@@ -57,7 +63,44 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID
5763
return;
5864
}
5965

60-
await this.ParseSpdxFileAsync(singleFileComponentRecorder, file);
66+
await this.ParseSpdxFileAsync(this.GetManifestComponentRecorder(singleFileComponentRecorder), file);
67+
}
68+
69+
protected override async Task<IObservable<ProcessRequest>> OnPrepareDetectionAsync(IObservable<ProcessRequest> processRequests, IDictionary<string, string> detectorArgs, CancellationToken cancellationToken = default)
70+
{
71+
var filteredProcessRequests = new List<ProcessRequest>();
72+
73+
await processRequests.ForEachAsync(async pr =>
74+
{
75+
var fileLocation = pr.ComponentStream.Location;
76+
var fileName = Path.GetFileName(fileLocation);
77+
78+
if (fileName.Equals(ManifestInfoFile, StringComparison.OrdinalIgnoreCase))
79+
{
80+
this.Logger.LogDebug("Discovered VCPKG package manifest file at: {Location}", pr.ComponentStream.Location);
81+
82+
using (var reader = new StreamReader(pr.ComponentStream.Stream))
83+
{
84+
var contents = await reader.ReadToEndAsync().ConfigureAwait(false);
85+
var manifestData = JsonConvert.DeserializeObject<ManifestInfo>(contents);
86+
87+
if (manifestData == null || string.IsNullOrWhiteSpace(manifestData.ManifestPath))
88+
{
89+
this.Logger.LogDebug("Failed to deserialize manifest-info.json or missing ManifestPath at {Path}", pr.ComponentStream.Location);
90+
}
91+
else
92+
{
93+
this.manifestMappings.TryAdd(fileLocation, manifestData.ManifestPath);
94+
}
95+
}
96+
}
97+
else
98+
{
99+
filteredProcessRequests.Add(pr);
100+
}
101+
}).ConfigureAwait(false);
102+
103+
return filteredProcessRequests.ToObservable();
61104
}
62105

63106
private async Task ParseSpdxFileAsync(
@@ -123,4 +166,64 @@ private async Task ParseSpdxFileAsync(
123166
}
124167
}
125168
}
169+
170+
/// <summary>
171+
/// Attempts to resolve and return a manifest component recorder for the given recorder.
172+
/// Returns the matching manifest component recorder if found; otherwise, returns the original recorder.
173+
/// </summary>
174+
private ISingleFileComponentRecorder GetManifestComponentRecorder(ISingleFileComponentRecorder singleFileComponentRecorder)
175+
{
176+
try
177+
{
178+
var manifestFileLocation = singleFileComponentRecorder.ManifestFileLocation;
179+
180+
var vcpkgInstalledIndex = manifestFileLocation.IndexOf(VcpkgInstalledFolder, StringComparison.OrdinalIgnoreCase);
181+
if (vcpkgInstalledIndex < 0)
182+
{
183+
this.Logger.LogDebug(
184+
"Could not find '{VcpkgInstalled}' in ManifestFileLocation: '{ManifestFileLocation}'. Returning original recorder.",
185+
VcpkgInstalledFolder,
186+
manifestFileLocation);
187+
188+
return singleFileComponentRecorder;
189+
}
190+
191+
var vcpkgInstalledDir = manifestFileLocation[..(vcpkgInstalledIndex + VcpkgInstalledFolder.Length)];
192+
193+
var preferredManifest = Path.Combine(vcpkgInstalledDir, "vcpkg", ManifestInfoFile);
194+
var fallbackManifest = Path.Combine(vcpkgInstalledDir, ManifestInfoFile);
195+
196+
// Try preferred location first
197+
if (this.manifestMappings.TryGetValue(preferredManifest, out var manifestPath) && manifestPath != null)
198+
{
199+
return this.ComponentRecorder.CreateSingleFileComponentRecorder(manifestPath);
200+
}
201+
else if (this.manifestMappings.TryGetValue(fallbackManifest, out manifestPath) && manifestPath != null)
202+
{
203+
// Use the fallback location.
204+
this.Logger.LogDebug(
205+
"Preferred manifest at '{PreferredManifest}' was not found or invalid. Using fallback manifest at '{FallbackManifest}'.",
206+
preferredManifest,
207+
fallbackManifest);
208+
209+
return this.ComponentRecorder.CreateSingleFileComponentRecorder(manifestPath);
210+
}
211+
212+
this.Logger.LogDebug(
213+
"No valid manifest-info.json found at either '{PreferredManifest}' or '{FallbackManifest}' for base location '{VcpkgInstalledDir}'. Returning original recorder.",
214+
preferredManifest,
215+
fallbackManifest,
216+
vcpkgInstalledDir);
217+
}
218+
catch (Exception ex)
219+
{
220+
this.Logger.LogWarning(
221+
ex,
222+
"An exception occurred while resolving manifest component recorder for '{ManifestFileLocation}'. Returning original recorder.",
223+
singleFileComponentRecorder.ManifestFileLocation);
224+
}
225+
226+
// Always return the original recorder if no manifest is found or on error
227+
return singleFileComponentRecorder;
228+
}
126229
}

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace Microsoft.ComponentDetection.Detectors.Tests;
22

3+
using System;
34
using System.Collections.Generic;
45
using System.IO;
56
using System.Linq;
@@ -177,4 +178,60 @@ public async Task TestInvalidFileAsync()
177178
var components = detectedComponents.ToList();
178179
components.Should().BeEmpty();
179180
}
181+
182+
[TestMethod]
183+
[DataTestMethod]
184+
[DataRow("vcpkg_installed\\manifest-info.json", "vcpkg.json")]
185+
[DataRow("vcpkg_installed\\vcpkg\\manifest-info.json", "vcpkg.json")]
186+
[DataRow("bad_location\\manifest-info.json", "vcpkg_installed\\packageLocation\\vcpkg.spdx.json")]
187+
public async Task TestVcpkgManifestFileAsync(string manifestPath, string pathToVcpkg)
188+
{
189+
var t_pathToVcpkg = CrossPlatformPath(Path.GetFullPath(pathToVcpkg));
190+
var t_manifestPath = CrossPlatformPath(Path.GetFullPath(manifestPath));
191+
192+
var spdxFile = @"{
193+
""SPDXID"": ""SPDXRef - DOCUMENT"",
194+
""documentNamespace"":
195+
""https://spdx.org/spdxdocs/nlohmann-json-x64-linux-3.10.4-78c7f190-b402-44d1-a364-b9ac86392b84"",
196+
""name"": ""nlohmann-json:[email protected] 69dcfc6886529ad2d210f71f132d743672a7e65d2c39f53456f17fc5fc08b278"",
197+
""packages"": [
198+
{
199+
""name"": ""nlohmann-json"",
200+
""SPDXID"": ""SPDXRef-port"",
201+
""versionInfo"": ""3.10.4#5"",
202+
""downloadLocation"": ""git+https://github.com/Microsoft/vcpkg#ports/nlohmann-json"",
203+
""homepage"": ""https://github.com/nlohmann/json"",
204+
""licenseConcluded"": ""NOASSERTION"",
205+
""licenseDeclared"": ""NOASSERTION"",
206+
""copyrightText"": ""NOASSERTION"",
207+
""description"": ""JSON for Modern C++"",
208+
""comment"": ""This is the port (recipe) consumed by vcpkg.""
209+
}
210+
]
211+
}";
212+
var manifestFile = $@"{{
213+
""manifest-path"": ""{t_pathToVcpkg.Replace("\\", "\\\\")}""
214+
}}";
215+
216+
var (scanResult, componentRecorder) = await this.DetectorTestUtility
217+
.WithFile(CrossPlatformPath(Path.GetFullPath("vcpkg_installed\\packageLocation\\vcpkg.spdx.json")), spdxFile)
218+
.WithFile(t_manifestPath, manifestFile)
219+
.ExecuteDetectorAsync();
220+
221+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
222+
223+
var detectedComponents = componentRecorder.GetDependencyGraphsByLocation();
224+
225+
var singleFileComponent = detectedComponents.FirstOrDefault();
226+
singleFileComponent.Should().NotBeNull();
227+
228+
var expectedResult = singleFileComponent.Key.Replace("/tmp/", string.Empty);
229+
expectedResult.Should().Be(t_pathToVcpkg);
230+
}
231+
232+
private static string CrossPlatformPath(string relPath)
233+
{
234+
var segments = relPath.Split(['\\', '/'], StringSplitOptions.RemoveEmptyEntries);
235+
return Path.Combine(segments);
236+
}
180237
}

0 commit comments

Comments
 (0)