Skip to content

Commit 196a81f

Browse files
committed
chore: #14886 add licensing & notice files for grails-cli shadowJar
1 parent 6968298 commit 196a81f

File tree

8 files changed

+402
-91
lines changed

8 files changed

+402
-91
lines changed

grails-forge/buildSrc/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ dependencies {
8484
}
8585
implementation "org.antlr:antlr4-runtime:$antlr4Version"
8686
implementation "org.gradle.crypto.checksum:org.gradle.crypto.checksum.gradle.plugin:$gradleChecksumPluginVersion"
87+
implementation "org.apache.ant:ant:$antVersion"
8788
}
8889

8990
gradlePlugin {
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.grails.buildsrc
18+
19+
import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer
20+
import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext
21+
import groovy.transform.CompileDynamic
22+
import groovy.transform.CompileStatic
23+
import groovy.util.logging.Slf4j
24+
import org.apache.tools.zip.ZipEntry
25+
import org.apache.tools.zip.ZipOutputStream
26+
import org.gradle.api.file.FileTreeElement
27+
import org.gradle.api.tasks.Input
28+
29+
import java.util.regex.Pattern
30+
31+
/**
32+
* supports combining into a single license file.
33+
*/
34+
@Slf4j
35+
@CompileStatic
36+
class GrailsShadowLicenseTransform implements Transformer {
37+
38+
private static final List<Pattern> LICENSE_PATTERNS = [
39+
Pattern.compile('META-INF/[^/]*LICENSE[^/]*', Pattern.CASE_INSENSITIVE),
40+
Pattern.compile('META-INF/LICENSES/.*', Pattern.CASE_INSENSITIVE),
41+
Pattern.compile('[^/]*LICENSE[^/]*', Pattern.CASE_INSENSITIVE),
42+
Pattern.compile('LICENSES/.*', Pattern.CASE_INSENSITIVE)
43+
]
44+
45+
private static final String LICENSE_PATH = 'META-INF/LICENSE'
46+
47+
private LinkedHashMap<String, LicenseHolder> licenses = [:]
48+
49+
@Input
50+
String licenseAppendixEnding = 'LIMITATIONS UNDER THE LICENSE.'
51+
52+
@Input
53+
String licenseTermsEnding = 'END OF TERMS AND CONDITIONS'
54+
55+
@Input
56+
String licenseTermsStart = 'APACHE LICENSE VERSION 2.0'
57+
58+
@Input
59+
String licenseText // to be loaded by file
60+
61+
@Input
62+
Boolean separators = false
63+
64+
@Override
65+
boolean canTransformResource(FileTreeElement element) {
66+
def path = element.relativePath.pathString
67+
LICENSE_PATTERNS.any { pattern -> pattern.matcher(path).matches() }
68+
}
69+
70+
@Override
71+
@CompileDynamic
72+
// Multiple assignments without list expressions on the right hand side are unsupported in static type checking mode
73+
void transform(TransformerContext context) {
74+
if (!licenses) {
75+
// Add our license as previously seen so we can dedupe - this transformer only applies to the copy of other jars
76+
def (grailsLicense, grailsIndexMappings) = normalize(licenseText)
77+
licenses[grailsLicense] = new LicenseHolder(license: licenseText, indexMappings: grailsIndexMappings)
78+
}
79+
80+
context.is.withReader {
81+
BufferedReader reader = new BufferedReader(it)
82+
83+
def license = stripJavaBlockComment(reader.text)
84+
def (String normalized, List<Integer> indexMappings) = normalize(license)
85+
86+
// resect Apache License
87+
String resected = resectLicense(license, normalized, indexMappings)
88+
if (!resected.trim()) {
89+
return // only contained duplicated license terms with the ASF license
90+
}
91+
92+
def (String resectedNormalized, List<Integer> resectedIndexMappings) = normalize(resected)
93+
def previouslySeen = getVariations(resectedNormalized).any { licenses.containsKey(it) }
94+
if (!previouslySeen) {
95+
licenses[resectedNormalized] = new LicenseHolder(license: resected, indexMappings: resectedIndexMappings)
96+
}
97+
}
98+
}
99+
100+
private static String stripJavaBlockComment(String text) {
101+
if (!text.startsWith('/*')) {
102+
return text
103+
}
104+
105+
return text
106+
.replaceAll('^/\\*+|\\*+/\\s*$', '') // opening & closing comment
107+
.readLines()
108+
.collect {
109+
it.replaceFirst(/^(\s*\*)?/, '').trim()
110+
} // leading whitespace & *
111+
.join('\n')
112+
}
113+
114+
private static Tuple2<String, List<Integer>> normalize(String license) {
115+
def sb = new StringBuilder()
116+
List<Integer> indexMappings = [] // each char in sb maps to original index
117+
118+
boolean previousWhitespace = false
119+
for (int i = 0; i < license.length(); i++) {
120+
char c = license.charAt(i)
121+
if (c.isWhitespace()) {
122+
if (!previousWhitespace) {
123+
sb.append(' ')
124+
indexMappings << i
125+
previousWhitespace = true
126+
}
127+
} else {
128+
sb.append(Character.toUpperCase(c))
129+
indexMappings << i
130+
previousWhitespace = false
131+
}
132+
}
133+
134+
String normalized = sb.toString().trim()
135+
int startTrim = sb.indexOf(normalized)
136+
int endTrim = startTrim + normalized.length()
137+
new Tuple2<String, List<Integer>>(normalized, indexMappings[startTrim..<endTrim])
138+
}
139+
140+
private String resectLicense(String license, String normalized, List<Integer> indexMappings) {
141+
if (!normalized.startsWith(licenseTermsStart.toUpperCase())) {
142+
return license // not ASF license, return as is
143+
}
144+
145+
// try to search on the appendix first
146+
String endOfLicenseMarker = normalize(licenseAppendixEnding).v1
147+
int end1Index = normalized.indexOf(endOfLicenseMarker)
148+
if (end1Index >= 0) {
149+
// license included the appendix
150+
def originalEnding = indexMappings[end1Index + endOfLicenseMarker.size() - 1] + 1
151+
if (originalEnding > license.length()) {
152+
// only the license is present
153+
return null
154+
}
155+
156+
return license.substring(originalEnding)
157+
}
158+
159+
// try to search on the terms ending
160+
String endMarker = normalize(licenseTermsEnding).v1
161+
int end2Index = normalized.indexOf(endMarker)
162+
if (end2Index >= 0) {
163+
// bare license
164+
def originalEnding = indexMappings[end2Index + endMarker.size() - 1] + 1
165+
if (originalEnding > license.length()) {
166+
// only the license is present
167+
return null
168+
}
169+
170+
return license.substring(originalEnding)
171+
}
172+
173+
license
174+
}
175+
176+
private static List<String> getVariations(String license) {
177+
[license.trim()].collectMany {
178+
[it, it.replace('http://', 'https://'), it.replace('https://', 'http://')]
179+
}
180+
}
181+
182+
@Override
183+
boolean hasTransformedResource() {
184+
true
185+
}
186+
187+
@Override
188+
void modifyOutputStream(ZipOutputStream os, boolean preserveFileTimestamps) {
189+
ZipEntry zipEntry = new ZipEntry(LICENSE_PATH)
190+
zipEntry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, zipEntry.time)
191+
os.putNextEntry(zipEntry)
192+
193+
os.withPrintWriter { writer ->
194+
licenses.entrySet().withIndex().each { license ->
195+
if (license.v1.value == null) {
196+
return // skip the license that will be copied by shadow from our existing jars
197+
}
198+
199+
writer.println(license.v1.value.license)
200+
if (separators && license.v2 < licenses.size() - 1) {
201+
writer.println("-------------------------${license.v2}---------------------------")
202+
}
203+
}
204+
205+
writer.flush()
206+
}
207+
208+
licenses = [:]
209+
}
210+
211+
private static class LicenseHolder {
212+
213+
String license
214+
List<Integer> indexMappings
215+
}
216+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.grails.buildsrc
18+
19+
import com.github.jengelman.gradle.plugins.shadow.transformers.ApacheNoticeResourceTransformer
20+
import groovy.transform.CompileStatic
21+
import groovy.util.logging.Slf4j
22+
import org.gradle.api.file.FileTreeElement
23+
24+
import java.util.regex.Pattern
25+
26+
/**
27+
* jakarta is eclipse licensed, so we need to include the NOTICE.md file
28+
*/
29+
@Slf4j
30+
@CompileStatic
31+
class GrailsShadowNoticeTransform extends ApacheNoticeResourceTransformer {
32+
33+
private static final List<Pattern> NOTICE_PATTERNS = [
34+
Pattern.compile('META-INF/[^/]*NOTICE[^/]*', Pattern.CASE_INSENSITIVE),
35+
Pattern.compile('[^/]*NOTICE[^/]*', Pattern.CASE_INSENSITIVE)
36+
]
37+
38+
@Override
39+
boolean canTransformResource(FileTreeElement element) {
40+
if (super.canTransformResource(element)) {
41+
return true
42+
}
43+
44+
def path = element.relativePath.pathString
45+
NOTICE_PATTERNS.any { pattern -> pattern.matcher(path).matches() }
46+
}
47+
}

grails-forge/gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ title=Grails Application Forge
2424
projectDesc=Generates Grails applications
2525

2626
# for forge build process which is a micronaut 3.x.x app with picocli
27+
antVersion=1.10.15
2728
antlr4Version=4.8-1!!
2829
apacheRatVersion=0.8.1
2930
asciidoctorGradleJvmVersion=4.0.4

grails-forge/grails-cli/build.gradle

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
* limitations under the License.
1616
*/
1717
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
18-
import com.github.jengelman.gradle.plugins.shadow.transformers.ApacheLicenseResourceTransformer
19-
import com.github.jengelman.gradle.plugins.shadow.transformers.ApacheNoticeResourceTransformer
18+
import org.apache.grails.buildsrc.GrailsShadowLicenseTransform
19+
import org.apache.grails.buildsrc.GrailsShadowNoticeTransform
2020

2121
plugins {
2222
id 'groovy'
@@ -38,26 +38,17 @@ ext {
3838
version = projectVersion
3939
group = 'org.apache.grails'
4040

41+
evaluationDependsOn(':grails-forge-cli')
42+
evaluationDependsOn(':grails-shell-cli-shadow')
4143
dependencies {
4244
implementation platform("org.apache.grails:grails-bom:$projectVersion")
43-
44-
// shell-cli dependencies
45-
implementation 'org.apache.grails:grails-shell-cli'
46-
implementation 'org.springframework.boot:spring-boot-cli'
4745
implementation 'org.apache.groovy:groovy'
48-
implementation 'org.apache.groovy:groovy-ant'
49-
implementation 'org.apache.groovy:groovy-templates'
5046

51-
// The commands are pulled from the shellCliDependencies in the shell project, but they are not in the shell jar file,
52-
// so they must be added here. To keep the jar file smaller, only a subset is included here
53-
implementation 'org.apache.grails:grails-core' // for ApplicationContextCommandFactory to lookup commands
54-
implementation 'org.apache.grails:grails-url-mappings' // for UrlMappingsReportCommand
55-
// profiles will add additional libraries as needed to surface commands
47+
// shell-cli dependencies
48+
runtimeOnly files(project(':grails-shell-cli-shadow').tasks.named('shadowJar'))
5649

5750
// forge dependencies
58-
implementation project(':grails-forge-cli'), {
59-
exclude group: 'org.slf4j', module: 'slf4j-nop'
60-
}
51+
runtimeOnly files(project(':grails-forge-cli').tasks.named('shadowJar'))
6152
}
6253

6354
apply {
@@ -79,14 +70,29 @@ TaskProvider<Jar> shadowJarTask = tasks.named('shadowJar', ShadowJar)
7970
shadowJarTask.configure { ShadowJar it ->
8071
it.archiveClassifier.set('all')
8172

82-
// TODO: This needs reworked so we have one consistent license view, for now the disclaimer is shipped so this should suffice until this is fixed.
83-
it.transform(ApacheLicenseResourceTransformer)
84-
it.transform(ApacheNoticeResourceTransformer)
85-
it.exclude('DISCLAIMER', 'license.header', 'licenses/**', 'META-INF/NOTICE.md', 'META-INF/NOTICE', 'META-INF/NOTICE.md', 'META-INF/licenses/**', 'META-INF/LICENSE.md')
86-
8773
// services & custom grails.factories have to be merged since commands can be created from both sources
8874
it.mergeServiceFiles()
8975
it.mergeServiceFiles('META-INF/grails.factories')
76+
77+
// groovy extensions are shipped with grails-core
78+
it.mergeGroovyExtensionModules()
79+
80+
// combines all NOTICE files
81+
it.transform(GrailsShadowNoticeTransform) {
82+
it.projectName = 'Apache Grails'
83+
it.inceptionYear = '2005'
84+
}
85+
86+
// combines all LICENSE files
87+
it.transform(GrailsShadowLicenseTransform) {
88+
it.licenseText = rootProject.layout.projectDirectory.file('../licenses/LICENSE-Apache-2.0.txt').getAsFile().text
89+
}
90+
91+
it.exclude(
92+
'DISCLAIMER', // because we include our own jars, we need to exclude any files that we include
93+
'META-INF/DEPENDENCIES', // until we publish our own SBOM, this won't be correct so exclude
94+
'about.html' // restatement of the Eclipse Distribution License - Version 1.0 for jakarta
95+
)
9096
}
9197
// publish shadow jar separately from the regular library
9298
artifacts { archives shadowJarTask }

0 commit comments

Comments
 (0)