Feature: Add conan detector that parses conan.lock files of conan package manager version 1.x (#692)

Co-authored-by: Justin Perez <justinmp@vt.edu>
Co-authored-by: Ashok Gowtham Mathivanan <amathivanan@lenovo.com>
This commit is contained in:
Ashok Gowtham M 2023-08-09 20:23:52 +05:30 коммит произвёл GitHub
Родитель 00cc3b9080
Коммит 9de5ba459e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
16 изменённых файлов: 638 добавлений и 2 удалений

11
docs/detectors/conan.md Normal file
Просмотреть файл

@ -0,0 +1,11 @@
# Conan Detection
## Requirements
Conan detection relies on a conan.lock file being present.
## Detection strategy
Conan detection is performed by parsing every **conan.lock** found under the scan directory.
## Known limitations
Conan detection will not work if lock files are not being used or not yet generated. So ensure to run the conan build to generate the lock file(s) before running the scan.
Full dependency graph generation is not supported. However, dependency relationships identified/present in the **conan.lock** file is captured.

Просмотреть файл

@ -17,6 +17,7 @@ public class TypedComponentConverter : JsonConverter
{ ComponentType.Git, typeof(GitComponent) },
{ ComponentType.RubyGems, typeof(RubyGemsComponent) },
{ ComponentType.Cargo, typeof(CargoComponent) },
{ ComponentType.Conan, typeof(ConanComponent) },
{ ComponentType.Pip, typeof(PipComponent) },
{ ComponentType.Go, typeof(GoComponent) },
{ ComponentType.DockerImage, typeof(DockerImageComponent) },

Просмотреть файл

@ -53,4 +53,7 @@ public enum ComponentType : byte
[EnumMember]
DockerReference = 16,
[EnumMember]
Conan = 17,
}

Просмотреть файл

@ -0,0 +1,27 @@
namespace Microsoft.ComponentDetection.Contracts.TypedComponent;
using PackageUrl;
public class ConanComponent : TypedComponent
{
private ConanComponent()
{
// reserved for deserialization
}
public ConanComponent(string name, string version)
{
this.Name = this.ValidateRequiredInput(name, nameof(this.Name), nameof(ComponentType.Conan));
this.Version = this.ValidateRequiredInput(version, nameof(this.Version), nameof(ComponentType.Conan));
}
public string Name { get; set; }
public string Version { get; set; }
public override ComponentType Type => ComponentType.Conan;
public override string Id => $"{this.Name} {this.Version} - {this.Type}";
public override PackageURL PackageUrl => new PackageURL("conan", string.Empty, this.Name, this.Version, null, string.Empty);
}

Просмотреть файл

@ -0,0 +1,91 @@
namespace Microsoft.ComponentDetection.Detectors.Conan;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.Internal;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.ComponentDetection.Detectors.Conan.Contracts;
using Microsoft.Extensions.Logging;
public class ConanLockComponentDetector : FileComponentDetector, IDefaultOffComponentDetector
{
public ConanLockComponentDetector(
IComponentStreamEnumerableFactory componentStreamEnumerableFactory,
IObservableDirectoryWalkerFactory walkerFactory,
ILogger<ConanLockComponentDetector> logger)
{
this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
this.Scanner = walkerFactory;
this.Logger = logger;
}
public override string Id => "ConanLock";
public override IList<string> SearchPatterns => new List<string> { "conan.lock" };
public override IEnumerable<ComponentType> SupportedComponentTypes => new[] { ComponentType.Conan };
public override int Version { get; } = 1;
public override IEnumerable<string> Categories => new List<string> { "Conan" };
protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary<string, string> detectorArgs)
{
var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
var conanLockFile = processRequest.ComponentStream;
try
{
var conanLock = await JsonSerializer.DeserializeAsync<ConanLock>(conanLockFile.Stream);
this.RecordLockfileVersion(conanLock.Version);
if (!conanLock.HasNodes())
{
return;
}
var packagesDictionary = conanLock.GraphLock.Nodes;
var explicitReferencedDependencies = new HashSet<string>();
var developmentDependencies = new HashSet<string>();
if (packagesDictionary.ContainsKey("0"))
{
packagesDictionary.Remove("0", out var rootNode);
if (rootNode?.Requires != null)
{
explicitReferencedDependencies = new HashSet<string>(rootNode.Requires);
}
if (rootNode?.BuildRequires != null)
{
developmentDependencies = new HashSet<string>(rootNode.BuildRequires);
}
}
foreach (var (packageIndex, package) in packagesDictionary)
{
singleFileComponentRecorder.RegisterUsage(
new DetectedComponent(package.ToComponent()),
isExplicitReferencedDependency: explicitReferencedDependencies.Contains(packageIndex),
isDevelopmentDependency: developmentDependencies.Contains(packageIndex));
}
foreach (var (conanPackageIndex, package) in packagesDictionary)
{
var parentPackages = packagesDictionary.Values.Where(package => package.Requires?.Contains(conanPackageIndex) == true);
foreach (var parentPackage in parentPackages)
{
singleFileComponentRecorder.RegisterUsage(new DetectedComponent(package.ToComponent()), false, parentPackage.ToComponent().Id, isDevelopmentDependency: false);
}
}
}
catch (Exception e)
{
// If something went wrong, just ignore the file
this.Logger.LogError(e, "Failed to process conan.lock file '{ConanLockLocation}'", conanLockFile.Location);
}
}
}

