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
-

Reply via email to