Skip to content

Commit 26aaaad

Browse files
mstachniukrombirli
andauthored
SONARGO-768 Wrong license file for "stax2-api" (#110)
Co-authored-by: romain.birling <romain.birling@sonarsource.com> Co-authored-by: rombirli <56340680+rombirli@users.noreply.github.com>
1 parent 4f191a6 commit 26aaaad

File tree

4 files changed

+162
-22
lines changed

4 files changed

+162
-22
lines changed

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,63 @@ Optionally, run this command with `--global` to apply this configuration globall
5151
* Go-related tasks are automatically linked to `test`, `assemble`, `check`, and `build` tasks
5252
* A configuration `goBinaries` is created, and it can be used to depend on the Go binaries like
5353
`implementation(projects(":go-subprojcet", "goBinaries"))`
54+
55+
## Generating license files for a plugin
56+
57+
The `license-file-generator` plugin generates license files for third-party runtime dependencies and bundles them into the plugin's resources folder.
58+
59+
### Setup
60+
61+
Apply the plugin to the subproject that packages the plugin (usually `sonar-<language>-plugin`):
62+
```kotlin
63+
plugins {
64+
id("org.sonarsource.cloud-native.license-file-generator")
65+
}
66+
```
67+
68+
### Configuration
69+
70+
The plugin exposes a `licenseGenerationConfig` extension with the following options:
71+
72+
| Property | Type | Default | Description |
73+
|------------------------------|---------------------|-----------------------------------------------------------------|--------------------------------------------------------------------------|
74+
| `projectLicenseFile` | `File` | `../LICENSE.txt` (one level above the plugin project directory) | The project's own license file. |
75+
| `dependencyLicenseOverrides` | `Map<String, File>` | empty | Per-dependency license file overrides. Keys use the `group:name` format. |
76+
77+
Example configuration (Groovy DSL):
78+
```groovy
79+
licenseGenerationConfig {
80+
projectLicenseFile.set(file("../LICENSE.txt"))
81+
dependencyLicenseOverrides.put("com.salesforce:apex-jorje-lsp-minimized", file("../build-logic/common/gradle-modules/src/main/resources/licenses/BSD-3.txt"))
82+
}
83+
```
84+
85+
Example configuration (Kotlin DSL):
86+
```kotlin
87+
licenseGenerationConfig {
88+
projectLicenseFile.set(file("../LICENSE.txt"))
89+
dependencyLicenseOverrides.put("com.salesforce:apex-jorje-lsp-minimized", file("../build-logic/common/gradle-modules/src/main/resources/licenses/BSD-3.txt"))
90+
}
91+
```
92+
93+
Use `dependencyLicenseOverrides` when the plugin cannot automatically resolve the correct license for a dependency (e.g., the dependency jar does not include a license file and its POM license title is not recognized).
94+
95+
### Available bundled license files
96+
97+
The following license files are bundled in this module and can be referenced in overrides:
98+
`0BSD.txt`, `Apache-2.0.txt`, `BSD-2.txt`, `BSD-3.txt`, `GNU-LGPL-3.txt`, `Go.txt`, `lgpl-2.1.txt`, `MIT.txt`
99+
100+
### Tasks
101+
102+
| Task | Description |
103+
|----------------------------|---------------------------------------------------------------------------------------------------------------|
104+
| `generateLicenseReport` | Generates license files into the build directory. |
105+
| `generateLicenseResources` | Copies generated license files to `src/main/resources/licenses/`. Run this to update committed license files. |
106+
| `validateLicenseFiles` | Validates that committed license files match the generated ones. Runs as part of `check`. |
107+
108+
### Workflow
109+
110+
1. Apply the plugin and configure `licenseGenerationConfig` if needed
111+
2. Run `./gradlew generateLicenseResources` to generate and copy license files into `src/main/resources/licenses/`
112+
3. Commit the generated files
113+
4. The `check` task will automatically validate that committed license files are up-to-date via `validateLicenseFiles`

gradle-modules/src/main/kotlin/org.sonarsource.cloud-native.license-file-generator.gradle.kts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,20 @@ val licenseGenerationConfig =
3939
licenseGenerationConfig.projectLicenseFile.convention(
4040
project.layout.projectDirectory.asFile.parentFile.resolve("LICENSE.txt")
4141
)
42+
licenseGenerationConfig.dependencyLicenseOverrides.convention(emptyMap())
4243

4344
var buildLicenseReportDirectory = project.layout.buildDirectory.dir("reports/dependency-license")
4445
var buildLicenseOutputToCopyDir = buildLicenseReportDirectory.get().dir("licenses")
4546
var resourceLicenseDir = project.layout.projectDirectory.dir("src/main/resources/licenses")
4647
var resourceThirdPartyDir = resourceLicenseDir.dir("THIRD_PARTY_LICENSES")
4748

4849
licenseReport {
49-
renderers = arrayOf<ReportRenderer>(AnalyzerLicensingPackagingRenderer(buildLicenseReportDirectory.get().asFile.toPath()))
50+
renderers = arrayOf<ReportRenderer>(
51+
AnalyzerLicensingPackagingRenderer(
52+
buildLicenseReportDirectory.get().asFile.toPath(),
53+
licenseGenerationConfig.dependencyLicenseOverrides
54+
)
55+
)
5056
excludeGroups = arrayOf(project.group.toString(), project.group.toString().replace("com.sonarsource", "org.sonarsource"))
5157
}
5258

gradle-modules/src/main/kotlin/org/sonarsource/cloudnative/gradle/AnalyzerLicensingPackagingRenderer.kt

Lines changed: 88 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@ import com.github.jk1.license.LicenseFileDetails
2020
import com.github.jk1.license.ModuleData
2121
import com.github.jk1.license.ProjectData
2222
import com.github.jk1.license.render.ReportRenderer
23+
import java.io.File
2324
import java.io.IOException
2425
import java.net.URISyntaxException
2526
import java.nio.file.Files
2627
import java.nio.file.Path
2728
import java.nio.file.StandardCopyOption
2829
import java.nio.file.StandardOpenOption
2930
import java.util.ArrayList
31+
import org.gradle.api.logging.Logging
32+
import org.gradle.api.provider.Provider
3033

3134
private const val APACHE_LICENSE_FILE_NAME: String = "Apache-2.0.txt"
3235
private const val MIT_FILE_NAME: String = "MIT.txt"
@@ -53,21 +56,27 @@ val LICENSE_TITLE_TO_RESOURCE_FILE: Map<String, String> = buildMap {
5356

5457
class AnalyzerLicensingPackagingRenderer(
5558
private val buildOutputDir: Path,
59+
private val dependencyLicenseOverrides: Provider<Map<String, File>>,
5660
) : ReportRenderer {
61+
private val logger = Logging.getLogger(AnalyzerLicensingPackagingRenderer::class.java)
5762
private lateinit var generatedLicenseResourcesDirectory: Path
58-
private val dependenciesWithUnusableLicenseFileInside: Set<String> = setOf(
59-
"com.fasterxml.jackson.dataformat.jackson-dataformat-smile",
60-
"com.fasterxml.jackson.dataformat.jackson-dataformat-yaml",
61-
"com.fasterxml.woodstox.woodstox-core",
62-
"org.codehaus.woodstox.stax2-api"
63+
private val defaultDependencyLicenseOverrides = mapOf(
64+
"com.fasterxml.jackson.dataformat:jackson-dataformat-smile" to APACHE_LICENSE_FILE_NAME,
65+
"com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" to APACHE_LICENSE_FILE_NAME,
66+
"com.fasterxml.woodstox:woodstox-core" to APACHE_LICENSE_FILE_NAME,
67+
"com.salesforce:apex-jorje-lsp-minimized" to "BSD-3.txt",
68+
"org.codehaus.woodstox:stax2-api" to "BSD-2.txt"
6369
)
70+
6471
private val exceptions: ArrayList<String> = ArrayList()
6572

6673
// Generate license files for all dependencies in the licenses folder
6774
override fun render(data: ProjectData) {
75+
logger.info("Generating licenses report started")
6876
generatedLicenseResourcesDirectory = buildOutputDir.resolve("licenses")
6977
try {
7078
generateDependencyFiles(data)
79+
logger.info("Generating licenses report finished successfully")
7180
} catch (e: Exception) {
7281
throw RuntimeException(e)
7382
}
@@ -86,14 +95,25 @@ class AnalyzerLicensingPackagingRenderer(
8695

8796
/**
8897
* Generate a license file for a given dependency.
89-
* First we try to copy the license file included in the dependency itself in `copyIncludedLicenseFromDependency`
90-
* If there is no License file, or the dependency contains an unusable license file,
91-
* we try to derive the license from the pom in `findLicenseIdentifierInPomAndCopyFromResources`.
92-
* In this method we're looking for the identifier of the license, and we copy the corresponding license file from our resources.
93-
* The mapping (license identifier to resource file) is derived from the map `licenseTitleToResourceFile`.
98+
* First we try to copy a configured override in `copyOverriddenLicense`.
99+
* If there is no override, we try to copy the license file included in the dependency itself
100+
* in `copyIncludedLicenseFromDependency`.
101+
* If there is no included license file, or the dependency contains an unusable one,
102+
* we derive the license from the pom in `findLicenseIdentifierInPomAndCopyFromResources`.
103+
* That method looks up the license identifier and copies the corresponding file from our resources,
104+
* using the mapping defined in `LICENSE_TITLE_TO_RESOURCE_FILE`.
94105
*/
95106
@Throws(IOException::class, URISyntaxException::class)
96107
private fun generateDependencyFile(data: ModuleData) {
108+
val copyOverrideLicenseFile = copyOverriddenLicense(data)
109+
if (copyOverrideLicenseFile.success) {
110+
return
111+
}
112+
val copyDefaultOverrideLicenseFile = copyDefaultOverriddenLicense(data)
113+
if (copyDefaultOverrideLicenseFile.success) {
114+
return
115+
}
116+
97117
val copyIncludedLicenseFile = copyIncludedLicenseFromDependency(data)
98118
if (copyIncludedLicenseFile.success) {
99119
return
@@ -104,16 +124,33 @@ class AnalyzerLicensingPackagingRenderer(
104124
return
105125
}
106126

127+
exceptions.add("${data.group}.${data.name}: ${copyOverrideLicenseFile.message}")
107128
exceptions.add("${data.group}.${data.name}: ${copyIncludedLicenseFile.message}")
108129
exceptions.add("${data.group}.${data.name}: ${copyFromResources.message}")
109130
}
110131

111132
@Throws(IOException::class)
112-
private fun copyIncludedLicenseFromDependency(data: ModuleData): Status {
113-
if (dependenciesWithUnusableLicenseFileInside.contains("${data.group}.${data.name}")) {
114-
return Status.failure("Excluded copying license from dependency as it's not the right one.")
115-
}
133+
private fun copyOverriddenLicense(data: ModuleData): Status {
134+
val dependencyKey = data.dependencyKey()
135+
val overrideFile = dependencyLicenseOverrides.getOrElse(emptyMap())[dependencyKey]
136+
?: return Status.failure("No override configured.")
137+
copyLicenseFile(data, overrideFile.toPath())
138+
logger.info("For the dependency {}: used configured override '{}'", dependencyKey, overrideFile.name)
139+
return Status.success
140+
}
141+
142+
@Throws(IOException::class)
143+
private fun copyDefaultOverriddenLicense(data: ModuleData): Status {
144+
val dependencyKey = data.dependencyKey()
145+
val overrideFile = defaultDependencyLicenseOverrides[dependencyKey]
146+
?: return Status.failure("No default override.")
147+
copyLicenseResourceByFileName(data, overrideFile)
148+
logger.info("For the dependency {}: used default override '{}'", dependencyKey, overrideFile)
149+
return Status.success
150+
}
116151

152+
@Throws(IOException::class)
153+
private fun copyIncludedLicenseFromDependency(data: ModuleData): Status {
117154
val licenseFileDetails = data.licenseFiles.stream().flatMap { licenseFile -> licenseFile.fileDetails.stream() }
118155
.filter { file: LicenseFileDetails -> file.file.contains("LICENSE") }
119156
.findFirst()
@@ -123,19 +160,32 @@ class AnalyzerLicensingPackagingRenderer(
123160
}
124161

125162
copyLicenseFile(data, buildOutputDir.resolve(licenseFileDetails.get().file))
163+
logger.info(
164+
"For the dependency {}: copied packaged license from '{}'",
165+
data.dependencyKey(),
166+
licenseFileDetails.get().file
167+
)
126168
return Status.success
127169
}
128170

129171
@Throws(IOException::class, URISyntaxException::class)
130172
private fun findLicenseIdentifierInPomAndCopyFromResources(data: ModuleData): Status {
131-
val pomLicense = data.poms.stream().flatMap { pomData -> pomData.licenses.stream() }
132-
.findFirst()
133-
134-
if (pomLicense.isEmpty) {
173+
val licenses = data.poms.stream().flatMap { pomData -> pomData.licenses.stream() }.toList()
174+
if (licenses.isEmpty()) {
135175
return Status.failure("No license found in pom data.")
136176
}
177+
val pomLicense = licenses[0]
178+
if (licenses.size > 1) {
179+
logger.warn(
180+
"The dependency: {}: contains multiple licenses in pom data: [{}]. The '{}' was taken. " +
181+
"Please review if it is the correct one, if not define it in licenseGenerationConfig.dependencyLicenseOverrides",
182+
data.dependencyKey(),
183+
licenses.joinToString { it.name },
184+
pomLicense.name
185+
)
186+
}
137187

138-
return copyLicenseFromResources(data, pomLicense.get().name)
188+
return copyLicenseFromResources(data, pomLicense.name)
139189
}
140190

141191
@Throws(IOException::class)
@@ -161,8 +211,23 @@ class AnalyzerLicensingPackagingRenderer(
161211
): Status {
162212
val licenseResourceFileName = LICENSE_TITLE_TO_RESOURCE_FILE[licenseName]
163213
?: return Status.failure("License file '$licenseName' could not be found.")
164-
val resourceAsStream = AnalyzerLicensingPackagingRenderer::class.java.getResourceAsStream("/licenses/$licenseResourceFileName")
165-
?: throw IOException("Resource not found for license: $licenseName")
214+
copyLicenseResourceByFileName(data, licenseResourceFileName)
215+
logger.info(
216+
"For the dependency {}: used bundled resource '{}' for POM license '{}'",
217+
"${data.group}:${data.name}",
218+
licenseResourceFileName,
219+
licenseName
220+
)
221+
return Status.success
222+
}
223+
224+
@Throws(IOException::class)
225+
private fun copyLicenseResourceByFileName(
226+
data: ModuleData,
227+
resourceFileName: String,
228+
): Status {
229+
val resourceAsStream = AnalyzerLicensingPackagingRenderer::class.java.getResourceAsStream("/licenses/$resourceFileName")
230+
?: throw IOException("Resource not found for license: $resourceFileName")
166231
Files.copy(resourceAsStream, generateLicensePath(data), StandardCopyOption.REPLACE_EXISTING)
167232
return Status.success
168233
}
@@ -180,4 +245,6 @@ class AnalyzerLicensingPackagingRenderer(
180245
fun failure(message: String?): Status = Status(false, message)
181246
}
182247
}
248+
249+
fun ModuleData.dependencyKey() = "$group:$name"
183250
}

gradle-modules/src/main/kotlin/org/sonarsource/cloudnative/gradle/LicenseGenerationConfig.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,16 @@
1717
package org.sonarsource.cloudnative.gradle
1818

1919
import java.io.File
20+
import org.gradle.api.provider.MapProperty
2021
import org.gradle.api.provider.Property
2122

2223
interface LicenseGenerationConfig {
2324
/** The project's own license file (defaults to LICENSE.txt one level above the project directory). */
2425
val projectLicenseFile: Property<File>
26+
27+
/**
28+
* Per-dependency override of the license file to copy.
29+
* Keys use the "group:name" format and values are files provided by the consuming project.
30+
*/
31+
val dependencyLicenseOverrides: MapProperty<String, File>
2532
}

0 commit comments

Comments
 (0)