Просмотреть файл

@ -0,0 +1,20 @@
namespace Microsoft.ComponentDetection.Detectors.Conan.Contracts;
using System.Text.Json.Serialization;
public class ConanLock
{
[JsonPropertyName("version")]
public string Version { get; set; }
[JsonPropertyName("profile_host")]
public string ProfileHost { get; set; }
[JsonPropertyName("profile_build")]
public string ProfileBuild { get; set; }
[JsonPropertyName("graph_lock")]
public ConanLockGraph GraphLock { get; set; }
internal bool HasNodes() => this.GraphLock?.Nodes?.Count > 0;
}

Просмотреть файл

@ -0,0 +1,13 @@
namespace Microsoft.ComponentDetection.Detectors.Conan.Contracts;
using System.Collections.Generic;
using System.Text.Json.Serialization;
public class ConanLockGraph
{
[JsonPropertyName("revisions_enabled")]
public bool RevisionsEnabled { get; set; }
[JsonPropertyName("nodes")]
public Dictionary<string, ConanLockNode> Nodes { get; set; }
}

Просмотреть файл

@ -0,0 +1,42 @@
namespace Microsoft.ComponentDetection.Detectors.Conan.Contracts;
using System;
using System.Linq;
using System.Text.Json.Serialization;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
public class ConanLockNode
{
[JsonPropertyName("context")]
public string Context { get; set; }
[JsonPropertyName("modified")]
public bool? Modified { get; set; }
[JsonPropertyName("options")]
public string Options { get; set; }
[JsonPropertyName("package_id")]
public string PackageId { get; set; }
[JsonPropertyName("path")]
public string Path { get; set; }
[JsonPropertyName("prev")]
public string Previous { get; set; }
[JsonPropertyName("ref")]
public string Reference { get; set; }
[JsonPropertyName("requires")]
public string[] Requires { get; set; }
[JsonPropertyName("build_requires")]
public string[] BuildRequires { get; set; }
internal string Name() => this.Reference == null ? string.Empty : this.Reference.Split('/', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).FirstOrDefault("Unknown");
internal TypedComponent ToComponent() => new ConanComponent(this.Name(), this.Version());
internal string Version() => this.Reference == null ? string.Empty : this.Reference.Split('/', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Skip(1).FirstOrDefault("None");
}

Просмотреть файл

@ -4,6 +4,7 @@ using Microsoft.ComponentDetection.Common;
using Microsoft.ComponentDetection.Common.Telemetry;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Detectors.CocoaPods;
using Microsoft.ComponentDetection.Detectors.Conan;
using Microsoft.ComponentDetection.Detectors.Dockerfile;
using Microsoft.ComponentDetection.Detectors.Go;
using Microsoft.ComponentDetection.Detectors.Gradle;
@ -78,6 +79,9 @@ public static class ServiceCollectionExtensions
// CocoaPods
services.AddSingleton<IComponentDetector, PodComponentDetector>();
// Conan
services.AddSingleton<IComponentDetector, ConanLockComponentDetector>();
// Conda
services.AddSingleton<IComponentDetector, CondaLockComponentDetector>();

Просмотреть файл

@ -122,6 +122,18 @@ public class TypedComponentSerializationTests
cargoComponent.Version.Should().Be("1.2.3");
}
[TestMethod]
public void TypedComponent_Serialization_Conan()
{
TypedComponent tc = new ConanComponent("SomeConanPackage", "1.2.3");
var result = JsonConvert.SerializeObject(tc);
var deserializedTC = JsonConvert.DeserializeObject<TypedComponent>(result);
deserializedTC.Should().BeOfType(typeof(ConanComponent));
var conanComponent = (ConanComponent)deserializedTC;
conanComponent.Name.Should().Be("SomeConanPackage");
conanComponent.Version.Should().Be("1.2.3");
}
[TestMethod]
public void TypedComponent_Serialization_Pip()
{

Просмотреть файл

@ -0,0 +1,259 @@
namespace Microsoft.ComponentDetection.Detectors.Tests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.ComponentDetection.Detectors.Conan;
using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
using Microsoft.ComponentDetection.TestsUtilities;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
[TestCategory("Governance/All")]
[TestCategory("Governance/ComponentDetection")]
public class ConanLockComponentDetectorTests : BaseDetectorTest<ConanLockComponentDetector>
{
private readonly string testConanLockString = @"{
""graph_lock"": {
""nodes"": {
""0"": {
""ref"": ""MyConanProject/None"",
""options"": ""SomeLongOptionsString"",
""requires"": [
""1"",
""2"",
""3"",
""4"",
""5"",
""6""
],
""path"": ""../conanfile.py"",
""context"": ""host""
},
""1"": {
""ref"": ""libabc/1.2.12#someHashOfLibAbc"",
""options"": ""someOptionsString"",
""package_id"": ""packageIdOfLibAbc"",
""prev"": ""someHashOfLibAbc"",
""context"": ""host""
},
""2"": {
""ref"": ""libawesomelibrary/3.2.1#someHashOfLibAwesomeLibrary"",
""options"": ""someOptionsString"",
""package_id"": ""packageIdOfLibAwesomeLibrary"",
""prev"": ""someHashOfLibAwesomeLibrary"",
""requires"": [
""1""
],
""context"": ""host""
},
""3"": {
""ref"": ""libanotherlibrary1/2.3.4#someHashOfLibAnotherLibrary1"",
""options"": ""someOptionsString"",
""package_id"": ""packageIdOfLibAnotherLibrary1"",
""prev"": ""someHashOfLibAnotherLibrary1"",
""requires"": [
""4"",
""6""
],
""context"": ""host""
},
""4"": {
""ref"": ""libanotherlibrary2/3.4.5#someHashOfLibAnotherLibrary2"",
""options"": ""someOptionsString"",
""package_id"": ""packageIdOfLibAnotherLibrary2"",
""prev"": ""someHashOfLibAnotherLibrary2"",
""requires"": [
""5""
],
""context"": ""host""
},
""5"": {
""ref"": ""libanotherlibrary3/4.5.6#someHashOfLibAnotherLibrary3"",
""options"": """",
""package_id"": ""packageIdOfLibAnotherLibrary3"",
""prev"": ""someHashOfLibAnotherLibrary3"",
""context"": ""host""
},
""6"": {
""ref"": ""libanotherlibrary4/5.6.7#someHashOfLibAnotherLibrary4"",
""options"": ""someOptionsString"",
""package_id"": ""packageIdOfLibAnotherLibrary4"",
""prev"": ""someHashOfLibAnotherLibrary4"",
""modified"": true,
""context"": ""host""
}
},
""revisions_enabled"": true
},
""version"": ""0.4"",
""profile_host"": ""someLongProfileHostSettingsString\n"",
""profile_build"": ""someLongProfileBuildSettingsString\n""
}
";
private readonly string testConanLockStringWithNullValueForRootNode = @"{
""graph_lock"": {
""nodes"": {
""0"": null,
""1"": {
""ref"": ""libabc/1.2.12#someHashOfLibAbc"",
""options"": ""someOptionsString"",
""package_id"": ""packageIdOfLibAbc"",
""prev"": ""someHashOfLibAbc"",
""context"": ""host""
}
},
""revisions_enabled"": true
},
""version"": ""0.4"",
""profile_host"": ""someLongProfileHostSettingsString\n"",
""profile_build"": ""someLongProfileBuildSettingsString\n""
}
";
private readonly string testConanLockNoDependenciesString = @"{
""graph_lock"": {
""nodes"": {
""0"": {
""ref"": ""MyConanProject/None"",
""options"": ""SomeLongOptionsString"",
""path"": ""../conanfile.py"",
""context"": ""host""
}
},
""revisions_enabled"": true
},
""version"": ""0.4"",
""profile_host"": ""someLongProfileHostSettingsString\n"",
""profile_build"": ""someLongProfileBuildSettingsString\n""
}
";
[TestMethod]
public async Task TestGraphIsCorrectAsync()
{
var (result, componentRecorder) = await this.DetectorTestUtility
.WithFile("Conan.lock", this.testConanLockString)
.ExecuteDetectorAsync();
result.ResultCode.Should().Be(ProcessingResultCode.Success);
componentRecorder.GetDetectedComponents().Count().Should().Be(6);
var graph = componentRecorder.GetDependencyGraphsByLocation().Values.First(); // There should only be 1
// Verify explicitly referenced roots
var rootComponents = new List<string>
{
"libabc 1.2.12#someHashOfLibAbc - Conan",
"libawesomelibrary 3.2.1#someHashOfLibAwesomeLibrary - Conan",
"libanotherlibrary1 2.3.4#someHashOfLibAnotherLibrary1 - Conan",
"libanotherlibrary2 3.4.5#someHashOfLibAnotherLibrary2 - Conan",
"libanotherlibrary3 4.5.6#someHashOfLibAnotherLibrary3 - Conan",
"libanotherlibrary4 5.6.7#someHashOfLibAnotherLibrary4 - Conan",
};
var enumerable = graph.GetComponents().ToList();
rootComponents.ForEach(rootComponentId =>
{
graph.IsComponentExplicitlyReferenced(rootComponentId).Should().BeTrue($"Expected Component to be explicitly referenced but it is not: {rootComponentId}");
});
// components without any dependencies
graph.GetDependenciesForComponent("libabc 1.2.12#someHashOfLibAbc - Conan").Should().BeEmpty();
graph.GetDependenciesForComponent("libanotherlibrary3 4.5.6#someHashOfLibAnotherLibrary3 - Conan").Should().BeEmpty();
graph.GetDependenciesForComponent("libanotherlibrary4 5.6.7#someHashOfLibAnotherLibrary4 - Conan").Should().BeEmpty();
// Verify dependencies for other dependencies
graph.GetDependenciesForComponent("libawesomelibrary 3.2.1#someHashOfLibAwesomeLibrary - Conan").Should().BeEquivalentTo(new[] { "libabc 1.2.12#someHashOfLibAbc - Conan" });
var a = graph.GetDependenciesForComponent("libanotherlibrary1 2.3.4#someHashOfLibAnotherLibrary1 - Conan");
graph.GetDependenciesForComponent("libanotherlibrary1 2.3.4#someHashOfLibAnotherLibrary1 - Conan").Should().BeEquivalentTo(new[] { "libanotherlibrary2 3.4.5#someHashOfLibAnotherLibrary2 - Conan", "libanotherlibrary4 5.6.7#someHashOfLibAnotherLibrary4 - Conan" });
graph.GetDependenciesForComponent("libanotherlibrary2 3.4.5#someHashOfLibAnotherLibrary2 - Conan").Should().BeEquivalentTo(new[] { "libanotherlibrary3 4.5.6#someHashOfLibAnotherLibrary3 - Conan" });
}
[TestMethod]
public async Task TestDetectionForConanLockFileWithNullValuesForRootNodeAsync()
{
var (result, componentRecorder) = await this.DetectorTestUtility
.WithFile("Conan.lock", this.testConanLockStringWithNullValueForRootNode)
.ExecuteDetectorAsync();
result.ResultCode.Should().Be(ProcessingResultCode.Success);
componentRecorder.GetDetectedComponents().Count().Should().Be(1);
(componentRecorder.GetDetectedComponents().First().Component as ConanComponent).Name.Should().Be("libabc");
(componentRecorder.GetDetectedComponents().First().Component as ConanComponent).Version.Should().Be("1.2.12#someHashOfLibAbc");
}
[TestMethod]
public async Task TestConanDetectorAsync()
{
var (result, componentRecorder) = await this.DetectorTestUtility
.WithFile("Conan.lock", this.testConanLockString)
.ExecuteDetectorAsync();
result.ResultCode.Should().Be(ProcessingResultCode.Success);
componentRecorder.GetDetectedComponents().Count().Should().Be(6);
IDictionary<string, string> packageVersions = new Dictionary<string, string>()
{
{ "libabc", "1.2.12#someHashOfLibAbc" },
{ "libawesomelibrary", "3.2.1#someHashOfLibAwesomeLibrary" },
{ "libanotherlibrary1", "2.3.4#someHashOfLibAnotherLibrary1" },
{ "libanotherlibrary2", "3.4.5#someHashOfLibAnotherLibrary2" },
{ "libanotherlibrary3", "4.5.6#someHashOfLibAnotherLibrary3" },
{ "libanotherlibrary4", "5.6.7#someHashOfLibAnotherLibrary4" },
};
IDictionary<string, ISet<string>> packageDependencyRoots = new Dictionary<string, ISet<string>>()
{
{ "libabc", new HashSet<string>() { "libabc", "libawesomelibrary" } },
{ "libawesomelibrary", new HashSet<string>() { "libawesomelibrary" } },
{ "libanotherlibrary1", new HashSet<string>() { "libanotherlibrary1" } },
{ "libanotherlibrary2", new HashSet<string>() { "libanotherlibrary2", "libanotherlibrary1" } },
{ "libanotherlibrary3", new HashSet<string>() { "libanotherlibrary3", "libanotherlibrary1", "libanotherlibrary2" } },
{ "libanotherlibrary4", new HashSet<string>() { "libanotherlibrary4", "libanotherlibrary1" } },
};
ISet<string> componentNames = new HashSet<string>();
foreach (var discoveredComponent in componentRecorder.GetDetectedComponents())
{
// Verify each package has the right information
var packageName = (discoveredComponent.Component as ConanComponent).Name;
// Verify version
(discoveredComponent.Component as ConanComponent).Version.Should().Be(packageVersions[packageName]);
var dependencyRoots = new HashSet<string>();
componentRecorder.AssertAllExplicitlyReferencedComponents(
discoveredComponent.Component.Id,
packageDependencyRoots[packageName].Select(expectedRoot =>
new Func<ConanComponent, bool>(parentComponent => parentComponent.Name == expectedRoot)).ToArray());
componentNames.Add(packageName);
}
// Verify all packages were detected
foreach (var expectedPackage in packageVersions.Keys)
{
componentNames.Should().Contain(expectedPackage);
}
}
[TestMethod]
public async Task TestConanDetector_SupportNoDependenciesAsync()
{
var (result, componentRecorder) = await this.DetectorTestUtility
.WithFile("conan.lock", this.testConanLockNoDependenciesString)
.ExecuteDetectorAsync();
result.ResultCode.Should().Be(ProcessingResultCode.Success);
componentRecorder.GetDetectedComponents().Should().BeEmpty();
}
}

Просмотреть файл

@ -38,14 +38,14 @@ function main()
Set-Location ((Get-Item $repoPath).FullName + "\src\Microsoft.ComponentDetection")
dotnet run scan --SourceDirectory $verificationTestRepo --Output $output `
--DockerImagesToScan $dockerImagesToScan `
--DetectorArgs DockerReference=EnableIfDefaultOff,SPDX22SBOM=EnableIfDefaultOff,CondaLock=EnableIfDefaultOff
--DetectorArgs DockerReference=EnableIfDefaultOff,SPDX22SBOM=EnableIfDefaultOff,CondaLock=EnableIfDefaultOff,ConanLock=EnableIfDefaultOff
Set-Location $CDRelease
dotnet restore
Set-Location ($CDRelease + "\src\Microsoft.ComponentDetection")
dotnet run scan --SourceDirectory $verificationTestRepo --Output $releaseOutput `
--DockerImagesToScan $dockerImagesToScan `
--DetectorArgs DockerReference=EnableIfDefaultOff,SPDX22SBOM=EnableIfDefaultOff,CondaLock=EnableIfDefaultOff
--DetectorArgs DockerReference=EnableIfDefaultOff,SPDX22SBOM=EnableIfDefaultOff,CondaLock=EnableIfDefaultOff,ConanLock=EnableIfDefaultOff
$env:GITHUB_OLD_ARTIFACTS_DIR = $releaseOutput
$env:GITHUB_NEW_ARTIFACTS_DIR = $output

Просмотреть файл

@ -0,0 +1,77 @@
{
"graph_lock": {
"nodes": {
"0": {
"ref": "MyAwesomeConanProject/1.2.5",
"options": "",
"requires": [
"1"
],
"build_requires": [
"6"
],
"path": "conanfile.py",
"context": "host"
},
"1": {
"ref": "boost/1.82.0",
"options": "",
"package_id": "dd7f5f958c7381cfd81e611a16062de0c827160a",
"prev": "0",
"modified": true,
"requires": [
"2",
"3",
"4"
],
"build_requires": [
"5"
],
"context": "host"
},
"2": {
"ref": "zlib/1.2.13",
"options": "",
"package_id": "240c2182163325b213ca6886a7614c8ed2bf1738",
"prev": "0",
"modified": true,
"context": "host"
},
"3": {
"ref": "bzip2/1.0.8",
"options": "",
"package_id": "238a93dc813ca1550968399f1f8925565feeff8e",
"prev": "0",
"modified": true,
"context": "host"
},
"4": {
"ref": "libbacktrace/cci.20210118",
"options": "",
"package_id": "240c2182163325b213ca6886a7614c8ed2bf1738",
"prev": "0",
"modified": true,
"context": "host"
},
"5": {
"ref": "b2/4.9.6",
"options": "",
"package_id": "a5ad5696abf650a25eea8f377806b3d5fe234e6e",
"prev": "0",
"modified": true,
"context": "host"
},
"6": {
"ref": "gtest/1.8.1",
"options": "",
"package_id": "fb16a498e820fb09d04ff9374a782b5b21da0601",
"prev": "0",
"modified": true,
"context": "host"
}
},
"revisions_enabled": false
},
"version": "0.4",
"profile_host": "\n"
}

Просмотреть файл

@ -0,0 +1,11 @@
from conans import ConanFile
class MyAwesome(ConanFile):
name = "MyAwesomeConanProject"
version = "1.2.5"
def requirements(self):
self.requires("boost/1.82.0")
def build_requirements(self):
self.tool_requires("gtest/1.8.1")

Просмотреть файл

@ -0,0 +1,60 @@
{
"graph_lock": {
"nodes": {
"0": {
"options": "",
"requires": [
"1"
],
"build_requires": [
"5"
],
"path": "conanfile.txt",
"context": "host"
},
"1": {
"ref": "boost/1.82.0",
"options": "",
"package_id": "dd7f5f958c7381cfd81e611a16062de0c827160a",
"prev": "0",
"requires": [
"2",
"3",
"4"
],
"context": "host"
},
"2": {
"ref": "zlib/1.2.13",
"options": "",
"package_id": "240c2182163325b213ca6886a7614c8ed2bf1738",
"prev": "0",
"context": "host"
},
"3": {
"ref": "bzip2/1.0.8",
"options": "",
"package_id": "238a93dc813ca1550968399f1f8925565feeff8e",
"prev": "0",
"context": "host"
},
"4": {
"ref": "libbacktrace/cci.20210118",
"options": "",
"package_id": "240c2182163325b213ca6886a7614c8ed2bf1738",
"prev": "0",
"context": "host"
},
"5": {
"ref": "gtest/1.8.1",
"options": "",
"package_id": "fb16a498e820fb09d04ff9374a782b5b21da0601",
"prev": "0",
"context": "host"
}
},
"revisions_enabled": false
},
"version": "0.4",
"profile_host": "\n"
}

Просмотреть файл

@ -0,0 +1,5 @@
[requires]
boost/1.82.0
[tool_requires]
gtest/1.8.1