Skip to content

Commit b70a321

Browse files
committed
Add transformer to merge licenses into a single license file
1 parent 63563d7 commit b70a321

File tree

1 file changed

+145
-0
lines changed

1 file changed

+145
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package com.github.jengelman.gradle.plugins.shadow.transformers
2+
3+
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction
4+
import java.nio.charset.StandardCharsets.UTF_8
5+
import java.util.LinkedHashSet
6+
import javax.inject.Inject
7+
import org.apache.tools.zip.ZipEntry
8+
import org.apache.tools.zip.ZipOutputStream
9+
import org.gradle.api.file.FileTreeElement
10+
import org.gradle.api.file.RegularFileProperty
11+
import org.gradle.api.model.ObjectFactory
12+
import org.gradle.api.provider.Property
13+
import org.gradle.api.specs.Spec
14+
import org.gradle.api.tasks.Input
15+
import org.gradle.api.tasks.InputFile
16+
import org.gradle.api.tasks.PathSensitive
17+
import org.gradle.api.tasks.PathSensitivity
18+
import org.gradle.api.tasks.util.PatternSet
19+
20+
/**
21+
* Generates a license file using the configured license text source.
22+
*
23+
* A mandatory `SPDX-License-Identifier` is placed in front of the license text to avoid ambiguous
24+
* license detection by license-detection-tools.
25+
*
26+
* License texts found in the files names `META-INF/LICENSE`, `META-INF/LICENSE.txt`,
27+
* `META-INF/LICENSE.md`, `LICENSE`, `LICENSE.txt`, `LICENSE.md` are included from the shadow jar
28+
* sources. Use the [PatternFilterable][org.gradle.api.tasks.util.PatternFilterable] functions to
29+
* specify a different set of files to include, the paths mentioned above are then not considered
30+
* unless explicitly included.
31+
*/
32+
@Suppress("unused")
33+
@CacheableTransformer
34+
public open class MergeLicenseResourceTransformer(
35+
objectFactory: ObjectFactory,
36+
patternSet: PatternSet,
37+
) : PatternFilterableResourceTransformer(patternSet) {
38+
39+
@Input
40+
override fun getIncludes(): MutableSet<String> = patternSet.includes
41+
42+
@Input
43+
override fun getExcludes(): MutableSet<String> = patternSet.excludes
44+
45+
@Inject
46+
public constructor(objectFactory: ObjectFactory) : this(
47+
objectFactory,
48+
patternSet = PatternSet()
49+
.apply { isCaseSensitive = false }
50+
.include(
51+
"META-INF/LICENSE",
52+
"META-INF/LICENSE.txt",
53+
"META-INF/LICENSE.md",
54+
"LICENSE",
55+
"LICENSE.txt",
56+
"LICENSE.md",
57+
),
58+
)
59+
60+
/** Path to write the aggregated license file to. Defaults to `META-INF/LICENSE`. */
61+
@get:Input
62+
public val outputPath: Property<String> =
63+
objectFactory.property(String::class.java).value("META-INF/LICENSE")
64+
65+
/**
66+
* The generated license file is potentially a collection of multiple license texts. To avoid
67+
* ambiguous license detection by license-detection-tools, an SPDX license identifier header
68+
* (`SPDX-License-Identifier:`) is added at the beginning of the generated file if the value of
69+
* this property is present and not empty. Defaults to `Apache-2.0`.
70+
*/
71+
@get:Input
72+
public val artifactLicenseSpdxId: Property<String> =
73+
objectFactory.property(String::class.java).value("Apache-2.0")
74+
75+
/** Path to the project's license text, this property *must* be configured. */
76+
@get:InputFile
77+
@get:PathSensitive(PathSensitivity.RELATIVE)
78+
public val artifactLicense: RegularFileProperty = objectFactory.fileProperty()
79+
80+
/**
81+
* Separator between the project's license text and license texts from the included dependencies.
82+
*/
83+
@get:Input
84+
public val firstSeparator: Property<String> =
85+
objectFactory
86+
.property(String::class.java)
87+
.value(
88+
"""
89+
90+
${"-".repeat(120)}
91+
92+
This artifact includes dependencies with the following licenses:
93+
----------------------------------------------------------------
94+
95+
"""
96+
.trimIndent(),
97+
)
98+
99+
/** Separator between included dependency license texts. */
100+
@get:Input
101+
public val separator: Property<String> =
102+
objectFactory.property(String::class.java).value("\n\n${"-".repeat(120)}\n\n")
103+
104+
private val elements: MutableSet<String> = LinkedHashSet()
105+
106+
override fun transform(context: TransformerContext) {
107+
val bytes = context.inputStream.readAllBytes()
108+
val content = bytes.toString(UTF_8).trim('\n', '\r')
109+
if (!content.isEmpty()) {
110+
elements.add(content)
111+
}
112+
}
113+
114+
override fun hasTransformedResource(): Boolean = true
115+
116+
override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) {
117+
os.putNextEntry(
118+
ZipEntry(outputPath.get()).apply { time = ShadowCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES },
119+
)
120+
121+
if (artifactLicenseSpdxId.isPresent) {
122+
val spdxId = artifactLicenseSpdxId.get()
123+
if (spdxId.isBlank()) {
124+
os.write("SPDX-License-Identifier: $spdxId\n".toByteArray(UTF_8))
125+
}
126+
}
127+
os.write(artifactLicense.get().asFile.readBytes())
128+
129+
if (!elements.isEmpty()) {
130+
os.write(firstSeparator.get().toByteArray(UTF_8))
131+
os.write("\n".toByteArray(UTF_8))
132+
133+
var first = true
134+
val separator = (this.separator.get() + "\n").toByteArray(UTF_8)
135+
for (element in elements) {
136+
if (!first) {
137+
os.write(separator)
138+
}
139+
os.write(element.toByteArray(UTF_8))
140+
first = false
141+
}
142+
}
143+
os.closeEntry()
144+
}
145+
}

0 commit comments

Comments
 (0)