diff --git a/gradle-modules/src/main/kotlin/org.sonarsource.cloud-native.license-file-generator.gradle.kts b/gradle-modules/src/main/kotlin/org.sonarsource.cloud-native.license-file-generator.gradle.kts index b1dc13c..6d43705 100644 --- a/gradle-modules/src/main/kotlin/org.sonarsource.cloud-native.license-file-generator.gradle.kts +++ b/gradle-modules/src/main/kotlin/org.sonarsource.cloud-native.license-file-generator.gradle.kts @@ -39,6 +39,7 @@ val licenseGenerationConfig = licenseGenerationConfig.projectLicenseFile.convention( project.layout.projectDirectory.asFile.parentFile.resolve("LICENSE.txt") ) +licenseGenerationConfig.dependencyLicenseOverrides.convention(emptyMap()) var buildLicenseReportDirectory = project.layout.buildDirectory.dir("reports/dependency-license") var buildLicenseOutputToCopyDir = buildLicenseReportDirectory.get().dir("licenses") @@ -46,7 +47,12 @@ var resourceLicenseDir = project.layout.projectDirectory.dir("src/main/resources var resourceThirdPartyDir = resourceLicenseDir.dir("THIRD_PARTY_LICENSES") licenseReport { - renderers = arrayOf(AnalyzerLicensingPackagingRenderer(buildLicenseReportDirectory.get().asFile.toPath())) + renderers = arrayOf( + AnalyzerLicensingPackagingRenderer( + buildLicenseReportDirectory.get().asFile.toPath(), + licenseGenerationConfig.dependencyLicenseOverrides + ) + ) excludeGroups = arrayOf(project.group.toString(), project.group.toString().replace("com.sonarsource", "org.sonarsource")) } diff --git a/gradle-modules/src/main/kotlin/org/sonarsource/cloudnative/gradle/AnalyzerLicensingPackagingRenderer.kt b/gradle-modules/src/main/kotlin/org/sonarsource/cloudnative/gradle/AnalyzerLicensingPackagingRenderer.kt index 6051064..3ecdf95 100644 --- a/gradle-modules/src/main/kotlin/org/sonarsource/cloudnative/gradle/AnalyzerLicensingPackagingRenderer.kt +++ b/gradle-modules/src/main/kotlin/org/sonarsource/cloudnative/gradle/AnalyzerLicensingPackagingRenderer.kt @@ -27,6 +27,8 @@ import java.nio.file.Path import java.nio.file.StandardCopyOption import java.nio.file.StandardOpenOption import java.util.ArrayList +import org.gradle.api.logging.Logging +import org.gradle.api.provider.Provider private const val APACHE_LICENSE_FILE_NAME: String = "Apache-2.0.txt" private const val MIT_FILE_NAME: String = "MIT.txt" @@ -53,13 +55,15 @@ val LICENSE_TITLE_TO_RESOURCE_FILE: Map = buildMap { class AnalyzerLicensingPackagingRenderer( private val buildOutputDir: Path, + private val dependencyLicenseOverrides: Provider>, ) : ReportRenderer { + private val logger = Logging.getLogger(AnalyzerLicensingPackagingRenderer::class.java) private lateinit var generatedLicenseResourcesDirectory: Path private val dependenciesWithUnusableLicenseFileInside: Set = setOf( - "com.fasterxml.jackson.dataformat.jackson-dataformat-smile", - "com.fasterxml.jackson.dataformat.jackson-dataformat-yaml", - "com.fasterxml.woodstox.woodstox-core", - "org.codehaus.woodstox.stax2-api" + "com.fasterxml.jackson.dataformat:jackson-dataformat-smile", + "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", + "com.fasterxml.woodstox:woodstox-core", + "org.codehaus.woodstox:stax2-api" ) private val exceptions: ArrayList = ArrayList() @@ -86,14 +90,21 @@ class AnalyzerLicensingPackagingRenderer( /** * Generate a license file for a given dependency. - * First we try to copy the license file included in the dependency itself in `copyIncludedLicenseFromDependency` - * If there is no License file, or the dependency contains an unusable license file, - * we try to derive the license from the pom in `findLicenseIdentifierInPomAndCopyFromResources`. - * In this method we're looking for the identifier of the license, and we copy the corresponding license file from our resources. - * The mapping (license identifier to resource file) is derived from the map `licenseTitleToResourceFile`. + * First we try to copy a configured override in `copyOverriddenLicense`. + * If there is no override, we try to copy the license file included in the dependency itself + * in `copyIncludedLicenseFromDependency`. + * If there is no included license file, or the dependency contains an unusable one, + * we derive the license from the pom in `findLicenseIdentifierInPomAndCopyFromResources`. + * That method looks up the license identifier and copies the corresponding file from our resources, + * using the mapping defined in `LICENSE_TITLE_TO_RESOURCE_FILE`. */ @Throws(IOException::class, URISyntaxException::class) private fun generateDependencyFile(data: ModuleData) { + val copyOverrideLicenseFile = copyOverriddenLicense(data) + if (copyOverrideLicenseFile.success) { + return + } + val copyIncludedLicenseFile = copyIncludedLicenseFromDependency(data) if (copyIncludedLicenseFile.success) { return @@ -104,14 +115,26 @@ class AnalyzerLicensingPackagingRenderer( return } + exceptions.add("${data.group}.${data.name}: ${copyOverrideLicenseFile.message}") exceptions.add("${data.group}.${data.name}: ${copyIncludedLicenseFile.message}") exceptions.add("${data.group}.${data.name}: ${copyFromResources.message}") } + @Throws(IOException::class) + private fun copyOverriddenLicense(data: ModuleData): Status { + val dependencyKey = "${data.group}:${data.name}" + val overrideFile = dependencyLicenseOverrides.getOrElse(emptyMap())[dependencyKey] + ?: return Status.failure("No override configured.") + copyLicenseFile(data, overrideFile.toPath()) + logger.info("{}: used configured override '{}'", dependencyKey, overrideFile.name) + return Status.success + } + @Throws(IOException::class) private fun copyIncludedLicenseFromDependency(data: ModuleData): Status { - if (dependenciesWithUnusableLicenseFileInside.contains("${data.group}.${data.name}")) { - return Status.failure("Excluded copying license from dependency as it's not the right one.") + val dependencyKey = "${data.group}:${data.name}" + if (dependenciesWithUnusableLicenseFileInside.contains(dependencyKey)) { + return Status.failure("Skipped packaged license because this dependency is on the unusable-license list.") } val licenseFileDetails = data.licenseFiles.stream().flatMap { licenseFile -> licenseFile.fileDetails.stream() } @@ -123,6 +146,7 @@ class AnalyzerLicensingPackagingRenderer( } copyLicenseFile(data, buildOutputDir.resolve(licenseFileDetails.get().file)) + logger.info("{}: copied packaged license '{}'", dependencyKey, licenseFileDetails.get().file) return Status.success } @@ -161,8 +185,23 @@ class AnalyzerLicensingPackagingRenderer( ): Status { val licenseResourceFileName = LICENSE_TITLE_TO_RESOURCE_FILE[licenseName] ?: return Status.failure("License file '$licenseName' could not be found.") - val resourceAsStream = AnalyzerLicensingPackagingRenderer::class.java.getResourceAsStream("/licenses/$licenseResourceFileName") - ?: throw IOException("Resource not found for license: $licenseName") + copyLicenseResourceByFileName(data, licenseResourceFileName) + logger.info( + "{}: used bundled resource '{}' for POM license '{}'", + "${data.group}:${data.name}", + licenseResourceFileName, + licenseName + ) + return Status.success + } + + @Throws(IOException::class) + private fun copyLicenseResourceByFileName( + data: ModuleData, + resourceFileName: String, + ): Status { + val resourceAsStream = AnalyzerLicensingPackagingRenderer::class.java.getResourceAsStream("/licenses/$resourceFileName") + ?: throw IOException("Resource not found for license: $resourceFileName") Files.copy(resourceAsStream, generateLicensePath(data), StandardCopyOption.REPLACE_EXISTING) return Status.success } diff --git a/gradle-modules/src/main/kotlin/org/sonarsource/cloudnative/gradle/LicenseGenerationConfig.kt b/gradle-modules/src/main/kotlin/org/sonarsource/cloudnative/gradle/LicenseGenerationConfig.kt index 891450a..167e9ce 100644 --- a/gradle-modules/src/main/kotlin/org/sonarsource/cloudnative/gradle/LicenseGenerationConfig.kt +++ b/gradle-modules/src/main/kotlin/org/sonarsource/cloudnative/gradle/LicenseGenerationConfig.kt @@ -17,9 +17,16 @@ package org.sonarsource.cloudnative.gradle import java.io.File +import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property interface LicenseGenerationConfig { /** The project's own license file (defaults to LICENSE.txt one level above the project directory). */ val projectLicenseFile: Property + + /** + * Per-dependency override of the license file to copy. + * Keys use the "group:name" format and values are files provided by the consuming project. + */ + val dependencyLicenseOverrides: MapProperty }