From 48cee702eb25b6c75455ecac443bf504a8cd5c5d Mon Sep 17 00:00:00 2001 From: Martin Nonnenmacher Date: Mon, 4 Oct 2021 21:19:46 +0200 Subject: [PATCH 1/9] analyzer: Align on using "parse" instead of "extract" Prefer "parse" over "extract" to align the function naming in Bower, Cargo, and Conan with the other package manager implementations. Signed-off-by: Martin Nonnenmacher --- analyzer/src/main/kotlin/managers/Bower.kt | 47 +++++++-------- analyzer/src/main/kotlin/managers/Cargo.kt | 35 +++++------ analyzer/src/main/kotlin/managers/Conan.kt | 69 +++++++++++----------- 3 files changed, 77 insertions(+), 74 deletions(-) diff --git a/analyzer/src/main/kotlin/managers/Bower.kt b/analyzer/src/main/kotlin/managers/Bower.kt index f7a63b259e15e..210e029d572f4 100644 --- a/analyzer/src/main/kotlin/managers/Bower.kt +++ b/analyzer/src/main/kotlin/managers/Bower.kt @@ -1,5 +1,6 @@ /* * Copyright (C) 2017-2019 HERE Europe B.V. + * Copyright (C) 2021 Bosch.IO GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,7 +65,7 @@ class Bower( private const val SCOPE_NAME_DEPENDENCIES = "dependencies" private const val SCOPE_NAME_DEV_DEPENDENCIES = "devDependencies" - private fun extractPackageId(node: JsonNode) = + private fun parsePackageId(node: JsonNode) = Identifier( type = "Bower", namespace = "", @@ -72,25 +73,25 @@ class Bower( version = node["pkgMeta"]["version"].textValueOrEmpty() ) - private fun extractRepositoryType(node: JsonNode) = + private fun parseRepositoryType(node: JsonNode) = VcsType(node["pkgMeta"]["repository"]?.get("type").textValueOrEmpty()) - private fun extractRepositoryUrl(node: JsonNode) = + private fun parseRepositoryUrl(node: JsonNode) = node["pkgMeta"]["repository"]?.get("url")?.textValue() ?: node["pkgMeta"]["_source"].textValueOrEmpty() - private fun extractRevision(node: JsonNode): String = + private fun parseRevision(node: JsonNode): String = node["pkgMeta"]["_resolution"]?.get("commit")?.textValue() ?: node["pkgMeta"]["_resolution"]?.get("tag").textValueOrEmpty() - private fun extractVcsInfo(node: JsonNode) = + private fun parseVcsInfo(node: JsonNode) = VcsInfo( - type = extractRepositoryType(node), - url = extractRepositoryUrl(node), - revision = extractRevision(node) + type = parseRepositoryType(node), + url = parseRepositoryUrl(node), + revision = parseRevision(node) ) - private fun extractDeclaredLicenses(node: JsonNode): SortedSet = + private fun parseDeclaredLicenses(node: JsonNode): SortedSet = sortedSetOf().apply { val license = node["pkgMeta"]["license"].textValueOrEmpty() if (license.isNotEmpty()) add(license) @@ -112,22 +113,22 @@ class Bower( }?.let { addAll(it) } } - private fun extractPackage(node: JsonNode) = + private fun parsePackage(node: JsonNode) = Package( - id = extractPackageId(node), + id = parsePackageId(node), authors = parseAuthors(node), - declaredLicenses = extractDeclaredLicenses(node), + declaredLicenses = parseDeclaredLicenses(node), description = node["pkgMeta"]["description"].textValueOrEmpty(), homepageUrl = node["pkgMeta"]["homepage"].textValueOrEmpty(), binaryArtifact = RemoteArtifact.EMPTY, sourceArtifact = RemoteArtifact.EMPTY, // TODO: implement me! - vcs = extractVcsInfo(node) + vcs = parseVcsInfo(node) ) private fun getDependencyNodes(node: JsonNode): Sequence = node["dependencies"].fieldsOrEmpty().asSequence().map { it.value } - private fun extractPackages(node: JsonNode): Map { + private fun parsePackages(node: JsonNode): Map { val result = mutableMapOf() val stack = Stack() @@ -135,7 +136,7 @@ class Bower( while (!stack.empty()) { val currentNode = stack.pop() - val pkg = extractPackage(currentNode) + val pkg = parsePackage(currentNode) result["${pkg.id.name}:${pkg.id.version}"] = pkg stack += getDependencyNodes(currentNode) @@ -181,7 +182,7 @@ class Bower( return result } - private fun extractDependencyTree( + private fun parseDependencyTree( node: JsonNode, scopeName: String, alternativeNodes: Map = getNodesWithCompleteDependencies(node) @@ -195,15 +196,15 @@ class Bower( // information. // See https://github.com/bower/bower/blob/6bc778d/lib/core/Manager.js#L557 and below. val alternativeNode = checkNotNull(alternativeNodes[dependencyKeyOf(node)]) - return extractDependencyTree(alternativeNode, scopeName, alternativeNodes) + return parseDependencyTree(alternativeNode, scopeName, alternativeNodes) } node["pkgMeta"][scopeName].fieldNamesOrEmpty().forEach { val childNode = node["dependencies"][it] val childScope = SCOPE_NAME_DEPENDENCIES - val childDependencies = extractDependencyTree(childNode, childScope, alternativeNodes) + val childDependencies = parseDependencyTree(childNode, childScope, alternativeNodes) val packageReference = PackageReference( - id = extractPackageId(childNode), + id = parsePackageId(childNode), dependencies = childDependencies ) result += packageReference @@ -236,17 +237,17 @@ class Bower( installDependencies(workingDir) val dependenciesJson = listDependencies(workingDir) val rootNode = jsonMapper.readTree(dependenciesJson) - val packages = extractPackages(rootNode) + val packages = parsePackages(rootNode) val dependenciesScope = Scope( name = SCOPE_NAME_DEPENDENCIES, - dependencies = extractDependencyTree(rootNode, SCOPE_NAME_DEPENDENCIES) + dependencies = parseDependencyTree(rootNode, SCOPE_NAME_DEPENDENCIES) ) val devDependenciesScope = Scope( name = SCOPE_NAME_DEV_DEPENDENCIES, - dependencies = extractDependencyTree(rootNode, SCOPE_NAME_DEV_DEPENDENCIES) + dependencies = parseDependencyTree(rootNode, SCOPE_NAME_DEV_DEPENDENCIES) ) - val projectPackage = extractPackage(rootNode) + val projectPackage = parsePackage(rootNode) val project = Project( id = projectPackage.id, definitionFilePath = VersionControlSystem.getPathInfo(definitionFile).path, diff --git a/analyzer/src/main/kotlin/managers/Cargo.kt b/analyzer/src/main/kotlin/managers/Cargo.kt index edf3ce66b46e1..ff4514e87998b 100644 --- a/analyzer/src/main/kotlin/managers/Cargo.kt +++ b/analyzer/src/main/kotlin/managers/Cargo.kt @@ -1,5 +1,6 @@ /* * Copyright (C) 2019 HERE Europe B.V. + * Copyright (C) 2021 Bosch.IO GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -146,7 +147,7 @@ class Cargo( } }.toSortedSet() - val id = extractCargoId(node) + val id = parseCargoId(node) val pkg = packages.getValue(id) val linkage = if (isProjectDependency(id)) PackageLinkage.PROJECT_STATIC else PackageLinkage.STATIC @@ -166,8 +167,8 @@ class Cargo( val hashes = readHashes(resolveLockfile(metadata)) val packages = metadata["packages"].associateBy( - { extractCargoId(it) }, - { extractPackage(it, hashes) } + { parseCargoId(it) }, + { parsePackage(it, hashes) } ) val projectId = metadata["workspace_members"] @@ -231,13 +232,13 @@ class Cargo( private val PATH_DEPENDENCY_REGEX = Regex("""^.*\(path\+file://(.*)\)$""") private fun checksumKeyOf(metadata: JsonNode): String { - val id = extractCargoId(metadata) + val id = parseCargoId(metadata) return "\"checksum $id\"" } -private fun extractCargoId(node: JsonNode) = node["id"].textValueOrEmpty() +private fun parseCargoId(node: JsonNode) = node["id"].textValueOrEmpty() -private fun extractDeclaredLicenses(node: JsonNode): SortedSet { +private fun parseDeclaredLicenses(node: JsonNode): SortedSet { val declaredLicenses = node["license"].textValueOrEmpty().split('/') .map { it.trim() } .filterTo(sortedSetOf()) { it.isNotEmpty() } @@ -260,24 +261,24 @@ private fun processDeclaredLicenses(licenses: Set): ProcessedDeclaredLic // https://github.com/rust-lang/cargo/pull/4920 DeclaredLicenseProcessor.process(licenses, operator = SpdxOperator.OR) -private fun extractPackage(node: JsonNode, hashes: Map): Package { - val declaredLicenses = extractDeclaredLicenses(node) +private fun parsePackage(node: JsonNode, hashes: Map): Package { + val declaredLicenses = parseDeclaredLicenses(node) val declaredLicensesProcessed = processDeclaredLicenses(declaredLicenses) return Package( - id = extractPackageId(node), + id = parsePackageId(node), authors = parseAuthors(node["authors"]), declaredLicenses = declaredLicenses, declaredLicensesProcessed = declaredLicensesProcessed, description = node["description"].textValueOrEmpty(), binaryArtifact = RemoteArtifact.EMPTY, - sourceArtifact = extractSourceArtifact(node, hashes).orEmpty(), + sourceArtifact = parseSourceArtifact(node, hashes).orEmpty(), homepageUrl = "", - vcs = extractVcsInfo(node) + vcs = parseVcsInfo(node) ) } -private fun extractPackageId(node: JsonNode) = +private fun parsePackageId(node: JsonNode) = Identifier( type = "Crate", // Note that Rust / Cargo do not support package namespaces, see: @@ -287,9 +288,9 @@ private fun extractPackageId(node: JsonNode) = version = node["version"].textValueOrEmpty() ) -private fun extractRepositoryUrl(node: JsonNode) = node["repository"].textValueOrEmpty() +private fun parseRepositoryUrl(node: JsonNode) = node["repository"].textValueOrEmpty() -private fun extractSourceArtifact( +private fun parseSourceArtifact( node: JsonNode, hashes: Map ): RemoteArtifact? { @@ -305,8 +306,8 @@ private fun extractSourceArtifact( return RemoteArtifact(url, hash) } -private fun extractVcsInfo(node: JsonNode) = - VcsHost.toVcsInfo(extractRepositoryUrl(node)) +private fun parseVcsInfo(node: JsonNode) = + VcsHost.toVcsInfo(parseRepositoryUrl(node)) private fun getResolvedVersion( parentName: String, @@ -333,7 +334,7 @@ private fun getResolvedVersion( } /** - * Extract information about authors from the given [node] with package metadata. + * Parse information about authors from the given [node] with package metadata. */ private fun parseAuthors(node: JsonNode?): SortedSet = node?.mapNotNullTo(sortedSetOf()) { parseAuthorString(it.textValue()) } ?: sortedSetOf() diff --git a/analyzer/src/main/kotlin/managers/Conan.kt b/analyzer/src/main/kotlin/managers/Conan.kt index a176e76176b4a..cbaca5605e2ed 100644 --- a/analyzer/src/main/kotlin/managers/Conan.kt +++ b/analyzer/src/main/kotlin/managers/Conan.kt @@ -2,6 +2,7 @@ * Copyright (C) 2019 HERE Europe B.V. * Copyright (C) 2019 Verifa Oy. * Copyright (C) 2019 Bosch Software Innovations GmbH + * Copyright (C) 2021 Bosch.IO GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -165,18 +166,18 @@ class Conan( val dependenciesJson = run(workingDir, "info", ".", "-j").stdout val rootNode = jsonMapper.readTree(dependenciesJson) val packageList = removeProjectPackage(rootNode, definitionFile) - val packages = extractPackages(packageList, workingDir) + val packages = parsePackages(packageList, workingDir) val dependenciesScope = Scope( name = SCOPE_NAME_DEPENDENCIES, - dependencies = extractDependencies(rootNode, SCOPE_NAME_DEPENDENCIES, workingDir) + dependencies = parseDependencies(rootNode, SCOPE_NAME_DEPENDENCIES, workingDir) ) val devDependenciesScope = Scope( name = SCOPE_NAME_DEV_DEPENDENCIES, - dependencies = extractDependencies(rootNode, SCOPE_NAME_DEV_DEPENDENCIES, workingDir) + dependencies = parseDependencies(rootNode, SCOPE_NAME_DEV_DEPENDENCIES, workingDir) ) - val projectPackage = extractProjectPackage(rootNode, definitionFile, workingDir) + val projectPackage = parseProjectPackage(rootNode, definitionFile, workingDir) return listOf( ProjectAnalyzerResult( @@ -203,7 +204,7 @@ class Conan( /** * Return the dependency tree starting from a [rootNode] for given [scopeName]. */ - private fun extractDependencyTree( + private fun parseDependencyTree( rootNode: JsonNode, workingDir: File, pkg: JsonNode, @@ -218,14 +219,14 @@ class Conan( log.debug { "Found child '$childRef'." } val packageReference = PackageReference( - id = extractPackageId(child, workingDir), - dependencies = extractDependencyTree(rootNode, workingDir, child, SCOPE_NAME_DEPENDENCIES) + id = parsePackageId(child, workingDir), + dependencies = parseDependencyTree(rootNode, workingDir, child, SCOPE_NAME_DEPENDENCIES) ) result += packageReference val packageDevReference = PackageReference( - id = extractPackageId(child, workingDir), - dependencies = extractDependencyTree(rootNode, workingDir, child, SCOPE_NAME_DEV_DEPENDENCIES) + id = parsePackageId(child, workingDir), + dependencies = parseDependencyTree(rootNode, workingDir, child, SCOPE_NAME_DEV_DEPENDENCIES) ) result += packageDevReference @@ -236,9 +237,9 @@ class Conan( } /** - * Run through each package and extract list of its dependencies (also transitive ones). + * Run through each package and parse the list of its dependencies (also transitive ones). */ - private fun extractDependencies( + private fun parseDependencies( rootNode: JsonNode, scopeName: String, workingDir: File @@ -248,7 +249,7 @@ class Conan( while (!stack.empty()) { val pkg = stack.pop() - extractDependencyTree(rootNode, workingDir, pkg, scopeName).forEach { + parseDependencyTree(rootNode, workingDir, pkg, scopeName).forEach { dependencies += it } } @@ -258,13 +259,13 @@ class Conan( /** * Return the map of packages and their identifiers which are contained in [node]. */ - private fun extractPackages(node: List, workingDir: File): Map { + private fun parsePackages(node: List, workingDir: File): Map { val result = mutableMapOf() val stack = Stack().apply { addAll(node) } while (!stack.empty()) { val currentNode = stack.pop() - val pkg = extractPackage(currentNode, workingDir) + val pkg = parsePackage(currentNode, workingDir) result["${pkg.id.name}:${pkg.id.version}"] = pkg } @@ -272,18 +273,18 @@ class Conan( } /** - * Return the [Package] extracted from given [node]. + * Return the [Package] parsed from the given [node]. */ - private fun extractPackage(node: JsonNode, workingDir: File) = + private fun parsePackage(node: JsonNode, workingDir: File) = Package( - id = extractPackageId(node, workingDir), + id = parsePackageId(node, workingDir), authors = parseAuthors(node), - declaredLicenses = extractDeclaredLicenses(node), - description = extractPackageField(node, workingDir, "description"), + declaredLicenses = parseDeclaredLicenses(node), + description = parsePackageField(node, workingDir, "description"), homepageUrl = node["homepage"].textValueOrEmpty(), binaryArtifact = RemoteArtifact.EMPTY, // TODO: implement me! sourceArtifact = RemoteArtifact.EMPTY, // TODO: implement me! - vcs = extractVcsInfo(node) + vcs = parseVcsInfo(node) ) /** @@ -306,7 +307,7 @@ class Conan( /** * Return the set of declared licenses contained in [node]. */ - private fun extractDeclaredLicenses(node: JsonNode): SortedSet = + private fun parseDeclaredLicenses(node: JsonNode): SortedSet = sortedSetOf().also { licenses -> node["license"]?.mapNotNullTo(licenses) { it.textValue() } } @@ -314,18 +315,18 @@ class Conan( /** * Return the [Identifier] for the package contained in [node]. */ - private fun extractPackageId(node: JsonNode, workingDir: File) = + private fun parsePackageId(node: JsonNode, workingDir: File) = Identifier( type = "Conan", namespace = "", - name = extractPackageField(node, workingDir, "name"), - version = extractPackageField(node, workingDir, "version") + name = parsePackageField(node, workingDir, "name"), + version = parsePackageField(node, workingDir, "version") ) /** * Return the [VcsInfo] contained in [node]. */ - private fun extractVcsInfo(node: JsonNode) = + private fun parseVcsInfo(node: JsonNode) = VcsInfo( type = VcsType.GIT, url = node["url"].textValueOrEmpty(), @@ -335,18 +336,18 @@ class Conan( /** * Return the value of [field] from the output of `conan inspect --raw` for the package in [node]. */ - private fun extractPackageField(node: JsonNode, workingDir: File, field: String): String = + private fun parsePackageField(node: JsonNode, workingDir: File, field: String): String = runInspectRawField(node["display_name"].textValue(), workingDir, field) /** * Return a [Package] containing project-level information depending on which [definitionFile] was found: * - conanfile.txt: `conan inspect conanfile.txt` is not supported. - * - conanfile.py: `conan inspect conanfile.py` is supported and more useful project metadata is extracted. + * - conanfile.py: `conan inspect conanfile.py` is supported and more useful project metadata is parsed. * * TODO: The format of `conan info` output for a conanfile.txt file may be such that we can get project metadata * from the `requires` field. Need to investigate whether this is a sure thing before implementing. */ - private fun extractProjectPackage(rootNode: JsonNode, definitionFile: File, workingDir: File): Package { + private fun parseProjectPackage(rootNode: JsonNode, definitionFile: File, workingDir: File): Package { val projectPackageJson = requireNotNull(rootNode.find { it["reference"].textValue().contains(definitionFile.name) }) @@ -359,7 +360,7 @@ class Conan( } /** - * Return a [Package] containing project-level information extracted from [node] and [definitionFile] using the + * Return a [Package] containing project-level information parsed from [node] and [definitionFile] using the * `conan inspect` command. */ private fun generateProjectPackageFromConanfilePy(node: JsonNode, definitionFile: File, workingDir: File): Package = @@ -371,16 +372,16 @@ class Conan( version = runInspectRawField(definitionFile.name, workingDir, "version") ), authors = parseAuthors(node), - declaredLicenses = extractDeclaredLicenses(node), + declaredLicenses = parseDeclaredLicenses(node), description = runInspectRawField(definitionFile.name, workingDir, "description"), homepageUrl = node["homepage"].textValueOrEmpty(), binaryArtifact = RemoteArtifact.EMPTY, // TODO: implement me! sourceArtifact = RemoteArtifact.EMPTY, // TODO: implement me! - vcs = extractVcsInfo(node) + vcs = parseVcsInfo(node) ) /** - * Return a [Package] containing project-level information extracted from [node]. + * Return a [Package] containing project-level information parsed from [node]. */ private fun generateProjectPackageFromConanfileTxt(node: JsonNode): Package = Package( @@ -391,12 +392,12 @@ class Conan( version = "" ), authors = parseAuthors(node), - declaredLicenses = extractDeclaredLicenses(node), + declaredLicenses = parseDeclaredLicenses(node), description = "", homepageUrl = node["homepage"].textValueOrEmpty(), binaryArtifact = RemoteArtifact.EMPTY, // TODO: implement me! sourceArtifact = RemoteArtifact.EMPTY, // TODO: implement me! - vcs = extractVcsInfo(node) + vcs = parseVcsInfo(node) ) /** From e3ad35e4eec93eb2413df9151382cd40d89d60f4 Mon Sep 17 00:00:00 2001 From: Martin Nonnenmacher Date: Mon, 4 Oct 2021 21:56:33 +0200 Subject: [PATCH 2/9] Conan: Trivially fix a parameter name Signed-off-by: Martin Nonnenmacher --- analyzer/src/main/kotlin/managers/Conan.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/analyzer/src/main/kotlin/managers/Conan.kt b/analyzer/src/main/kotlin/managers/Conan.kt index cbaca5605e2ed..3079cad744969 100644 --- a/analyzer/src/main/kotlin/managers/Conan.kt +++ b/analyzer/src/main/kotlin/managers/Conan.kt @@ -257,11 +257,11 @@ class Conan( } /** - * Return the map of packages and their identifiers which are contained in [node]. + * Return the map of packages and their identifiers which are contained in [nodes]. */ - private fun parsePackages(node: List, workingDir: File): Map { + private fun parsePackages(nodes: List, workingDir: File): Map { val result = mutableMapOf() - val stack = Stack().apply { addAll(node) } + val stack = Stack().apply { addAll(nodes) } while (!stack.empty()) { val currentNode = stack.pop() From 92d491d6d9de9a1e2b258382ce9ccca00f7ba5b2 Mon Sep 17 00:00:00 2001 From: Martin Nonnenmacher Date: Mon, 4 Oct 2021 22:22:29 +0200 Subject: [PATCH 3/9] Conan: Simplify iterations Using the `Stack` class was completely unnecessary and only made the code more complex. Signed-off-by: Martin Nonnenmacher --- analyzer/src/main/kotlin/managers/Conan.kt | 30 +++++----------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/analyzer/src/main/kotlin/managers/Conan.kt b/analyzer/src/main/kotlin/managers/Conan.kt index 3079cad744969..e607ef550c217 100644 --- a/analyzer/src/main/kotlin/managers/Conan.kt +++ b/analyzer/src/main/kotlin/managers/Conan.kt @@ -29,7 +29,6 @@ import com.vdurmont.semver4j.Requirement import java.io.File import java.net.Authenticator import java.util.SortedSet -import java.util.Stack import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory import org.ossreviewtoolkit.analyzer.PackageManager @@ -243,35 +242,18 @@ class Conan( rootNode: JsonNode, scopeName: String, workingDir: File - ): SortedSet { - val stack = Stack().apply { addAll(rootNode) } - val dependencies = mutableSetOf() - - while (!stack.empty()) { - val pkg = stack.pop() - parseDependencyTree(rootNode, workingDir, pkg, scopeName).forEach { - dependencies += it - } - } - return dependencies.toSortedSet() - } + ): SortedSet = + rootNode.flatMapTo(sortedSetOf()) { pkg -> parseDependencyTree(rootNode, workingDir, pkg, scopeName) } /** * Return the map of packages and their identifiers which are contained in [nodes]. */ - private fun parsePackages(nodes: List, workingDir: File): Map { - val result = mutableMapOf() - val stack = Stack().apply { addAll(nodes) } - - while (!stack.empty()) { - val currentNode = stack.pop() - val pkg = parsePackage(currentNode, workingDir) - result["${pkg.id.name}:${pkg.id.version}"] = pkg + private fun parsePackages(nodes: List, workingDir: File): Map = + nodes.associate { node -> + val pkg = parsePackage(node, workingDir) + "${pkg.id.name}:${pkg.id.version}" to pkg } - return result - } - /** * Return the [Package] parsed from the given [node]. */ From b2130504efe127496a326ad237d0cc0b924d364b Mon Sep 17 00:00:00 2001 From: Martin Nonnenmacher Date: Mon, 4 Oct 2021 22:25:34 +0200 Subject: [PATCH 4/9] Conan: Do not call the iterator function `forEach` can be used directly as Kotlin automatically creates an iterator for iterable objects. Signed-off-by: Martin Nonnenmacher --- analyzer/src/main/kotlin/managers/Conan.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analyzer/src/main/kotlin/managers/Conan.kt b/analyzer/src/main/kotlin/managers/Conan.kt index e607ef550c217..95e435fc298ca 100644 --- a/analyzer/src/main/kotlin/managers/Conan.kt +++ b/analyzer/src/main/kotlin/managers/Conan.kt @@ -213,7 +213,7 @@ class Conan( pkg[scopeName]?.forEach { val childRef = it.textValueOrEmpty() - rootNode.iterator().forEach { child -> + rootNode.forEach { child -> if (child["reference"].textValueOrEmpty() == childRef) { log.debug { "Found child '$childRef'." } From 44d557b6393653834921e2f440af58252b9286fc Mon Sep 17 00:00:00 2001 From: Martin Nonnenmacher Date: Mon, 4 Oct 2021 22:58:37 +0200 Subject: [PATCH 5/9] Conan: Cache the results of running inspect Running "conan inspect" takes about 1s each time. Calling it separately for each field is therefore a huge waste of time, especially since the same fields are requested multiple times for the same packages while building the dependency tree. To fix this, run "conan inspect" only once for each package and cache the results for all fields. This requires to create a temporary JSON file, because "conan inspect" does not support to output the JSON to stdout. This reduces the execution time of the `ConanFunTest` from about 100s to about 30s and should have an even larger impact on projects with more dependencies, especially if there are repetitions within the dependency tree. Signed-off-by: Martin Nonnenmacher --- analyzer/src/main/kotlin/managers/Conan.kt | 31 +++++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/analyzer/src/main/kotlin/managers/Conan.kt b/analyzer/src/main/kotlin/managers/Conan.kt index 95e435fc298ca..d54245108a408 100644 --- a/analyzer/src/main/kotlin/managers/Conan.kt +++ b/analyzer/src/main/kotlin/managers/Conan.kt @@ -49,7 +49,9 @@ import org.ossreviewtoolkit.model.jsonMapper import org.ossreviewtoolkit.utils.CommandLineTool import org.ossreviewtoolkit.utils.Os import org.ossreviewtoolkit.utils.ProcessCapture +import org.ossreviewtoolkit.utils.createOrtTempFile import org.ossreviewtoolkit.utils.log +import org.ossreviewtoolkit.utils.safeDeleteRecursively import org.ossreviewtoolkit.utils.stashDirectories import org.ossreviewtoolkit.utils.textValueOrEmpty import org.ossreviewtoolkit.utils.toUri @@ -81,6 +83,8 @@ class Conan( ) = Conan(managerName, analysisRoot, analyzerConfig, repoConfig) } + private val pkgInspectResults = mutableMapOf() + override fun command(workingDir: File?) = "conan" // TODO: Add support for Conan lock files. @@ -98,7 +102,16 @@ class Conan( /** * Primary method for resolving dependencies from [definitionFile]. */ - override fun resolveDependencies(definitionFile: File): List { + override fun resolveDependencies(definitionFile: File): List = + try { + resolvedDependenciesInternal(definitionFile) + } finally { + // Clear the inspection result cache, because we call "conan config install" for each definition file which + // could overwrite the remotes and result in different metadata for packages with the same name and version. + pkgInspectResults.clear() + } + + private fun resolvedDependenciesInternal(definitionFile: File): List { val conanHome = Os.userHomeDirectory.resolve(".conan") // This is where Conan caches downloaded packages [1]. Note that the package cache is not concurrent, and its @@ -272,8 +285,12 @@ class Conan( /** * Return the value `conan inspect` reports for the given [field]. */ - private fun runInspectRawField(pkgName: String, workingDir: File, field: String): String = - run(workingDir, "inspect", pkgName, "--raw", field).stdout + private fun inspectField(pkgName: String, workingDir: File, field: String): String = + pkgInspectResults.getOrPut(pkgName) { + val jsonFile = createOrtTempFile(managerName) + run(workingDir, "inspect", pkgName, "--json", jsonFile.absolutePath).requireSuccess() + jsonMapper.readTree(jsonFile).also { jsonFile.parentFile.safeDeleteRecursively(force = true) } + }.get(field).textValueOrEmpty() /** * Return the full list of packages, excluding the project level information. @@ -319,7 +336,7 @@ class Conan( * Return the value of [field] from the output of `conan inspect --raw` for the package in [node]. */ private fun parsePackageField(node: JsonNode, workingDir: File, field: String): String = - runInspectRawField(node["display_name"].textValue(), workingDir, field) + inspectField(node["display_name"].textValue(), workingDir, field) /** * Return a [Package] containing project-level information depending on which [definitionFile] was found: @@ -350,12 +367,12 @@ class Conan( id = Identifier( type = managerName, namespace = "", - name = runInspectRawField(definitionFile.name, workingDir, "name"), - version = runInspectRawField(definitionFile.name, workingDir, "version") + name = inspectField(definitionFile.name, workingDir, "name"), + version = inspectField(definitionFile.name, workingDir, "version") ), authors = parseAuthors(node), declaredLicenses = parseDeclaredLicenses(node), - description = runInspectRawField(definitionFile.name, workingDir, "description"), + description = inspectField(definitionFile.name, workingDir, "description"), homepageUrl = node["homepage"].textValueOrEmpty(), binaryArtifact = RemoteArtifact.EMPTY, // TODO: implement me! sourceArtifact = RemoteArtifact.EMPTY, // TODO: implement me! From 1478c64f18b671e360c9db0d99a65dd1bfa38044 Mon Sep 17 00:00:00 2001 From: Martin Nonnenmacher Date: Mon, 4 Oct 2021 23:59:39 +0200 Subject: [PATCH 6/9] ConanFunTest: Use a dependency with more transitive dependencies Use a dependency with one more level of transitive dependencies to better show the issues with building the dependency tree which will be fixed in the next commit. Signed-off-by: Martin Nonnenmacher --- .../synthetic/conan-expected-output-txt.yml | 30 +++++++++++++++++++ .../synthetic/conan-txt/conanfile.txt | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml b/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml index 70414ee40ab17..2c40b350d7070 100644 --- a/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml +++ b/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml @@ -22,6 +22,7 @@ project: - id: "Conan::libxml2:2.9.12" dependencies: - id: "Conan::zlib:1.2.11" + - id: "Conan::libxslt:1.1.34" - name: "requires" dependencies: - id: "Conan::libcurl:7.79.1" @@ -117,6 +118,35 @@ packages: url: "https://github.com/conan-io/conan-center-index.git" revision: "" path: "" +- id: "Conan::libxslt:1.1.34" + purl: "pkg:conan/libxslt@1.1.34" + declared_licenses: + - "MIT" + declared_licenses_processed: + spdx_expression: "MIT" + description: "libxslt is a software library implementing XSLT processor, based on\ + \ libxml2" + homepage_url: "https://xmlsoft.org" + binary_artifact: + url: "" + hash: + value: "" + algorithm: "" + source_artifact: + url: "" + hash: + value: "" + algorithm: "" + vcs: + type: "Git" + url: "https://github.com/conan-io/conan-center-index" + revision: "" + path: "" + vcs_processed: + type: "Git" + url: "https://github.com/conan-io/conan-center-index.git" + revision: "" + path: "" - id: "Conan::openssl:1.1.1l" purl: "pkg:conan/openssl@1.1.1l" declared_licenses: diff --git a/analyzer/src/funTest/assets/projects/synthetic/conan-txt/conanfile.txt b/analyzer/src/funTest/assets/projects/synthetic/conan-txt/conanfile.txt index c34733ea06785..8339d39bb8e24 100644 --- a/analyzer/src/funTest/assets/projects/synthetic/conan-txt/conanfile.txt +++ b/analyzer/src/funTest/assets/projects/synthetic/conan-txt/conanfile.txt @@ -1,5 +1,5 @@ [build_requires] -libxml2/2.9.12 +libxslt/1.1.34 [requires] libcurl/7.79.1 From aa1717a4755591aafea57e8318d6cc7e5857a441 Mon Sep 17 00:00:00 2001 From: Martin Nonnenmacher Date: Mon, 4 Oct 2021 23:49:24 +0200 Subject: [PATCH 7/9] Conan: Fix building the dependency tree Fix two issues with building the dependency tree: * The algorithm wrongly used all nodes in the output of "conan info" as root nodes of the dependency, but only the first node represents the root. * The dependencies for different scopes were assigned to separate `PackageReference` objects with the same id and afterwards added to a set. As a result only one of them was contained in the set. The result was that the root level of the dependency tree contained too many dependencies, while transitive dependencies were missing. While at it, rename some variables for better readability. Signed-off-by: Martin Nonnenmacher --- .../synthetic/conan-expected-output-py.yml | 1 - .../synthetic/conan-expected-output-txt.yml | 11 ++- analyzer/src/main/kotlin/managers/Conan.kt | 75 +++++++++---------- 3 files changed, 40 insertions(+), 47 deletions(-) diff --git a/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-py.yml b/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-py.yml index 13b0b38c55e6a..7f89d41172fdb 100644 --- a/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-py.yml +++ b/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-py.yml @@ -28,7 +28,6 @@ project: - id: "Conan::openssl:3.0.0" dependencies: - id: "Conan::zlib:1.2.11" - - id: "Conan::zlib:1.2.11" packages: - id: "Conan::openssl:3.0.0" purl: "pkg:conan/openssl@3.0.0" diff --git a/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml b/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml index 2c40b350d7070..8e57464dc902d 100644 --- a/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml +++ b/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml @@ -18,19 +18,18 @@ project: scopes: - name: "build_requires" dependencies: - - id: "Conan::libiconv:1.16" - - id: "Conan::libxml2:2.9.12" - dependencies: - - id: "Conan::zlib:1.2.11" - id: "Conan::libxslt:1.1.34" + dependencies: + - id: "Conan::libxml2:2.9.12" + dependencies: + - id: "Conan::libiconv:1.16" + - id: "Conan::zlib:1.2.11" - name: "requires" dependencies: - id: "Conan::libcurl:7.79.1" dependencies: - id: "Conan::openssl:1.1.1l" - id: "Conan::zlib:1.2.11" - - id: "Conan::openssl:1.1.1l" - - id: "Conan::zlib:1.2.11" packages: - id: "Conan::libcurl:7.79.1" purl: "pkg:conan/libcurl@7.79.1" diff --git a/analyzer/src/main/kotlin/managers/Conan.kt b/analyzer/src/main/kotlin/managers/Conan.kt index d54245108a408..869e1df2e1b5b 100644 --- a/analyzer/src/main/kotlin/managers/Conan.kt +++ b/analyzer/src/main/kotlin/managers/Conan.kt @@ -176,20 +176,20 @@ class Conan( installDependencies(workingDir) val dependenciesJson = run(workingDir, "info", ".", "-j").stdout - val rootNode = jsonMapper.readTree(dependenciesJson) - val packageList = removeProjectPackage(rootNode, definitionFile) + val pkgInfos = jsonMapper.readTree(dependenciesJson) + val packageList = removeProjectPackage(pkgInfos, definitionFile.name) val packages = parsePackages(packageList, workingDir) val dependenciesScope = Scope( name = SCOPE_NAME_DEPENDENCIES, - dependencies = parseDependencies(rootNode, SCOPE_NAME_DEPENDENCIES, workingDir) + dependencies = parseDependencies(pkgInfos, definitionFile.name, SCOPE_NAME_DEPENDENCIES, workingDir) ) val devDependenciesScope = Scope( name = SCOPE_NAME_DEV_DEPENDENCIES, - dependencies = parseDependencies(rootNode, SCOPE_NAME_DEV_DEPENDENCIES, workingDir) + dependencies = parseDependencies(pkgInfos, definitionFile.name, SCOPE_NAME_DEV_DEPENDENCIES, workingDir) ) - val projectPackage = parseProjectPackage(rootNode, definitionFile, workingDir) + val projectPackage = parseProjectPackage(pkgInfos, definitionFile, workingDir) return listOf( ProjectAnalyzerResult( @@ -214,37 +214,29 @@ class Conan( } /** - * Return the dependency tree starting from a [rootNode] for given [scopeName]. + * Return the dependency tree for [pkg] for the given [scopeName]. */ private fun parseDependencyTree( - rootNode: JsonNode, - workingDir: File, + pkgInfos: JsonNode, pkg: JsonNode, - scopeName: String + scopeName: String, + workingDir: File ): SortedSet { val result = mutableSetOf() - pkg[scopeName]?.forEach { - val childRef = it.textValueOrEmpty() - rootNode.forEach { child -> - if (child["reference"].textValueOrEmpty() == childRef) { - log.debug { "Found child '$childRef'." } - - val packageReference = PackageReference( - id = parsePackageId(child, workingDir), - dependencies = parseDependencyTree(rootNode, workingDir, child, SCOPE_NAME_DEPENDENCIES) - ) - result += packageReference + pkg[scopeName]?.forEach { childNode -> + val childRef = childNode.textValueOrEmpty() + pkgInfos.find { it["reference"].textValueOrEmpty() == childRef }?.let { pkgInfo -> + log.debug { "Found child '$childRef'." } - val packageDevReference = PackageReference( - id = parsePackageId(child, workingDir), - dependencies = parseDependencyTree(rootNode, workingDir, child, SCOPE_NAME_DEV_DEPENDENCIES) - ) + val id = parsePackageId(pkgInfo, workingDir) + val dependencies = parseDependencyTree(pkgInfos, pkgInfo, SCOPE_NAME_DEPENDENCIES, workingDir) + + parseDependencyTree(pkgInfos, pkgInfo, SCOPE_NAME_DEV_DEPENDENCIES, workingDir) - result += packageDevReference - } + result += PackageReference(id, dependencies = dependencies.toSortedSet()) } } + return result.toSortedSet() } @@ -252,11 +244,12 @@ class Conan( * Run through each package and parse the list of its dependencies (also transitive ones). */ private fun parseDependencies( - rootNode: JsonNode, + pkgInfos: JsonNode, + definitionFileName: String, scopeName: String, workingDir: File - ): SortedSet = - rootNode.flatMapTo(sortedSetOf()) { pkg -> parseDependencyTree(rootNode, workingDir, pkg, scopeName) } + ): SortedSet = + parseDependencyTree(pkgInfos, findProjectNode(pkgInfos, definitionFileName), scopeName, workingDir) /** * Return the map of packages and their identifiers which are contained in [nodes]. @@ -292,16 +285,20 @@ class Conan( jsonMapper.readTree(jsonFile).also { jsonFile.parentFile.safeDeleteRecursively(force = true) } }.get(field).textValueOrEmpty() + /** + * Find the node that represents the project defined in the definition file. + */ + private fun findProjectNode(pkgInfos: JsonNode, definitionFileName: String): JsonNode = + pkgInfos.first { + // Use "in" because conanfile.py's reference string often includes other data. + definitionFileName in it["reference"].textValueOrEmpty() + } + /** * Return the full list of packages, excluding the project level information. */ - private fun removeProjectPackage(rootNode: JsonNode, definitionFile: File): List = - rootNode.find { - // Contains because conanfile.py's reference string often includes other data. - it["reference"].textValueOrEmpty().contains(definitionFile.name) - }?.let { projectPackage -> - rootNode.minusElement(projectPackage) - } ?: rootNode.toList() + private fun removeProjectPackage(pkgInfos: JsonNode, definitionFileName: String): List = + pkgInfos.minusElement(findProjectNode(pkgInfos, definitionFileName)) /** * Return the set of declared licenses contained in [node]. @@ -346,10 +343,8 @@ class Conan( * TODO: The format of `conan info` output for a conanfile.txt file may be such that we can get project metadata * from the `requires` field. Need to investigate whether this is a sure thing before implementing. */ - private fun parseProjectPackage(rootNode: JsonNode, definitionFile: File, workingDir: File): Package { - val projectPackageJson = requireNotNull(rootNode.find { - it["reference"].textValue().contains(definitionFile.name) - }) + private fun parseProjectPackage(pkgInfos: JsonNode, definitionFile: File, workingDir: File): Package { + val projectPackageJson = findProjectNode(pkgInfos, definitionFile.name) return if (definitionFile.name == "conanfile.py") { generateProjectPackageFromConanfilePy(projectPackageJson, definitionFile, workingDir) From 02358a2d03d1db5bf78ba637b4c13da8d9f22d11 Mon Sep 17 00:00:00 2001 From: Martin Nonnenmacher Date: Tue, 5 Oct 2021 00:48:41 +0200 Subject: [PATCH 8/9] Conan: Try to get the VCS info from the homepage Do not parse the VCS info from the output of "conan info", because this only ever points to the recipe of the package, but not to the source code. Usually this points to the ConanCenter index [1]. Instead, try to parse the VCS info from the homepage URL of the package. This partly addresses #2037. [1] https://github.com/conan-io/conan-center-index Signed-off-by: Martin Nonnenmacher --- .../synthetic/conan-expected-output-py.yml | 12 ++--- .../synthetic/conan-expected-output-txt.yml | 44 +++++++++---------- analyzer/src/main/kotlin/managers/Conan.kt | 11 +++-- 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-py.yml b/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-py.yml index 7f89d41172fdb..9b64dca50d140 100644 --- a/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-py.yml +++ b/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-py.yml @@ -50,12 +50,12 @@ packages: algorithm: "" vcs: type: "Git" - url: "https://github.com/conan-io/conan-center-index" + url: "https://github.com/openssl/openssl.git" revision: "" path: "" vcs_processed: type: "Git" - url: "https://github.com/conan-io/conan-center-index.git" + url: "https://github.com/openssl/openssl.git" revision: "" path: "" - id: "Conan::zlib:1.2.11" @@ -78,12 +78,12 @@ packages: value: "" algorithm: "" vcs: - type: "Git" - url: "https://github.com/conan-io/conan-center-index" + type: "" + url: "" revision: "" path: "" vcs_processed: - type: "Git" - url: "https://github.com/conan-io/conan-center-index.git" + type: "" + url: "" revision: "" path: "" diff --git a/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml b/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml index 8e57464dc902d..81fe940fd80bb 100644 --- a/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml +++ b/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml @@ -50,13 +50,13 @@ packages: value: "" algorithm: "" vcs: - type: "Git" - url: "https://github.com/conan-io/conan-center-index" + type: "" + url: "" revision: "" path: "" vcs_processed: - type: "Git" - url: "https://github.com/conan-io/conan-center-index.git" + type: "" + url: "" revision: "" path: "" - id: "Conan::libiconv:1.16" @@ -80,13 +80,13 @@ packages: value: "" algorithm: "" vcs: - type: "Git" - url: "https://github.com/conan-io/conan-center-index" + type: "" + url: "" revision: "" path: "" vcs_processed: - type: "Git" - url: "https://github.com/conan-io/conan-center-index.git" + type: "" + url: "" revision: "" path: "" - id: "Conan::libxml2:2.9.12" @@ -108,13 +108,13 @@ packages: value: "" algorithm: "" vcs: - type: "Git" - url: "https://github.com/conan-io/conan-center-index" + type: "" + url: "" revision: "" path: "" vcs_processed: - type: "Git" - url: "https://github.com/conan-io/conan-center-index.git" + type: "" + url: "" revision: "" path: "" - id: "Conan::libxslt:1.1.34" @@ -137,13 +137,13 @@ packages: value: "" algorithm: "" vcs: - type: "Git" - url: "https://github.com/conan-io/conan-center-index" + type: "" + url: "" revision: "" path: "" vcs_processed: - type: "Git" - url: "https://github.com/conan-io/conan-center-index.git" + type: "" + url: "" revision: "" path: "" - id: "Conan::openssl:1.1.1l" @@ -167,12 +167,12 @@ packages: algorithm: "" vcs: type: "Git" - url: "https://github.com/conan-io/conan-center-index" + url: "https://github.com/openssl/openssl.git" revision: "" path: "" vcs_processed: type: "Git" - url: "https://github.com/conan-io/conan-center-index.git" + url: "https://github.com/openssl/openssl.git" revision: "" path: "" - id: "Conan::zlib:1.2.11" @@ -195,12 +195,12 @@ packages: value: "" algorithm: "" vcs: - type: "Git" - url: "https://github.com/conan-io/conan-center-index" + type: "" + url: "" revision: "" path: "" vcs_processed: - type: "Git" - url: "https://github.com/conan-io/conan-center-index.git" + type: "" + url: "" revision: "" path: "" diff --git a/analyzer/src/main/kotlin/managers/Conan.kt b/analyzer/src/main/kotlin/managers/Conan.kt index 869e1df2e1b5b..b5191ff94529a 100644 --- a/analyzer/src/main/kotlin/managers/Conan.kt +++ b/analyzer/src/main/kotlin/managers/Conan.kt @@ -263,17 +263,20 @@ class Conan( /** * Return the [Package] parsed from the given [node]. */ - private fun parsePackage(node: JsonNode, workingDir: File) = - Package( + private fun parsePackage(node: JsonNode, workingDir: File): Package { + val homepageUrl = node["homepage"].textValueOrEmpty() + + return Package( id = parsePackageId(node, workingDir), authors = parseAuthors(node), declaredLicenses = parseDeclaredLicenses(node), description = parsePackageField(node, workingDir, "description"), - homepageUrl = node["homepage"].textValueOrEmpty(), + homepageUrl = homepageUrl, binaryArtifact = RemoteArtifact.EMPTY, // TODO: implement me! sourceArtifact = RemoteArtifact.EMPTY, // TODO: implement me! - vcs = parseVcsInfo(node) + vcs = processPackageVcs(VcsInfo.EMPTY, homepageUrl) ) + } /** * Return the value `conan inspect` reports for the given [field]. From eb77a1c2e106fbdd4685009937e308df65008690 Mon Sep 17 00:00:00 2001 From: Martin Nonnenmacher Date: Tue, 5 Oct 2021 01:14:30 +0200 Subject: [PATCH 9/9] Conan: Parse the source artifact from the local repository Try to parse the source artifact information for packages from the conandata.yml file in the local repository. Resolves #2037. Signed-off-by: Martin Nonnenmacher --- .../synthetic/conan-expected-output-py.yml | 12 +++---- .../synthetic/conan-expected-output-txt.yml | 36 +++++++++---------- analyzer/src/main/kotlin/managers/Conan.kt | 36 +++++++++++++++---- 3 files changed, 54 insertions(+), 30 deletions(-) diff --git a/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-py.yml b/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-py.yml index 9b64dca50d140..aac0ec38a0f26 100644 --- a/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-py.yml +++ b/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-py.yml @@ -44,10 +44,10 @@ packages: value: "" algorithm: "" source_artifact: - url: "" + url: "https://www.openssl.org/source/openssl-3.0.0.tar.gz" hash: - value: "" - algorithm: "" + value: "59eedfcb46c25214c9bd37ed6078297b4df01d012267fe9e9eee31f61bc70536" + algorithm: "SHA-256" vcs: type: "Git" url: "https://github.com/openssl/openssl.git" @@ -73,10 +73,10 @@ packages: value: "" algorithm: "" source_artifact: - url: "" + url: "https://zlib.net/zlib-1.2.11.tar.gz" hash: - value: "" - algorithm: "" + value: "c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1" + algorithm: "SHA-256" vcs: type: "" url: "" diff --git a/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml b/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml index 81fe940fd80bb..83531203c7be4 100644 --- a/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml +++ b/analyzer/src/funTest/assets/projects/synthetic/conan-expected-output-txt.yml @@ -45,10 +45,10 @@ packages: value: "" algorithm: "" source_artifact: - url: "" + url: "https://curl.se/download/curl-7.79.1.tar.gz" hash: - value: "" - algorithm: "" + value: "370b11201349816287fb0ccc995e420277fbfcaf76206e309b3f60f0eda090c2" + algorithm: "SHA-256" vcs: type: "" url: "" @@ -75,10 +75,10 @@ packages: value: "" algorithm: "" source_artifact: - url: "" + url: "https://ftp.gnu.org/gnu/libiconv/libiconv-1.16.tar.gz" hash: - value: "" - algorithm: "" + value: "e6a1b1b589654277ee790cce3734f07876ac4ccfaecbee8afa0b649cf529cc04" + algorithm: "SHA-256" vcs: type: "" url: "" @@ -103,10 +103,10 @@ packages: value: "" algorithm: "" source_artifact: - url: "" + url: "http://xmlsoft.org/sources/libxml2-2.9.12.tar.gz" hash: - value: "" - algorithm: "" + value: "c8d6681e38c56f172892c85ddc0852e1fd4b53b4209e7f4ebf17f7e2eae71d92" + algorithm: "SHA-256" vcs: type: "" url: "" @@ -132,10 +132,10 @@ packages: value: "" algorithm: "" source_artifact: - url: "" + url: "http://xmlsoft.org/sources/libxslt-1.1.34.tar.gz" hash: - value: "" - algorithm: "" + value: "98b1bd46d6792925ad2dfe9a87452ea2adebf69dcb9919ffd55bf926a7f93f7f" + algorithm: "SHA-256" vcs: type: "" url: "" @@ -161,10 +161,10 @@ packages: value: "" algorithm: "" source_artifact: - url: "" + url: "https://www.openssl.org/source/openssl-1.1.1l.tar.gz" hash: - value: "" - algorithm: "" + value: "0b7a3e5e59c34827fe0c3a74b7ec8baef302b98fa80088d7f9153aa16fa76bd1" + algorithm: "SHA-256" vcs: type: "Git" url: "https://github.com/openssl/openssl.git" @@ -190,10 +190,10 @@ packages: value: "" algorithm: "" source_artifact: - url: "" + url: "https://zlib.net/zlib-1.2.11.tar.gz" hash: - value: "" - algorithm: "" + value: "c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1" + algorithm: "SHA-256" vcs: type: "" url: "" diff --git a/analyzer/src/main/kotlin/managers/Conan.kt b/analyzer/src/main/kotlin/managers/Conan.kt index b5191ff94529a..6b4a3072a5f3b 100644 --- a/analyzer/src/main/kotlin/managers/Conan.kt +++ b/analyzer/src/main/kotlin/managers/Conan.kt @@ -34,6 +34,7 @@ import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory import org.ossreviewtoolkit.analyzer.PackageManager import org.ossreviewtoolkit.analyzer.parseAuthorString import org.ossreviewtoolkit.downloader.VersionControlSystem +import org.ossreviewtoolkit.model.Hash import org.ossreviewtoolkit.model.Identifier import org.ossreviewtoolkit.model.Package import org.ossreviewtoolkit.model.PackageReference @@ -46,6 +47,7 @@ import org.ossreviewtoolkit.model.VcsType import org.ossreviewtoolkit.model.config.AnalyzerConfiguration import org.ossreviewtoolkit.model.config.RepositoryConfiguration import org.ossreviewtoolkit.model.jsonMapper +import org.ossreviewtoolkit.model.yamlMapper import org.ossreviewtoolkit.utils.CommandLineTool import org.ossreviewtoolkit.utils.Os import org.ossreviewtoolkit.utils.ProcessCapture @@ -178,7 +180,7 @@ class Conan( val dependenciesJson = run(workingDir, "info", ".", "-j").stdout val pkgInfos = jsonMapper.readTree(dependenciesJson) val packageList = removeProjectPackage(pkgInfos, definitionFile.name) - val packages = parsePackages(packageList, workingDir) + val packages = parsePackages(packageList, workingDir, conanStoragePath) val dependenciesScope = Scope( name = SCOPE_NAME_DEPENDENCIES, @@ -254,26 +256,27 @@ class Conan( /** * Return the map of packages and their identifiers which are contained in [nodes]. */ - private fun parsePackages(nodes: List, workingDir: File): Map = + private fun parsePackages(nodes: List, workingDir: File, conanStoragePath: File): Map = nodes.associate { node -> - val pkg = parsePackage(node, workingDir) + val pkg = parsePackage(node, workingDir, conanStoragePath) "${pkg.id.name}:${pkg.id.version}" to pkg } /** * Return the [Package] parsed from the given [node]. */ - private fun parsePackage(node: JsonNode, workingDir: File): Package { + private fun parsePackage(node: JsonNode, workingDir: File, conanStoragePath: File): Package { + val id = parsePackageId(node, workingDir) val homepageUrl = node["homepage"].textValueOrEmpty() return Package( - id = parsePackageId(node, workingDir), + id = id, authors = parseAuthors(node), declaredLicenses = parseDeclaredLicenses(node), description = parsePackageField(node, workingDir, "description"), homepageUrl = homepageUrl, binaryArtifact = RemoteArtifact.EMPTY, // TODO: implement me! - sourceArtifact = RemoteArtifact.EMPTY, // TODO: implement me! + sourceArtifact = parseSourceArtifact(id, conanStoragePath), vcs = processPackageVcs(VcsInfo.EMPTY, homepageUrl) ) } @@ -338,6 +341,27 @@ class Conan( private fun parsePackageField(node: JsonNode, workingDir: File, field: String): String = inspectField(node["display_name"].textValue(), workingDir, field) + /** + * Try to read the source artifact from the [conanStoragePath], if not possible return [RemoteArtifact.EMPTY]. + */ + private fun parseSourceArtifact(id: Identifier, conanStoragePath: File): RemoteArtifact { + val conanDataFile = conanStoragePath.resolve("${id.name}/${id.version}/_/_/export/conandata.yml") + + return runCatching { + val conanData = yamlMapper.readTree(conanDataFile) + val artifactEntry = conanData["sources"][id.version] + + val url = artifactEntry["url"].let { urlNode -> + (urlNode.takeIf { it.isTextual } ?: urlNode.first()).textValueOrEmpty() + } + val hash = Hash.create(artifactEntry["sha256"].textValueOrEmpty()) + + RemoteArtifact(url, hash) + }.getOrElse { + RemoteArtifact.EMPTY + } + } + /** * Return a [Package] containing project-level information depending on which [definitionFile] was found: * - conanfile.txt: `conan inspect conanfile.txt` is not supported.