This is an automated email from the ASF dual-hosted git repository.
merlimat pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar.git
The following commit(s) were added to refs/heads/master by this push:
new a1b0a4fa15f [improve][build] Replace check-binary-license.sh with a
Gradle task (#25673)
a1b0a4fa15f is described below
commit a1b0a4fa15fc6620dabfbc8c83664556f8debd65
Author: Lari Hotari <[email protected]>
AuthorDate: Tue May 5 18:32:02 2026 +0300
[improve][build] Replace check-binary-license.sh with a Gradle task (#25673)
---
.github/workflows/pulsar-ci.yaml | 8 +-
README.md | 6 +
.../src/main/kotlin/CheckBinaryLicenseTask.kt | 167 +++++++++++++++++++++
...sar.binary-license-check-conventions.gradle.kts | 39 +++++
build.gradle.kts | 9 ++
distribution/server/build.gradle.kts | 5 +
distribution/shell/build.gradle.kts | 5 +
src/check-binary-license.sh | 95 ------------
8 files changed, 232 insertions(+), 102 deletions(-)
diff --git a/.github/workflows/pulsar-ci.yaml b/.github/workflows/pulsar-ci.yaml
index 39e675ba7e1..4de420d3d07 100644
--- a/.github/workflows/pulsar-ci.yaml
+++ b/.github/workflows/pulsar-ci.yaml
@@ -176,8 +176,7 @@ jobs:
--no-configuration-cache
- name: Check binary licenses
- run: |
- src/check-binary-license.sh
./distribution/server/build/distributions/apache-pulsar-*-bin.tar.gz
+ run: ./gradlew checkBinaryLicense --no-configuration-cache
- name: Upload Gradle reports
uses: actions/upload-artifact@v4
@@ -657,11 +656,6 @@ jobs:
- name: Build pulsar-test-latest-version Docker image
run: ./gradlew :tests:latest-version-image:dockerBuild${{
env.CI_JDK_MAJOR_VERSION != '21' && format(' -PdockerJavaVersion={0}',
env.CI_JDK_MAJOR_VERSION) || '' }}
- - name: Check binary licenses
- run: |
- src/check-binary-license.sh
./distribution/server/build/distributions/apache-pulsar-*-bin.tar.gz
- src/check-binary-license.sh
./distribution/shell/build/distributions/apache-pulsar-shell-*-bin.tar.gz
-
- name: Run Trivy container scan
id: trivy_scan
uses:
lhotari/sandboxed-trivy-action@555963036b2012b44c1071508a236e569db28ebb
diff --git a/README.md b/README.md
index c09d6f26acd..b729bf8135a 100644
--- a/README.md
+++ b/README.md
@@ -200,6 +200,12 @@ Check source code license headers and formatting:
./gradlew rat spotlessCheck checkstyleMain checkstyleTest
```
+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:
+
+```bash
+./gradlew checkBinaryLicense
+```
+
Compile and assemble individual module:
```bash
diff --git a/build-logic/conventions/src/main/kotlin/CheckBinaryLicenseTask.kt
b/build-logic/conventions/src/main/kotlin/CheckBinaryLicenseTask.kt
new file mode 100644
index 00000000000..4be5bd78759
--- /dev/null
+++ b/build-logic/conventions/src/main/kotlin/CheckBinaryLicenseTask.kt
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.file.ArchiveOperations
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import javax.inject.Inject
+
+/**
+ * Checks LICENSE/NOTICE coverage of bundled jars in a binary distribution
tarball.
+ *
+ * Mirrors the behaviour of the legacy `src/check-binary-license.sh`:
+ * 1. Every bundled jar whose basename does not contain "org.apache.pulsar"
+ * must appear as a substring of the LICENSE text.
+ * 2. Every jar referenced from LICENSE must be bundled.
+ * 3. Every jar referenced from NOTICE (except "checker-qual.jar") must be
bundled.
+ *
+ * Cacheable + configuration-cache friendly: state is held only on
inputs/outputs and the
+ * injected `ArchiveOperations` service; the task action does not reach into
the project.
+ */
+@CacheableTask
+abstract class CheckBinaryLicenseTask : DefaultTask() {
+
+ @get:InputFile
+ @get:PathSensitive(PathSensitivity.NONE)
+ abstract val binaryDistribution: RegularFileProperty
+
+ @get:OutputFile
+ abstract val report: RegularFileProperty
+
+ @get:Inject
+ abstract val archiveOperations: ArchiveOperations
+
+ @TaskAction
+ fun check() {
+ val tarFile = binaryDistribution.get().asFile
+ val tarTree = archiveOperations.tarTree(tarFile)
+
+ val licenseEntryRegex = Regex("^[^/]+/LICENSE$")
+ val noticeEntryRegex = Regex("^[^/]+/NOTICE$")
+ val nameExclusionSubstrings = listOf(
+ "pulsar-client",
+ "pulsar-cli-utils",
+ "pulsar-common",
+ "pulsar-package",
+ "pulsar-websocket",
+ "bouncy-castle-bc",
+ )
+
+ val bundledJars = sortedSetOf<String>()
+ var licenseContent: String? = null
+ var noticeContent: String? = null
+
+ tarTree.visit {
+ if (isDirectory) return@visit
+ val path = relativePath.pathString
+ when {
+ path.endsWith(".jar") -> {
+ val inExcludedDir = path.contains("/examples/") ||
path.contains("/instances/")
+ val nameExcluded = nameExclusionSubstrings.any {
name.contains(it) }
+ if (!inExcludedDir && !nameExcluded) {
+ bundledJars.add(name)
+ }
+ }
+ licenseEntryRegex.matches(path) -> licenseContent =
file.readText()
+ noticeEntryRegex.matches(path) -> noticeContent =
file.readText()
+ }
+ }
+
+ val license = licenseContent
+ ?: throw GradleException("Could not find a top-level LICENSE entry
in ${tarFile.name}")
+ val notice = noticeContent
+ ?: throw GradleException("Could not find a top-level NOTICE entry
in ${tarFile.name}")
+
+ val licenseJars = extractJarReferences(license)
+ val noticeJars = extractJarReferences(notice)
+
+ val errors = mutableListOf<String>()
+
+ // Check 1: every bundled non-pulsar jar must appear as a substring of
LICENSE.
+ for (jar in bundledJars) {
+ if (jar.contains("org.apache.pulsar")) continue
+ if (!license.contains(jar)) {
+ errors.add("$jar unaccounted for in LICENSE")
+ }
+ }
+
+ // Check 2: every jar mentioned in LICENSE must be bundled.
+ // Reference may contain wildcards like "org.rocksdb.*.jar"; treat it
as a regex
+ // to match the legacy bash `grep -q $J` semantics.
+ for (jar in licenseJars) {
+ val pattern = Regex(jar)
+ if (bundledJars.none { pattern.containsMatchIn(it) }) {
+ errors.add("$jar mentioned in LICENSE, but not bundled")
+ }
+ }
+
+ // Check 3: every jar mentioned in NOTICE (except checker-qual.jar)
must be bundled.
+ for (jar in noticeJars) {
+ if (jar == "checker-qual.jar") continue
+ val pattern = Regex(jar)
+ if (bundledJars.none { pattern.containsMatchIn(it) }) {
+ errors.add("$jar mentioned in NOTICE, but not bundled")
+ }
+ }
+
+ val reportFile = report.get().asFile
+ reportFile.parentFile.mkdirs()
+ reportFile.writeText(buildReport(tarFile.name, bundledJars,
licenseJars, noticeJars, errors))
+
+ if (errors.isNotEmpty()) {
+ errors.forEach { logger.error(it) }
+ throw GradleException(
+ "LICENSE/NOTICE check failed for ${tarFile.name}:
${errors.size} issue(s). " +
+ "See report at ${reportFile.absolutePath}",
+ )
+ }
+ }
+
+ private fun extractJarReferences(content: String): List<String> {
+ val jarRegex = Regex(""".* (.*\.jar).*""")
+ return content.lines().mapNotNull { line ->
jarRegex.matchEntire(line)?.groupValues?.get(1) }
+ }
+
+ private fun buildReport(
+ tarballName: String,
+ bundledJars: Set<String>,
+ licenseJars: List<String>,
+ noticeJars: List<String>,
+ errors: List<String>,
+ ): String = buildString {
+ appendLine("Binary license check report for $tarballName")
+ appendLine("Bundled jars: ${bundledJars.size}")
+ appendLine("Jars referenced in LICENSE: ${licenseJars.size}")
+ appendLine("Jars referenced in NOTICE: ${noticeJars.size}")
+ appendLine()
+ if (errors.isEmpty()) {
+ appendLine("Result: OK")
+ } else {
+ appendLine("Result: FAILED (${errors.size} issue(s))")
+ errors.forEach { appendLine(" - $it") }
+ }
+ }
+}
diff --git
a/build-logic/conventions/src/main/kotlin/pulsar.binary-license-check-conventions.gradle.kts
b/build-logic/conventions/src/main/kotlin/pulsar.binary-license-check-conventions.gradle.kts
new file mode 100644
index 00000000000..9e8e699c94e
--- /dev/null
+++
b/build-logic/conventions/src/main/kotlin/pulsar.binary-license-check-conventions.gradle.kts
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// Convention plugin: registers the `checkBinaryLicense` task for a
distribution module.
+//
+// Consumers wire the produced tarball lazily, e.g.:
+// binaryLicenseCheck { archive.set(serverDistTar.flatMap { it.archiveFile
}) }
+// The provider chain carries the task dependency on the producing tar task
without
+// resolving it at configuration time, keeping configuration-cache and
+// configure-on-demand happy.
+
+interface BinaryLicenseCheckExtension {
+ val archive: org.gradle.api.file.RegularFileProperty
+}
+
+val extension =
extensions.create<BinaryLicenseCheckExtension>("binaryLicenseCheck")
+
+tasks.register<CheckBinaryLicenseTask>("checkBinaryLicense") {
+ group = "verification"
+ description = "Check LICENSE/NOTICE coverage of bundled jars in the binary
distribution tarball"
+ binaryDistribution.set(extension.archive)
+
report.set(layout.buildDirectory.file("reports/binary-license-check/result.txt"))
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index 7b353a69d96..624e5259b11 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -94,6 +94,15 @@ tasks.register("serverDistTar") {
dependsOn(":distribution:pulsar-server-distribution:serverDistTar")
}
+tasks.register("checkBinaryLicense") {
+ group = "verification"
+ description = "Check LICENSE/NOTICE coverage of bundled jars in all binary
distributions"
+ dependsOn(
+ ":distribution:pulsar-server-distribution:checkBinaryLicense",
+ ":distribution:pulsar-shell-distribution:checkBinaryLicense",
+ )
+}
+
tasks.register("docker") {
description = "Build the Pulsar Docker image"
group = "docker"
diff --git a/distribution/server/build.gradle.kts
b/distribution/server/build.gradle.kts
index 5ca29d63819..1b2490b825d 100644
--- a/distribution/server/build.gradle.kts
+++ b/distribution/server/build.gradle.kts
@@ -19,6 +19,7 @@
plugins {
id("pulsar.java-conventions")
+ id("pulsar.binary-license-check-conventions")
}
// Distribution module — no Java compilation needed
@@ -350,6 +351,10 @@ tasks.named("assemble") {
dependsOn(serverDistTar)
}
+binaryLicenseCheck {
+ archive.set(serverDistTar.flatMap { it.archiveFile })
+}
+
// Export the runtime classpath to a file for bin/ scripts to use
// when running Pulsar from a development build (without lib/ directory)
val exportClasspath by tasks.registering {
diff --git a/distribution/shell/build.gradle.kts
b/distribution/shell/build.gradle.kts
index 3b99b5d7c97..6d21bae8d64 100644
--- a/distribution/shell/build.gradle.kts
+++ b/distribution/shell/build.gradle.kts
@@ -19,6 +19,7 @@
plugins {
id("pulsar.java-conventions")
+ id("pulsar.binary-license-check-conventions")
}
// Distribution module — no Java compilation needed
tasks.named("compileJava") { enabled = false }
@@ -213,6 +214,10 @@ tasks.named("assemble") {
dependsOn(shellDistTar, shellDistZip)
}
+binaryLicenseCheck {
+ archive.set(shellDistTar.flatMap { it.archiveFile })
+}
+
// Export the runtime classpath to a file for bin/ scripts to use
// when running Pulsar CLI tools from a development build
val exportClasspath by tasks.registering {
diff --git a/src/check-binary-license.sh b/src/check-binary-license.sh
deleted file mode 100755
index 6aec8b7cf1b..00000000000
--- a/src/check-binary-license.sh
+++ /dev/null
@@ -1,95 +0,0 @@
-#!/usr/bin/env bash
-#
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-#
-
-# Script to check licenses on a binary tarball.
-# It extracts the list of bundled jars, the NOTICE, and the LICENSE
-# files. It checked that every non-pulsar jar bundled is mentioned in the
-# LICENSE file. It checked that all jar files mentioned in NOTICE and
-# LICENSE are actually bundled.
-
-# all error fatal
-set -e
-
-TARBALL="$1"
-if [ -z $TARBALL ]; then
- echo "Usage: $0 <binary-tarball>"
- exit 1
-fi
-
-JARS=$(tar -tf $TARBALL | grep '\.jar' | grep -v '/examples/' | grep -v
'/instances/' | grep -v pulsar-client | grep -v pulsar-cli-utils | grep -v
pulsar-common | grep -v pulsar-package | grep -v pulsar-websocket | grep -v
bouncy-castle-bc | sed 's!.*/!!' | sort)
-
-LICENSEPATH=$(tar -tf $TARBALL | awk '/^[^\/]*\/LICENSE/')
-LICENSE=$(tar -O -xf $TARBALL "$LICENSEPATH")
-NOTICEPATH=$(tar -tf $TARBALL | awk '/^[^\/]*\/NOTICE/')
-NOTICE=$(tar -O -xf $TARBALL $NOTICEPATH)
-
-LICENSEJARS=$(echo "$LICENSE" | sed -nE 's!.* (.*\.jar).*!\1!gp')
-NOTICEJARS=$(echo "$NOTICE" | sed -nE 's!.* (.*\.jar).*!\1!gp')
-
-LINKEDINLICENSE=$(echo "$LICENSE" | sed -nE 's!.*(lib/[[:graph:]]*).*!\1!gp' |
sed 's!\.$!!')
-
-# errors not fatal
-set +e
-
-EXIT=0
-
-
-# Check all bundled jars are mentioned in LICENSE
-for J in $JARS; do
- echo $J | grep -q "org.apache.pulsar"
- if [ $? == 0 ]; then
- continue
- fi
-
- echo "$LICENSE" | grep -q $J
- if [ $? != 0 ]; then
- echo $J unaccounted for in LICENSE
- EXIT=1
- fi
-done
-
-# Check all jars mentioned in LICENSE are bundled
-for J in $LICENSEJARS; do
- echo "$JARS" | grep -q $J
- if [ $? != 0 ]; then
- echo $J mentioned in LICENSE, but not bundled
- EXIT=2
- fi
-done
-
-# Check all jars mentioned in NOTICE are bundled
-for J in $NOTICEJARS; do
- if [ $J == "checker-qual.jar" ]; then
- continue
- fi
- echo "$JARS" | grep -q $J
- if [ $? != 0 ]; then
- echo $J mentioned in NOTICE, but not bundled
- EXIT=3
- fi
-done
-
-if [ $EXIT != 0 ]; then
- echo
- echo It looks like there are issues with the LICENSE/NOTICE.
-fi
-
-exit $EXIT
-