Skip to content

Commit a1b0a4f

Browse files
authored
[improve][build] Replace check-binary-license.sh with a Gradle task (apache#25673)
1 parent 663741e commit a1b0a4f

8 files changed

Lines changed: 232 additions & 102 deletions

File tree

.github/workflows/pulsar-ci.yaml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,7 @@ jobs:
176176
--no-configuration-cache
177177
178178
- name: Check binary licenses
179-
run: |
180-
src/check-binary-license.sh ./distribution/server/build/distributions/apache-pulsar-*-bin.tar.gz
179+
run: ./gradlew checkBinaryLicense --no-configuration-cache
181180

182181
- name: Upload Gradle reports
183182
uses: actions/upload-artifact@v4
@@ -657,11 +656,6 @@ jobs:
657656
- name: Build pulsar-test-latest-version Docker image
658657
run: ./gradlew :tests:latest-version-image:dockerBuild${{ env.CI_JDK_MAJOR_VERSION != '21' && format(' -PdockerJavaVersion={0}', env.CI_JDK_MAJOR_VERSION) || '' }}
659658

660-
- name: Check binary licenses
661-
run: |
662-
src/check-binary-license.sh ./distribution/server/build/distributions/apache-pulsar-*-bin.tar.gz
663-
src/check-binary-license.sh ./distribution/shell/build/distributions/apache-pulsar-shell-*-bin.tar.gz
664-
665659
- name: Run Trivy container scan
666660
id: trivy_scan
667661
uses: lhotari/sandboxed-trivy-action@555963036b2012b44c1071508a236e569db28ebb

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,12 @@ Check source code license headers and formatting:
200200
./gradlew rat spotlessCheck checkstyleMain checkstyleTest
201201
```
202202

203+
Check that bundled dependencies are properly recorded in the binary distribution `LICENSE` and `NOTICE` files. Run this after adding, removing, or upgrading a runtime dependency to confirm the corresponding entry has been added to (or removed from) the LICENSE file. The task builds the binary distribution tarballs as needed:
204+
205+
```bash
206+
./gradlew checkBinaryLicense
207+
```
208+
203209
Compile and assemble individual module:
204210

205211
```bash
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import org.gradle.api.DefaultTask
21+
import org.gradle.api.GradleException
22+
import org.gradle.api.file.ArchiveOperations
23+
import org.gradle.api.file.RegularFileProperty
24+
import org.gradle.api.tasks.CacheableTask
25+
import org.gradle.api.tasks.InputFile
26+
import org.gradle.api.tasks.OutputFile
27+
import org.gradle.api.tasks.PathSensitive
28+
import org.gradle.api.tasks.PathSensitivity
29+
import org.gradle.api.tasks.TaskAction
30+
import javax.inject.Inject
31+
32+
/**
33+
* Checks LICENSE/NOTICE coverage of bundled jars in a binary distribution tarball.
34+
*
35+
* Mirrors the behaviour of the legacy `src/check-binary-license.sh`:
36+
* 1. Every bundled jar whose basename does not contain "org.apache.pulsar"
37+
* must appear as a substring of the LICENSE text.
38+
* 2. Every jar referenced from LICENSE must be bundled.
39+
* 3. Every jar referenced from NOTICE (except "checker-qual.jar") must be bundled.
40+
*
41+
* Cacheable + configuration-cache friendly: state is held only on inputs/outputs and the
42+
* injected `ArchiveOperations` service; the task action does not reach into the project.
43+
*/
44+
@CacheableTask
45+
abstract class CheckBinaryLicenseTask : DefaultTask() {
46+
47+
@get:InputFile
48+
@get:PathSensitive(PathSensitivity.NONE)
49+
abstract val binaryDistribution: RegularFileProperty
50+
51+
@get:OutputFile
52+
abstract val report: RegularFileProperty
53+
54+
@get:Inject
55+
abstract val archiveOperations: ArchiveOperations
56+
57+
@TaskAction
58+
fun check() {
59+
val tarFile = binaryDistribution.get().asFile
60+
val tarTree = archiveOperations.tarTree(tarFile)
61+
62+
val licenseEntryRegex = Regex("^[^/]+/LICENSE$")
63+
val noticeEntryRegex = Regex("^[^/]+/NOTICE$")
64+
val nameExclusionSubstrings = listOf(
65+
"pulsar-client",
66+
"pulsar-cli-utils",
67+
"pulsar-common",
68+
"pulsar-package",
69+
"pulsar-websocket",
70+
"bouncy-castle-bc",
71+
)
72+
73+
val bundledJars = sortedSetOf<String>()
74+
var licenseContent: String? = null
75+
var noticeContent: String? = null
76+
77+
tarTree.visit {
78+
if (isDirectory) return@visit
79+
val path = relativePath.pathString
80+
when {
81+
path.endsWith(".jar") -> {
82+
val inExcludedDir = path.contains("/examples/") || path.contains("/instances/")
83+
val nameExcluded = nameExclusionSubstrings.any { name.contains(it) }
84+
if (!inExcludedDir && !nameExcluded) {
85+
bundledJars.add(name)
86+
}
87+
}
88+
licenseEntryRegex.matches(path) -> licenseContent = file.readText()
89+
noticeEntryRegex.matches(path) -> noticeContent = file.readText()
90+
}
91+
}
92+
93+
val license = licenseContent
94+
?: throw GradleException("Could not find a top-level LICENSE entry in ${tarFile.name}")
95+
val notice = noticeContent
96+
?: throw GradleException("Could not find a top-level NOTICE entry in ${tarFile.name}")
97+
98+
val licenseJars = extractJarReferences(license)
99+
val noticeJars = extractJarReferences(notice)
100+
101+
val errors = mutableListOf<String>()
102+
103+
// Check 1: every bundled non-pulsar jar must appear as a substring of LICENSE.
104+
for (jar in bundledJars) {
105+
if (jar.contains("org.apache.pulsar")) continue
106+
if (!license.contains(jar)) {
107+
errors.add("$jar unaccounted for in LICENSE")
108+
}
109+
}
110+
111+
// Check 2: every jar mentioned in LICENSE must be bundled.
112+
// Reference may contain wildcards like "org.rocksdb.*.jar"; treat it as a regex
113+
// to match the legacy bash `grep -q $J` semantics.
114+
for (jar in licenseJars) {
115+
val pattern = Regex(jar)
116+
if (bundledJars.none { pattern.containsMatchIn(it) }) {
117+
errors.add("$jar mentioned in LICENSE, but not bundled")
118+
}
119+
}
120+
121+
// Check 3: every jar mentioned in NOTICE (except checker-qual.jar) must be bundled.
122+
for (jar in noticeJars) {
123+
if (jar == "checker-qual.jar") continue
124+
val pattern = Regex(jar)
125+
if (bundledJars.none { pattern.containsMatchIn(it) }) {
126+
errors.add("$jar mentioned in NOTICE, but not bundled")
127+
}
128+
}
129+
130+
val reportFile = report.get().asFile
131+
reportFile.parentFile.mkdirs()
132+
reportFile.writeText(buildReport(tarFile.name, bundledJars, licenseJars, noticeJars, errors))
133+
134+
if (errors.isNotEmpty()) {
135+
errors.forEach { logger.error(it) }
136+
throw GradleException(
137+
"LICENSE/NOTICE check failed for ${tarFile.name}: ${errors.size} issue(s). " +
138+
"See report at ${reportFile.absolutePath}",
139+
)
140+
}
141+
}
142+
143+
private fun extractJarReferences(content: String): List<String> {
144+
val jarRegex = Regex(""".* (.*\.jar).*""")
145+
return content.lines().mapNotNull { line -> jarRegex.matchEntire(line)?.groupValues?.get(1) }
146+
}
147+
148+
private fun buildReport(
149+
tarballName: String,
150+
bundledJars: Set<String>,
151+
licenseJars: List<String>,
152+
noticeJars: List<String>,
153+
errors: List<String>,
154+
): String = buildString {
155+
appendLine("Binary license check report for $tarballName")
156+
appendLine("Bundled jars: ${bundledJars.size}")
157+
appendLine("Jars referenced in LICENSE: ${licenseJars.size}")
158+
appendLine("Jars referenced in NOTICE: ${noticeJars.size}")
159+
appendLine()
160+
if (errors.isEmpty()) {
161+
appendLine("Result: OK")
162+
} else {
163+
appendLine("Result: FAILED (${errors.size} issue(s))")
164+
errors.forEach { appendLine(" - $it") }
165+
}
166+
}
167+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
// Convention plugin: registers the `checkBinaryLicense` task for a distribution module.
21+
//
22+
// Consumers wire the produced tarball lazily, e.g.:
23+
// binaryLicenseCheck { archive.set(serverDistTar.flatMap { it.archiveFile }) }
24+
// The provider chain carries the task dependency on the producing tar task without
25+
// resolving it at configuration time, keeping configuration-cache and
26+
// configure-on-demand happy.
27+
28+
interface BinaryLicenseCheckExtension {
29+
val archive: org.gradle.api.file.RegularFileProperty
30+
}
31+
32+
val extension = extensions.create<BinaryLicenseCheckExtension>("binaryLicenseCheck")
33+
34+
tasks.register<CheckBinaryLicenseTask>("checkBinaryLicense") {
35+
group = "verification"
36+
description = "Check LICENSE/NOTICE coverage of bundled jars in the binary distribution tarball"
37+
binaryDistribution.set(extension.archive)
38+
report.set(layout.buildDirectory.file("reports/binary-license-check/result.txt"))
39+
}

build.gradle.kts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ tasks.register("serverDistTar") {
9494
dependsOn(":distribution:pulsar-server-distribution:serverDistTar")
9595
}
9696

97+
tasks.register("checkBinaryLicense") {
98+
group = "verification"
99+
description = "Check LICENSE/NOTICE coverage of bundled jars in all binary distributions"
100+
dependsOn(
101+
":distribution:pulsar-server-distribution:checkBinaryLicense",
102+
":distribution:pulsar-shell-distribution:checkBinaryLicense",
103+
)
104+
}
105+
97106
tasks.register("docker") {
98107
description = "Build the Pulsar Docker image"
99108
group = "docker"

distribution/server/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
plugins {
2121
id("pulsar.java-conventions")
22+
id("pulsar.binary-license-check-conventions")
2223
}
2324

2425
// Distribution module — no Java compilation needed
@@ -350,6 +351,10 @@ tasks.named("assemble") {
350351
dependsOn(serverDistTar)
351352
}
352353

354+
binaryLicenseCheck {
355+
archive.set(serverDistTar.flatMap { it.archiveFile })
356+
}
357+
353358
// Export the runtime classpath to a file for bin/ scripts to use
354359
// when running Pulsar from a development build (without lib/ directory)
355360
val exportClasspath by tasks.registering {

distribution/shell/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
plugins {
2121
id("pulsar.java-conventions")
22+
id("pulsar.binary-license-check-conventions")
2223
}
2324
// Distribution module — no Java compilation needed
2425
tasks.named("compileJava") { enabled = false }
@@ -213,6 +214,10 @@ tasks.named("assemble") {
213214
dependsOn(shellDistTar, shellDistZip)
214215
}
215216

217+
binaryLicenseCheck {
218+
archive.set(shellDistTar.flatMap { it.archiveFile })
219+
}
220+
216221
// Export the runtime classpath to a file for bin/ scripts to use
217222
// when running Pulsar CLI tools from a development build
218223
val exportClasspath by tasks.registering {

src/check-binary-license.sh

Lines changed: 0 additions & 95 deletions
This file was deleted.

0 commit comments

Comments
 (0)