Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conan improvements #8899

Merged
merged 4 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 59 additions & 111 deletions plugins/package-managers/conan/src/main/kotlin/Conan.kt
Original file line number Diff line number Diff line change
Expand Up @@ -164,24 +164,23 @@ class Conan(
run(workingDir, "info", definitionFile.name, "--json", jsonFile.absolutePath, *DUMMY_COMPILER_SETTINGS)
}

val pkgInfos = jsonMapper.readTree(jsonFile)
val pkgInfos = parsePackageInfos(jsonFile)
jsonFile.parentFile.safeDeleteRecursively(force = true)

val packageList = removeProjectPackage(pkgInfos, definitionFile.name)
val packages = parsePackages(packageList, workingDir)
val projectInfo = findProjectPackageInfo(pkgInfos, definitionFile.name)

val dependenciesScope = Scope(
name = SCOPE_NAME_DEPENDENCIES,
dependencies =
parseDependencies(pkgInfos, definitionFile.name, SCOPE_NAME_DEPENDENCIES, workingDir)
dependencies = parseDependencyTree(pkgInfos, projectInfo.requires, workingDir)
)
val devDependenciesScope = Scope(
name = SCOPE_NAME_DEV_DEPENDENCIES,
dependencies =
parseDependencies(pkgInfos, definitionFile.name, SCOPE_NAME_DEV_DEPENDENCIES, workingDir)
dependencies = parseDependencyTree(pkgInfos, projectInfo.buildRequires, workingDir)
)

val projectPackage = parseProjectPackage(pkgInfos, definitionFile, workingDir)
val projectPackage = generateProjectPackage(projectInfo, definitionFile, workingDir)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also worth noting in the commit message: The caller's projectInfo equals the original function's projectPackage, so the inlining also avoids calculating the value again.


