com.datadog.gradle.plugin.licenses.internal.DependenciesLicenseProvider.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dependency-license Show documentation
Show all versions of dependency-license Show documentation
This plugin generates the OSS licenses csv file for all dependencies.
The newest version!
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/
package com.datadog.gradle.plugin.licenses.internal
import com.datadog.gradle.plugin.licenses.utils.asSequence
import org.gradle.api.Project
import org.gradle.api.artifacts.component.ComponentIdentifier
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.artifacts.result.ComponentSelectionCause
import org.gradle.api.artifacts.result.ResolvedArtifactResult
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.maven.MavenModule
import org.gradle.maven.MavenPomArtifact
import org.slf4j.LoggerFactory
import org.w3c.dom.Document
import org.w3c.dom.Node
import javax.xml.parsers.DocumentBuilderFactory
@Suppress("TooManyFunctions")
internal class DependenciesLicenseProvider {
private val logger = LoggerFactory.getLogger(DependenciesLicenseProvider::class.java)
fun getThirdPartyDependencies(
project: Project,
transitive: Boolean,
listDependencyOnce: Boolean,
): List {
val dependencies = getConfigurationDependenciesMap(project, transitive)
val dependencyIds = dependencies.values.flatten()
val pomFilesList = resolvePomFiles(project, dependencyIds)
return listThirdPartyLicenses(dependencies, pomFilesList, listDependencyOnce)
}
// region Internal
@Suppress("FunctionMaxLength")
private fun getConfigurationDependenciesMap(
project: Project,
transitive: Boolean,
): Map> {
return project.configurations.filter { it.isCanBeResolved }
.map { configuration ->
configuration.name to configuration.incoming.resolutionResult.allDependencies
.filterIsInstance()
.filter {
transitive || (it.isRoot() && it.selected.id !is ProjectComponentIdentifier)
}
.map { it.selected.id }
}
.filter { it.second.isNotEmpty() }
.toMap()
}
private fun resolvePomFiles(
project: Project,
dependencyIds: List,
): Map {
return project.dependencies
.createArtifactResolutionQuery()
.withArtifacts(MavenModule::class.java, MavenPomArtifact::class.java)
.forComponents(dependencyIds)
.execute()
.resolvedComponents
.flatMap { result ->
result.getArtifacts(MavenPomArtifact::class.java)
.filterIsInstance()
.map { result.id to it.file.absolutePath }
}.toMap()
}
private fun listThirdPartyLicenses(
dependencies: Map>,
pomFilesList: Map,
listDependencyOnce: Boolean,
): List {
val licensesCache = mutableMapOf()
val sorted = dependencies
.map { (configuration, dependencies) ->
listThirdPartyLicensesInConfiguration(
configuration,
dependencies,
pomFilesList,
licensesCache,
)
}
.flatten()
.groupBy { it.origin }
.map { (origin, dependencies) ->
ThirdPartyDependency(
dependencies.map { it.component }.toSortedSet().firstOrNull()
?: ThirdPartyDependency.Component.BUILD,
origin,
dependencies.firstOrNull { it.license != License.Empty }?.license ?: License.Empty,
"",
dependencies.filter { !it.artifacts.isNullOrBlank() }
.distinctBy { it.artifacts }
.joinToString(",") { it.artifacts.orEmpty() },
)
}
.sortedBy { it.origin }
.sortedBy { it.component.ordinal }
return if (listDependencyOnce) {
val knownOrigins = mutableSetOf()
val result = mutableListOf()
sorted.forEach {
if (it.origin !in knownOrigins) {
result.add(it)
knownOrigins.add(it.origin)
} else {
logger.info("Ignoring ${it.component.csvName}/${it.origin}, already added.")
}
}
result
} else {
sorted
}
}
@Suppress("FunctionMaxLength")
private fun listThirdPartyLicensesInConfiguration(
configuration: String,
dependencies: List,
pomFilesList: Map,
licensesCache: MutableMap,
): List {
return dependencies
.filter { it !is ProjectComponentIdentifier }
.mapNotNull {
val pomFilePath = pomFilesList[it]
if (pomFilePath.isNullOrBlank()) {
logger.warn("Missing pom.xml file for dependency $it (${it.displayName} / ${it.javaClass.name})")
null
} else {
readInfoFromPomFile(configuration, pomFilePath, licensesCache)
}
}
}
private fun readInfoFromPomFile(
configuration: String,
path: String,
licensesCache: MutableMap,
): ThirdPartyDependency? {
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(path)
val groupId = readGroupIdFromPomDocument(document)
val versionString = readVersionFromDocument(document)
val licenseString = readLicenseFromPomDocument(document)
val artifactString = readArtifactFromPomDocument(document)
val dependency = if (groupId != null) {
val license =
getLicense(groupId, artifactString, versionString, licenseString, licensesCache)
ThirdPartyDependency(
component = configurationToComponent(configuration),
origin = groupId,
license = license,
copyright = "",
artifacts = "$artifactString:$versionString",
)
} else {
logger.warn("Missing groupId -> $path")
null
}
if (dependency?.component == ThirdPartyDependency.Component.UNKNOWN) {
logger.warn("Unknown configuration:'$configuration' for $groupId:$artifactString:$versionString")
}
return dependency
}
private fun getLicense(
groupId: String,
artifact: String?,
version: String?,
licenseString: String?,
licensesCache: MutableMap,
): License {
return if (artifact != null && version != null) {
val cacheKey = MavenCoordinates(
groupId,
artifact,
version,
)
if (licensesCache.containsKey(cacheKey)) {
licensesCache.getValue(cacheKey)
} else {
License.from(licenseString).also {
licensesCache[cacheKey] = it
}
}
} else {
License.from(licenseString)
}
}
private fun configurationToComponent(configuration: String): ThirdPartyDependency.Component {
return when (configuration) {
in knownImportConfiguration -> ThirdPartyDependency.Component.IMPORT
in knownImportTestConfiguration -> ThirdPartyDependency.Component.IMPORT_TEST
in knownBuildConfiguration -> ThirdPartyDependency.Component.BUILD
else -> ThirdPartyDependency.Component.UNKNOWN
}
}
private fun readGroupIdFromPomDocument(document: Document): String? {
val groupIdNode = document.documentElement
.getFirstDirectChildByTagName(TAG_GROUP_ID)
?: document.documentElement
.getFirstDirectChildByTagName(TAG_PARENT)
?.getFirstDirectChildByTagName(TAG_GROUP_ID)
return groupIdNode?.textContent
}
private fun readLicenseFromPomDocument(document: Document): String? {
val licenses = document.documentElement
.getFirstDirectChildByTagName(TAG_LICENSES)
?.getDirectChildrenByTagName(TAG_LICENSE)
?.mapNotNull {
it.getFirstDirectChildByTagName(TAG_NAME)
}
?.joinToString("/") { it.textContent }
return licenses
}
private fun readArtifactFromPomDocument(document: Document): String? {
return document.documentElement
.getFirstDirectChildByTagName(TAG_ARTIFACT_ID)
?.textContent
}
private fun readVersionFromDocument(document: Document): String? {
val versionNode = document.documentElement
.getFirstDirectChildByTagName(TAG_VERSION)
?: document.documentElement
.getFirstDirectChildByTagName(TAG_PARENT)
?.getFirstDirectChildByTagName(TAG_VERSION)
return versionNode?.textContent
}
private fun Node.getFirstDirectChildByTagName(tagName: String): Node? {
return getDirectChildrenByTagName(tagName).firstOrNull()
}
private fun Node.getDirectChildrenByTagName(tagName: String): Sequence {
return childNodes
.asSequence()
.filter { it.nodeName == tagName }
}
private fun ResolvedDependencyResult.isRoot(): Boolean {
return from.selectionReason.descriptions.any {
it.cause == ComponentSelectionCause.ROOT
}
}
// endregion
private data class MavenCoordinates(
private val groupId: String,
private val artifactId: String,
private val versionString: String,
)
companion object {
private const val TAG_PARENT = "parent"
private const val TAG_GROUP_ID = "groupId"
private const val TAG_ARTIFACT_ID = "artifactId"
private const val TAG_VERSION = "version"
private const val TAG_LICENSES = "licenses"
private const val TAG_LICENSE = "license"
private const val TAG_NAME = "name"
private val knownImportConfiguration = setOf(
"archives",
"compileClasspath",
"debugCompileClasspath",
"debugImplementationDependenciesMetadata",
"debugRuntimeClasspath",
"implementationDependenciesMetadata",
"jvmDebugRuntimeClasspath",
"jvmReleaseCompileClasspath",
"jvmReleaseRuntimeClasspath",
"releaseCompileClasspath",
"releaseImplementationDependenciesMetadata",
"releaseRuntimeClasspath",
"runtimeClasspath",
)
private val knownImportTestConfiguration = setOf(
"androidTestImplementationDependenciesMetadata",
"artDebugUnitTestCompileClasspath",
"artDebugUnitTestRuntimeClasspath",
"artReleaseUnitTestCompileClasspath",
"artReleaseUnitTestRuntimeClasspath",
"debugAndroidTestCompileClasspath",
"debugAndroidTestImplementationDependenciesMetadata",
"debugAndroidTestRuntimeClasspath",
"debugTestFixturesCompileClasspath",
"debugTestFixturesRuntimeClasspath",
"debugUnitTestCompileClasspath",
"debugUnitTestImplementationDependenciesMetadata",
"debugUnitTestRuntimeClasspath",
"jvmDebugAndroidTestCompileClasspath",
"jvmDebugAndroidTestRuntimeClasspath",
"jvmDebugUnitTestCompileClasspath",
"jvmDebugUnitTestRuntimeClasspath",
"jvmReleaseUnitTestCompileClasspath",
"jvmReleaseUnitTestRuntimeClasspath",
"kotlinCompilerPluginClasspathTest",
"releaseTestFixturesCompileClasspath",
"releaseTestFixturesCompileClasspath",
"releaseTestFixturesRuntimeClasspath",
"releaseUnitTestCompileClasspath",
"releaseUnitTestImplementationDependenciesMetadata",
"releaseUnitTestRuntimeClasspath",
"testCompileClasspath",
"testFixturesImplementationDependenciesMetadata",
"testImplementationDependenciesMetadata",
"testRuntimeClasspath",
"unmock",
)
private val knownBuildConfiguration = setOf(
"_agp_internal_javaPreCompileDebug_kspClasspath",
"_agp_internal_javaPreCompileRelease_kspClasspath",
"_internal_aapt2_binary",
"_internal-unified-test-platform-android-device-provider-ddmlib",
"_internal-unified-test-platform-android-device-provider-gradle",
"_internal-unified-test-platform-android-driver-instrumentation",
"_internal-unified-test-platform-android-test-plugin",
"_internal-unified-test-platform-android-test-plugin-host-additional-test-output",
"_internal-unified-test-platform-android-test-plugin-host-apk-installer",
"_internal-unified-test-platform-android-test-plugin-host-coverage",
"_internal-unified-test-platform-android-test-plugin-host-device-info",
"_internal-unified-test-platform-android-test-plugin-host-emulator-control",
"_internal-unified-test-platform-android-test-plugin-host-logcat",
"_internal-unified-test-platform-android-test-plugin-host-retention",
"_internal-unified-test-platform-android-test-plugin-result-listener-gradle",
"_internal-unified-test-platform-core",
"_internal-unified-test-platform-launcher",
"detekt",
"dokkaGfmPartialPlugin",
"dokkaGfmPartialRuntime",
"dokkaGfmPlugin",
"dokkaGfmRuntime",
"dokkaHtmlPartialPlugin",
"dokkaHtmlPartialRuntime",
"dokkaHtmlPlugin",
"dokkaHtmlRuntime",
"dokkaJavadocPartialPlugin",
"dokkaJavadocPartialRuntime",
"dokkaJavadocPlugin",
"dokkaJavadocRuntime",
"dokkaJekyllPartialPlugin",
"dokkaJekyllPartialRuntime",
"dokkaJekyllPlugin",
"dokkaJekyllRuntime",
"kotlin-extension",
"kotlinBuildToolsApiClasspath",
"kotlinCompilerClasspath",
"kotlinCompilerPluginClasspath",
"kotlinCompilerPluginClasspathDebug",
"kotlinCompilerPluginClasspathDebugAndroidTest",
"kotlinCompilerPluginClasspathDebugUnitTest",
"kotlinCompilerPluginClasspathMain",
"kotlinCompilerPluginClasspathRelease",
"kotlinCompilerPluginClasspathReleaseUnitTest",
"kotlinKlibCommonizerClasspath",
"koverJvmAgent",
"koverJvmReporter",
"kspDebugAndroidTestKotlinProcessorClasspath",
"kspDebugKotlinProcessorClasspath",
"kspDebugUnitTestKotlinProcessorClasspath",
"kspPluginClasspath",
"kspPluginClasspathNonEmbeddable",
"kspPluginClasspathNonEmbeddable",
"kspReleaseKotlinProcessorClasspath",
"kspReleaseUnitTestKotlinProcessorClasspath",
"lintClassPath",
)
}
}