return listOf(
ProjectAnalyzerResult(
Expand Down Expand Up @@ -261,63 +260,50 @@ class Conan(
}

/**
* Return the dependency tree for [pkg] for the given [scopeName].
* Return the dependency tree for the given [direct scope dependencies][requires].
*/
private fun parseDependencyTree(
pkgInfos: JsonNode,
pkg: JsonNode,
scopeName: String,
pkgInfos: List<PackageInfo>,
requires: List<String>,
workingDir: File
): Set<PackageReference> =
buildSet {
pkg[scopeName]?.forEach { childNode ->
val childRef = childNode.textValueOrEmpty()
pkgInfos.find { it["reference"].textValueOrEmpty() == childRef }?.let { pkgInfo ->
requires.forEach { childRef ->
pkgInfos.find { it.reference.orEmpty() == childRef }?.let { pkgInfo ->
logger.debug { "Found child '$childRef'." }

val id = parsePackageId(pkgInfo, workingDir)
val dependencies = parseDependencyTree(pkgInfos, pkgInfo, SCOPE_NAME_DEPENDENCIES, workingDir) +
parseDependencyTree(pkgInfos, pkgInfo, SCOPE_NAME_DEV_DEPENDENCIES, workingDir)
val dependencies = parseDependencyTree(pkgInfos, pkgInfo.requires, workingDir) +
parseDependencyTree(pkgInfos, pkgInfo.buildRequires, workingDir)

this += PackageReference(id, dependencies = dependencies)
}
}
}

/**
* Run through each package and parse the list of its dependencies (also transitive ones).
* Return the map of packages and their identifiers which are contained in [pkgInfos].
*/
private fun parseDependencies(
pkgInfos: JsonNode,
definitionFileName: String,
scopeName: String,
workingDir: File
): Set<PackageReference> =
parseDependencyTree(pkgInfos, findProjectNode(pkgInfos, definitionFileName), scopeName, workingDir)

/**
* Return the map of packages and their identifiers which are contained in [nodes].
*/
private fun parsePackages(nodes: List<JsonNode>, workingDir: File): Map<String, Package> =
nodes.associate { node ->
val pkg = parsePackage(node, workingDir)
private fun parsePackages(pkgInfos: List<PackageInfo>, workingDir: File): Map<String, Package> =
pkgInfos.associate { pkgInfo ->
val pkg = parsePackage(pkgInfo, workingDir)
"${pkg.id.name}:${pkg.id.version}" to pkg
}

/**
* Return the [Package] parsed from the given [node].
* Return the [Package] parsed from the given [pkgInfo].
*/
private fun parsePackage(node: JsonNode, workingDir: File): Package {
val homepageUrl = node["homepage"].textValueOrEmpty()
private fun parsePackage(pkgInfo: PackageInfo, workingDir: File): Package {
val homepageUrl = pkgInfo.homepage.orEmpty()

val id = parsePackageId(node, workingDir)
val id = parsePackageId(pkgInfo, workingDir)
val conanData = readConanData(id)

return Package(
id = id,
authors = parseAuthors(node),
declaredLicenses = parseDeclaredLicenses(node),
description = parsePackageField(node, workingDir, "description"),
authors = parseAuthors(pkgInfo),
declaredLicenses = pkgInfo.license.toSet(),
description = parsePackageField(pkgInfo, workingDir, "description"),
homepageUrl = homepageUrl,
binaryArtifact = RemoteArtifact.EMPTY, // TODO: implement me!
sourceArtifact = parseSourceArtifact(conanData),
Expand All @@ -337,54 +323,46 @@ class Conan(
}.get(field).textValueOrEmpty()

/**
* Find the node that represents the project defined in the definition file.
* Find the [PackageInfo] that represents the project defined in the definition file.
*/
private fun findProjectNode(pkgInfos: JsonNode, definitionFileName: String): JsonNode =
private fun findProjectPackageInfo(pkgInfos: List<PackageInfo>, definitionFileName: String): PackageInfo =
pkgInfos.first {
// Use "in" because conanfile.py's reference string often includes other data.
definitionFileName in it["reference"].textValueOrEmpty()
definitionFileName in it.reference.orEmpty()
}

/**
* Return the full list of packages, excluding the project level information.
*/
private fun removeProjectPackage(pkgInfos: JsonNode, definitionFileName: String): List<JsonNode> =
pkgInfos.minusElement(findProjectNode(pkgInfos, definitionFileName))

/**
* Return the set of declared licenses contained in [node].
*/
private fun parseDeclaredLicenses(node: JsonNode): Set<String> =
mutableSetOf<String>().also { licenses ->
node["license"]?.mapNotNullTo(licenses) { it.textValue() }
}
private fun removeProjectPackage(pkgInfos: List<PackageInfo>, definitionFileName: String): List<PackageInfo> =
pkgInfos.minusElement(findProjectPackageInfo(pkgInfos, definitionFileName))

/**
* Return the [Identifier] for the package contained in [node].
* Return the [Identifier] for the package contained in [pkgInfo].
*/
private fun parsePackageId(node: JsonNode, workingDir: File) =
private fun parsePackageId(pkgInfo: PackageInfo, workingDir: File) =
Identifier(
type = "Conan",
namespace = "",
name = parsePackageField(node, workingDir, "name"),
version = parsePackageField(node, workingDir, "version")
name = parsePackageField(pkgInfo, workingDir, "name"),
version = parsePackageField(pkgInfo, workingDir, "version")
)

/**
* Return the [VcsInfo] contained in [node].
* Return the [VcsInfo] contained in [pkgInfo].
*/
private fun parseVcsInfo(node: JsonNode): VcsInfo {
val revision = node["revision"].textValueOrEmpty()
val url = node["url"].textValueOrEmpty()
private fun parseVcsInfo(pkgInfo: PackageInfo): VcsInfo {
val revision = pkgInfo.revision.orEmpty()
val url = pkgInfo.url.orEmpty()
val vcsInfo = VcsHost.parseUrl(url)
return if (revision == "0") vcsInfo else vcsInfo.copy(revision = revision)
}

/**
* Return the value of [field] from the output of `conan inspect --raw` for the package in [node].
* Return the value of [field] from the output of `conan inspect --raw` for the package in [pkgInfo].
*/
private fun parsePackageField(node: JsonNode, workingDir: File, field: String): String =
inspectField(node["display_name"].textValue(), workingDir, field)
private fun parsePackageField(pkgInfo: PackageInfo, workingDir: File, field: String): String =
inspectField(pkgInfo.displayName, workingDir, field)

/**
* Return the generic map of Conan data for the [id].
Expand All @@ -409,8 +387,8 @@ class Conan(
* Return the source artifact contained in [conanData], or [RemoteArtifact.EMPTY] if no source artifact is
* available.
*/
private fun parseSourceArtifact(conanData: Map<String, JsonNode>): RemoteArtifact {
return runCatching {
private fun parseSourceArtifact(conanData: Map<String, JsonNode>): RemoteArtifact =
runCatching {
val artifactEntry = conanData.getValue("sources")

val url = artifactEntry["url"].let { urlNode ->
Expand All @@ -423,71 +401,41 @@ class Conan(
}.getOrElse {
RemoteArtifact.EMPTY
}
}

/**
* Return a [Package] containing project-level information depending on which [definitionFile] was found:
* Return a [Package] containing project-level information from [pkgInfo] and [definitionFile] using the
* `conan inspect` command if possible:
* - conanfile.txt: `conan inspect conanfile.txt` is not supported.
* - 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 parseProjectPackage(pkgInfos: JsonNode, definitionFile: File, workingDir: File): Package {
val projectPackageJson = findProjectNode(pkgInfos, definitionFile.name)

return if (definitionFile.name == "conanfile.py") {
generateProjectPackageFromConanfilePy(projectPackageJson, definitionFile, workingDir)
} else {
generateProjectPackageFromConanfileTxt(projectPackageJson)
}
}

/**
* 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 =
Package(
id = Identifier(
type = managerName,
namespace = "",
name = inspectField(definitionFile.name, workingDir, "name"),
version = inspectField(definitionFile.name, workingDir, "version")
),
authors = parseAuthors(node),
declaredLicenses = parseDeclaredLicenses(node),
description = inspectField(definitionFile.name, workingDir, "description"),
homepageUrl = node["homepage"].textValueOrEmpty(),
binaryArtifact = RemoteArtifact.EMPTY, // TODO: implement me!
sourceArtifact = RemoteArtifact.EMPTY, // TODO: implement me!
vcs = parseVcsInfo(node)
)
private fun generateProjectPackage(pkgInfo: PackageInfo, definitionFile: File, workingDir: File): Package {
fun inspectOrNull(field: String) =
definitionFile.name.takeIf { it == "conanfile.py" }?.let { inspectField(it, workingDir, field) }

/**
* Return a [Package] containing project-level information parsed from [node].
*/
private fun generateProjectPackageFromConanfileTxt(node: JsonNode): Package =
Package(
return Package(
id = Identifier(
type = managerName,
namespace = "",
name = node["reference"].textValueOrEmpty(),
version = ""
name = inspectOrNull("name") ?: pkgInfo.reference.orEmpty(),
version = inspectOrNull("version").orEmpty()
),
authors = parseAuthors(node),
declaredLicenses = parseDeclaredLicenses(node),
description = "",
homepageUrl = node["homepage"].textValueOrEmpty(),
authors = parseAuthors(pkgInfo),
declaredLicenses = pkgInfo.license.toSet(),
description = inspectOrNull("description").orEmpty(),
homepageUrl = pkgInfo.homepage.orEmpty(),
binaryArtifact = RemoteArtifact.EMPTY, // TODO: implement me!
sourceArtifact = RemoteArtifact.EMPTY, // TODO: implement me!
vcs = parseVcsInfo(node)
vcs = parseVcsInfo(pkgInfo)
)
}

/**
* Parse information about the package author from the given JSON [node]. If present, return a set containing the
* author name; otherwise, return an empty set.
* Parse information about the package author from the given [package info][pkgInfo]. If present, return a set
* containing the author name; otherwise, return an empty set.
*/
private fun parseAuthors(node: JsonNode): Set<String> =
setOfNotNull(parseAuthorString(node["author"]?.textValue(), '<', '('))
private fun parseAuthors(pkgInfo: PackageInfo): Set<String> =
setOfNotNull(parseAuthorString(pkgInfo.author.orEmpty(), '<', '('))
}
45 changes: 45 additions & 0 deletions plugins/package-managers/conan/src/main/kotlin/PackageInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.ossreviewtoolkit.plugins.packagemanagers.conan

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.module.kotlin.readValue

import java.io.File

import org.ossreviewtoolkit.model.jsonMapper

private val mapper = jsonMapper.copy().setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)

internal fun parsePackageInfos(file: File): List<PackageInfo> = mapper.readValue<List<PackageInfo>>(file)

@JsonIgnoreProperties(ignoreUnknown = true)
internal data class PackageInfo(
val reference: String? = null,
val author: String? = null,
val license: List<String> = emptyList(),
val homepage: String? = null,
val revision: String? = null,
val url: String? = null,
val displayName: String,
val requires: List<String> = emptyList(),
val buildRequires: List<String> = emptyList()
)
Loading