You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by lh...@apache.org on 2026/06/16 08:16:00 UTC
(pulsar) branch master updated: [fix][build] Fix the shaded client artifacts: jar contents and Maven publication (#26034)
This is an automated email from the ASF dual-hosted git repository.
lhotari 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 089990f9f5e [fix][build] Fix the shaded client artifacts: jar contents and Maven publication (#26034)
089990f9f5e is described below
commit 089990f9f5e92ee379781e8aad9966c16a81698b
Author: Lari Hotari <lh...@users.noreply.github.com>
AuthorDate: Tue Jun 16 11:15:53 2026 +0300
[fix][build] Fix the shaded client artifacts: jar contents and Maven publication (#26034)
---
.../pulsar.client-shade-conventions.gradle.kts | 114 +++++++++++++-
...lsar.public-java-library-conventions.gradle.kts | 2 +-
.../kotlin/pulsar.publish-conventions.gradle.kts | 161 +++-----------------
...sar.publish-repositories-conventions.gradle.kts | 169 +++++++++++++++++++++
build.gradle.kts | 28 +---
distribution/shell/src/assemble/LICENSE.bin.txt | 1 -
gradle/libs.versions.toml | 2 +-
microbench/build.gradle.kts | 4 +-
pulsar-client-admin-shaded/build.gradle.kts | 29 +++-
pulsar-client-all/build.gradle.kts | 24 +--
pulsar-client-shaded/build.gradle.kts | 42 +++--
pulsar-client/build.gradle.kts | 6 +-
pulsar-common/build.gradle.kts | 1 +
pulsar-functions/runtime-all/build.gradle.kts | 4 +-
14 files changed, 381 insertions(+), 206 deletions(-)
diff --git a/build-logic/conventions/src/main/kotlin/pulsar.client-shade-conventions.gradle.kts b/build-logic/conventions/src/main/kotlin/pulsar.client-shade-conventions.gradle.kts
index 60ed0cfa67c..608f93ce5ad 100644
--- a/build-logic/conventions/src/main/kotlin/pulsar.client-shade-conventions.gradle.kts
+++ b/build-logic/conventions/src/main/kotlin/pulsar.client-shade-conventions.gradle.kts
@@ -17,6 +17,15 @@
* under the License.
*/
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import org.gradle.api.attributes.Bundling
+import org.gradle.api.attributes.Category
+import org.gradle.api.attributes.LibraryElements
+import org.gradle.api.attributes.Usage
+import org.gradle.api.component.AdhocComponentWithVariants
+import org.gradle.api.tasks.PathSensitivity
+import java.util.zip.ZipFile
+
// Convention plugin for Pulsar client shaded modules (pulsar-client-shaded,
// pulsar-client-admin-shaded, pulsar-client-all). Configures the shadow jar
// with the shared dependency includes, file excludes, relocations, and
@@ -29,6 +38,40 @@ plugins {
val shadePrefix = "org.apache.pulsar.shade"
extra["shadePrefix"] = shadePrefix
+// ---- Published dependency scopes for non-bundled dependencies ----
+// The Shadow plugin publishes the `shadow` configuration's dependencies as the dependency-reduced
+// POM/Gradle Module Metadata of the shaded artifact, mapping ALL of them to Maven `runtime` scope
+// (a single java-runtime variant). That loses the api/implementation distinction the original
+// modules express, so consumers can't compile against the parts of the API that live in non-bundled
+// modules (e.g. pulsar-client-api).
+//
+// To restore control, modules can declare a dependency in the `shadowApi` configuration instead of
+// `shadow`. `shadowApi` deps are published with `compile` scope (a java-api variant added to the
+// shadow component), while `shadow` deps stay `runtime`. Both end up in the POM and GMM:
+// "shadowApi"(...) -> compile scope (consumer compile + runtime classpath)
+// "shadow"(...) -> runtime scope (consumer runtime classpath only)
+val shadowApi = configurations.dependencyScope("shadowApi") {
+ description = "Non-bundled dependencies published with compile (api) scope in the shaded " +
+ "artifact's dependency-reduced POM and Gradle Module Metadata."
+}
+val shadowApiElements = configurations.consumable("shadowApiElements") {
+ description = "API elements (compile scope) of the shaded artifact, mirroring shadowRuntimeElements."
+ extendsFrom(shadowApi.get())
+ attributes {
+ attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_API))
+ attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
+ attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR))
+ attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.SHADOWED))
+ }
+ // Carry the shaded jar so this variant is a complete API variant (like apiElements does for the
+ // standard java-library component).
+ outgoing.artifact(tasks.named("shadowJar"))
+}
+// Add the java-api variant to the shadow component so maven-publish emits the shadowApi deps with
+// compile scope. The shadow plugin already wires shadowRuntimeElements -> runtime scope.
+(components["shadow"] as AdhocComponentWithVariants)
+ .addVariantsFromConfiguration(shadowApiElements.get()) { mapToMavenScope("compile") }
+
tasks.named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
// ---- Dependency includes ----
@@ -171,10 +214,75 @@ tasks.named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJ
// ---- File content transformations ----
relocateAsyncHttpClientProperties(shadePrefix)
- // Relocate Netty native library filenames to avoid conflicts with unshaded Netty
+ // Relocate Netty native library filenames so the shaded Netty NativeLibraryLoader
+ // can find them (and to avoid conflicts with unshaded Netty). The loader prepends
+ // the shaded package prefix, with dots replaced by underscores, to the library
+ // name, so the prefix has to be inserted right before "netty" (after the "lib"
+ // prefix that .so/.jnilib files carry). For example
+ // META-INF/native/libnetty_transport_native_epoll_x86_64.so
+ // must be renamed to
+ // META-INF/native/liborg_apache_pulsar_shade_netty_transport_native_epoll_x86_64.so
+ val nettyNativePrefix = shadePrefix.replace('.', '_') + "_"
filesMatching("META-INF/native/**") {
- if (name.matches(Regex("netty.+\\.(so|jnilib|dll)"))) {
- path = path.replace(name, "org_apache_pulsar_shade_$name")
+ if (name.matches(Regex("(lib)?netty.+\\.(so|jnilib|dll)"))) {
+ path = path.replace(name, name.replaceFirst("netty", "${nettyNativePrefix}netty"))
+ }
+ }
+}
+
+// ---- Shaded jar content validation ----
+// Safety net: after the shaded jar is built, fail the build if it contains any file outside the
+// allowed package roots. This catches dependencies that ended up in the jar without being relocated
+// (e.g. leaked javax.* classes) or native libraries whose filenames were not rewritten. Directory
+// entries (the parent directories of the allowed roots) are ignored.
+val allowedShadedRoots = listOf(
+ "META-INF/",
+ "org/apache/pulsar/",
+ "com/scurrilous/circe/",
+ // Avro reflection annotations are intentionally excluded from the org.apache.avro relocation so
+ // that Avro recognizes them by their original name on user-supplied classes.
+ "org/apache/avro/reflect/",
+ // Bundled native libraries (libcirce-checksum.so, libcpu-affinity.so).
+ "lib/",
+)
+val verifyShadedJarContents = tasks.register("verifyShadedJarContents") {
+ description = "Fails the build if the shaded jar contains files outside the allowed package roots."
+ group = "verification"
+ val shadedJar = tasks.named<ShadowJar>("shadowJar").flatMap { it.archiveFile }
+ inputs.file(shadedJar).withPropertyName("shadedJar").withPathSensitivity(PathSensitivity.NONE)
+ inputs.property("allowedRoots", allowedShadedRoots)
+ val report = layout.buildDirectory.file("verification/verifyShadedJarContents.txt")
+ outputs.file(report).withPropertyName("report")
+ val allowedRoots = allowedShadedRoots
+ doLast {
+ val jar = shadedJar.get().asFile
+ val violations = mutableListOf<String>()
+ ZipFile(jar).use { zip ->
+ val entries = zip.entries()
+ while (entries.hasMoreElements()) {
+ val entry = entries.nextElement()
+ if (entry.isDirectory) continue
+ if (allowedRoots.none { entry.name.startsWith(it) }) {
+ violations.add(entry.name)
+ }
+ }
+ }
+ violations.sort()
+ if (violations.isNotEmpty()) {
+ throw GradleException(
+ "Shaded jar ${jar.name} contains ${violations.size} file(s) outside the allowed roots " +
+ "$allowedRoots:\n" + violations.joinToString("\n") { " $it" }
+ )
+ }
+ report.get().asFile.run {
+ parentFile.mkdirs()
+ writeText(
+ "OK: ${jar.name} contents are within the allowed roots:\n" +
+ allowedRoots.joinToString("\n") { " $it" } + "\n"
+ )
}
}
}
+tasks.named<ShadowJar>("shadowJar") {
+ finalizedBy(verifyShadedJarContents)
+}
diff --git a/build-logic/conventions/src/main/kotlin/pulsar.public-java-library-conventions.gradle.kts b/build-logic/conventions/src/main/kotlin/pulsar.public-java-library-conventions.gradle.kts
index 3bb57c41f91..5841e7d70ba 100644
--- a/build-logic/conventions/src/main/kotlin/pulsar.public-java-library-conventions.gradle.kts
+++ b/build-logic/conventions/src/main/kotlin/pulsar.public-java-library-conventions.gradle.kts
@@ -31,7 +31,7 @@ plugins {
// Test/compileOnly scoped dependencies are excluded since they don't appear in the POM.
// NAR modules are not validated here — they bundle all dependencies and have empty POMs.
run {
- val publishedScopes = listOf("api", "implementation", "runtimeOnly", "shadow")
+ val publishedScopes = listOf("api", "implementation", "runtimeOnly", "shadow", "shadowApi")
val configsToCheck = publishedScopes.mapNotNull { name ->
configurations.findByName(name)?.let { name to it }
}
diff --git a/build-logic/conventions/src/main/kotlin/pulsar.publish-conventions.gradle.kts b/build-logic/conventions/src/main/kotlin/pulsar.publish-conventions.gradle.kts
index 67436adb31a..a81b00d5792 100644
--- a/build-logic/conventions/src/main/kotlin/pulsar.publish-conventions.gradle.kts
+++ b/build-logic/conventions/src/main/kotlin/pulsar.publish-conventions.gradle.kts
@@ -17,17 +17,13 @@
* under the License.
*/
-// Convention plugin for publishing Pulsar modules to Maven repositories.
-// Configures maven-publish, GPG signing, POM metadata, sources/javadoc JARs,
-// the ASF Nexus release/snapshot repositories, and a local deploy repository
-// for testing.
-
-import org.gradle.api.services.BuildService
-import org.gradle.api.services.BuildServiceParameters
+// Convention plugin for the Maven PUBLICATION of each published Pulsar module: the software
+// component, POM metadata, sources/javadoc JARs, and the artifactId. The publish repositories
+// (local + ASF Nexus), GPG signing, the upload-serialization lock and the snapshot/release
+// validation are shared with the root parent-POM project via pulsar.publish-repositories-conventions.
plugins {
- `maven-publish`
- signing
+ id("pulsar.publish-repositories-conventions")
}
// --- java-library projects: JAR + sources + javadoc ---
@@ -108,11 +104,9 @@ run {
// Capture values in a local scope so withXml closures don't capture the script object
// (which would break configuration cache serialization)
val projectName = project.name
- val archivesNameValue = the<BasePluginExtension>().archivesName.get()
val isPlatformProject = plugins.hasPlugin("java-platform")
val isRootProject = project == rootProject
val pulsarVersion = version.toString()
- val localDeployRepoDir = rootProject.layout.buildDirectory.dir("local-deploy-repo")
// Per-module POM name and description. Read in afterEvaluate so that a description
// assigned in a module's build script body is picked up, and captured as plain strings
@@ -129,11 +123,21 @@ run {
}
}
+ // Set the published artifactId from archivesName. Deferred to afterEvaluate (per the Gradle
+ // maven-publish "deferred configuration" guidance) so a module can override archivesName in its
+ // build script body — e.g. the shaded client modules publish under their historical Maven
+ // artifactId ("pulsar-client", "pulsar-client-admin"). Read eagerly at plugin-application time
+ // the override would be missed. Captured as a plain string for configuration-cache safety.
+ afterEvaluate {
+ val archivesNameValue = the<BasePluginExtension>().archivesName.get()
+ publishing.publications.withType<MavenPublication>().configureEach {
+ artifactId = archivesNameValue
+ }
+ }
+
publishing {
publications {
withType<MavenPublication>().configureEach {
- artifactId = archivesNameValue
-
pom {
// Clean up POM XML and inject <parent> reference
withXml {
@@ -172,140 +176,9 @@ run {
}
}
}
-
- // Local Maven repository for testing/comparison
- repositories {
- maven {
- name = "localDeploy"
- url = uri(localDeployRepoDir)
- }
- }
- }
-}
-
-// --- Apache distribution repositories (ASF Nexus) ---
-// Repository names follow the ASF parent POM (apache.releases.https / apache.snapshots.https).
-// Publish with one of:
-// ./gradlew publishAllPublicationsToApacheSnapshotsRepository (for -SNAPSHOT versions)
-// ./gradlew publishAllPublicationsToApacheReleasesRepository (for release versions)
-// Uploads to the Apache staging repository are serialized by the mavenPublishLock shared build
-// service (defined below) so they all land in a single Nexus staging repository: Nexus creates an
-// implicit staging repository on first upload, and concurrent per-module uploads could otherwise be
-// split across multiple implicitly-created staging repositories. Because the lock handles this,
-// --no-parallel is not required.
-// Credentials are resolved by Gradle at execution time from the apacheReleasesUsername /
-// apacheReleasesPassword and apacheSnapshotsUsername / apacheSnapshotsPassword Gradle properties
-// (the credentials(PasswordCredentials::class) form, which keeps the publish tasks
-// configuration-cache compatible — explicitly assigned credentials would not be). Pass them as
-// ORG_GRADLE_PROJECT_-prefixed environment variables on the publish command line so the password
-// doesn't have to be stored in ~/.gradle/gradle.properties where it could leak to unrelated
-// builds; start the command line with a space to keep the password out of shell history:
-// ORG_GRADLE_PROJECT_apacheReleasesUsername=$APACHE_USER \
-// ORG_GRADLE_PROJECT_apacheReleasesPassword="<your ASF password>" \
-// ./gradlew publishAllPublicationsToApacheReleasesRepository ...
-// The URLs can be overridden with the apacheReleasesRepoUrl / apacheSnapshotsRepoUrl Gradle
-// properties (e.g. a file:// URL for testing the publication layout).
-run {
- fun MavenArtifactRepository.configureApacheRepository(urlProperty: String, defaultUrl: String) {
- val repositoryUrl = uri(providers.gradleProperty(urlProperty).getOrElse(defaultUrl))
- url = repositoryUrl
- // The file transport (an URL overridden to file:// for testing) rejects credentials,
- // and Gradle's credentials validation would fail when the properties aren't set.
- if (repositoryUrl.scheme != "file") {
- credentials(PasswordCredentials::class)
- }
- }
-
- publishing {
- repositories {
- maven {
- name = "apacheReleases"
- configureApacheRepository(
- "apacheReleasesRepoUrl",
- "https://repository.apache.org/service/local/staging/deploy/maven2"
- )
- }
- maven {
- name = "apacheSnapshots"
- configureApacheRepository(
- "apacheSnapshotsRepoUrl",
- "https://repository.apache.org/content/repositories/snapshots"
- )
- }
- }
- }
-
- // Validate before any upload: only -SNAPSHOT versions may go to apacheSnapshots and only
- // release versions to apacheReleases. (Maven's deploy picks the repository from the version;
- // in Gradle the task name picks the repository, so the version must be checked instead.)
- // The task's repository property is discarded by configuration cache serialization, so
- // capture the repository name at configuration time and only register the validation
- // action (capturing plain strings/booleans) for the Apache repositories.
- val projectVersion = version.toString()
- val isSnapshotVersion = projectVersion.endsWith("-SNAPSHOT")
- tasks.withType<PublishToMavenRepository>().configureEach {
- val repositoryName = repository.name
- if (repositoryName == "apacheReleases" || repositoryName == "apacheSnapshots") {
- doFirst {
- if (repositoryName == "apacheSnapshots" && !isSnapshotVersion) {
- throw GradleException(
- "Refusing to publish non-snapshot version '$projectVersion' to the " +
- "'apacheSnapshots' repository. Use " +
- "publishAllPublicationsToApacheReleasesRepository for release versions."
- )
- }
- if (repositoryName == "apacheReleases" && isSnapshotVersion) {
- throw GradleException(
- "Refusing to publish snapshot version '$projectVersion' to the " +
- "'apacheReleases' repository. Use " +
- "publishAllPublicationsToApacheSnapshotsRepository for -SNAPSHOT versions."
- )
- }
- }
- }
}
}
-// --- Serialize uploads to Maven repositories ---
-// Nexus creates an implicit staging repository on the first upload of a release, and concurrent
-// per-module uploads can end up split across multiple implicitly-created staging repositories
-// instead of being collected into a single one. A shared build service with maxParallelUsages = 1
-// ensures at most one PublishToMavenRepository upload runs at a time across the whole build, so the
-// rest of the build (compilation, jars, signing) can still run in parallel and --no-parallel is not
-// required.
-abstract class MavenPublishLock : BuildService<BuildServiceParameters.None>
-
-val mavenPublishLock = gradle.sharedServices.registerIfAbsent(
- "mavenPublishLock", MavenPublishLock::class
-) {
- maxParallelUsages = 1
-}
-
-tasks.withType<PublishToMavenRepository>().configureEach {
- usesService(mavenPublishLock)
-}
-
-// --- GPG signing ---
-signing {
- isRequired = !version.toString().endsWith("-SNAPSHOT")
-
- val useGpgCmd = providers.gradleProperty("useGpgCmd").orNull?.toBoolean() ?: false
- if (useGpgCmd) {
- useGpgCmd()
- }
-
- sign(publishing.publications)
-}
-
-// Disable signing tasks when no signing configuration is present (local dev without signing).
-// With -PuseGpgCmd=true, an explicit key isn't needed: the gpg command uses its default key
-// unless -Psigning.gnupg.keyName=<key id> selects one.
-tasks.withType<Sign>().configureEach {
- enabled = providers.gradleProperty("signing.keyId").isPresent ||
- providers.gradleProperty("signing.gnupg.keyName").isPresent ||
- (providers.gradleProperty("useGpgCmd").orNull?.toBoolean() ?: false)
-}
-
// NOTE: the enforced-platform validation error is intentionally NOT suppressed. The internal
// version-alignment platform (:pulsar-dependencies) is declared on the non-published
// `internalPlatform` bucket in pulsar.java-conventions (only the resolvable build classpaths
diff --git a/build-logic/conventions/src/main/kotlin/pulsar.publish-repositories-conventions.gradle.kts b/build-logic/conventions/src/main/kotlin/pulsar.publish-repositories-conventions.gradle.kts
new file mode 100644
index 00000000000..4fe98e0e1ad
--- /dev/null
+++ b/build-logic/conventions/src/main/kotlin/pulsar.publish-repositories-conventions.gradle.kts
@@ -0,0 +1,169 @@
+/*
+ * 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 for the Maven publishing TARGETS shared by every published Pulsar module:
+// the publish repositories (local + ASF Nexus), the snapshot/release validation, the upload
+// serialization lock, and GPG signing. It deliberately does NOT create any publication or touch the
+// `base`/`java` plugins, so it can be applied both to the root project (which publishes only the
+// POM-only `org.apache.pulsar:pulsar` parent artifact) and, via pulsar.publish-conventions, to every
+// java-library / java-platform module. Applying it everywhere keeps the parent POM and all modules
+// pointing at the same repositories and sharing the same upload lock, so a single `publish...` run
+// uploads the whole set into one Nexus staging repository.
+
+import org.gradle.api.services.BuildService
+import org.gradle.api.services.BuildServiceParameters
+
+plugins {
+ `maven-publish`
+ signing
+}
+
+// Local Maven repository for testing/comparison.
+publishing {
+ repositories {
+ maven {
+ name = "localDeploy"
+ url = uri(rootProject.layout.buildDirectory.dir("local-deploy-repo"))
+ }
+ }
+}
+
+// --- Apache distribution repositories (ASF Nexus) ---
+// Repository names follow the ASF parent POM (apache.releases.https / apache.snapshots.https).
+// Publish with one of:
+// ./gradlew publishAllPublicationsToApacheSnapshotsRepository (for -SNAPSHOT versions)
+// ./gradlew publishAllPublicationsToApacheReleasesRepository (for release versions)
+// Uploads to the Apache staging repository are serialized by the mavenPublishLock shared build
+// service (defined below) so they all land in a single Nexus staging repository: Nexus creates an
+// implicit staging repository on first upload, and concurrent per-module uploads could otherwise be
+// split across multiple implicitly-created staging repositories. Because the lock handles this,
+// --no-parallel is not required.
+// Credentials are resolved by Gradle at execution time from the apacheReleasesUsername /
+// apacheReleasesPassword and apacheSnapshotsUsername / apacheSnapshotsPassword Gradle properties
+// (the credentials(PasswordCredentials::class) form, which keeps the publish tasks
+// configuration-cache compatible — explicitly assigned credentials would not be). Pass them as
+// ORG_GRADLE_PROJECT_-prefixed environment variables on the publish command line so the password
+// doesn't have to be stored in ~/.gradle/gradle.properties where it could leak to unrelated
+// builds; start the command line with a space to keep the password out of shell history:
+// ORG_GRADLE_PROJECT_apacheReleasesUsername=$APACHE_USER \
+// ORG_GRADLE_PROJECT_apacheReleasesPassword="<your ASF password>" \
+// ./gradlew publishAllPublicationsToApacheReleasesRepository ...
+// The URLs can be overridden with the apacheReleasesRepoUrl / apacheSnapshotsRepoUrl Gradle
+// properties (e.g. a file:// URL for testing the publication layout).
+run {
+ fun MavenArtifactRepository.configureApacheRepository(urlProperty: String, defaultUrl: String) {
+ val repositoryUrl = uri(providers.gradleProperty(urlProperty).getOrElse(defaultUrl))
+ url = repositoryUrl
+ // The file transport (an URL overridden to file:// for testing) rejects credentials,
+ // and Gradle's credentials validation would fail when the properties aren't set.
+ if (repositoryUrl.scheme != "file") {
+ credentials(PasswordCredentials::class)
+ }
+ }
+
+ publishing {
+ repositories {
+ maven {
+ name = "apacheReleases"
+ configureApacheRepository(
+ "apacheReleasesRepoUrl",
+ "https://repository.apache.org/service/local/staging/deploy/maven2"
+ )
+ }
+ maven {
+ name = "apacheSnapshots"
+ configureApacheRepository(
+ "apacheSnapshotsRepoUrl",
+ "https://repository.apache.org/content/repositories/snapshots"
+ )
+ }
+ }
+ }
+
+ // Validate before any upload: only -SNAPSHOT versions may go to apacheSnapshots and only
+ // release versions to apacheReleases. (Maven's deploy picks the repository from the version;
+ // in Gradle the task name picks the repository, so the version must be checked instead.)
+ // Match on the task name (publish<Pub>PublicationTo<Repo>Repository) rather than reading
+ // task.repository, which can still be null while the task is being created (e.g. for the root
+ // project's POM-only publication). The captured booleans/strings keep this CC-compatible.
+ val projectVersion = version.toString()
+ val isSnapshotVersion = projectVersion.endsWith("-SNAPSHOT")
+ tasks.withType<PublishToMavenRepository>().configureEach {
+ val targetsApacheSnapshots = name.endsWith("ToApacheSnapshotsRepository")
+ val targetsApacheReleases = name.endsWith("ToApacheReleasesRepository")
+ if (targetsApacheSnapshots || targetsApacheReleases) {
+ doFirst {
+ if (targetsApacheSnapshots && !isSnapshotVersion) {
+ throw GradleException(
+ "Refusing to publish non-snapshot version '$projectVersion' to the " +
+ "'apacheSnapshots' repository. Use " +
+ "publishAllPublicationsToApacheReleasesRepository for release versions."
+ )
+ }
+ if (targetsApacheReleases && isSnapshotVersion) {
+ throw GradleException(
+ "Refusing to publish snapshot version '$projectVersion' to the " +
+ "'apacheReleases' repository. Use " +
+ "publishAllPublicationsToApacheSnapshotsRepository for -SNAPSHOT versions."
+ )
+ }
+ }
+ }
+ }
+}
+
+// --- Serialize uploads to Maven repositories ---
+// Nexus creates an implicit staging repository on the first upload of a release, and concurrent
+// per-module uploads can end up split across multiple implicitly-created staging repositories
+// instead of being collected into a single one. A shared build service with maxParallelUsages = 1
+// ensures at most one PublishToMavenRepository upload runs at a time across the whole build, so the
+// rest of the build (compilation, jars, signing) can still run in parallel and --no-parallel is not
+// required.
+abstract class MavenPublishLock : BuildService<BuildServiceParameters.None>
+
+val mavenPublishLock = gradle.sharedServices.registerIfAbsent(
+ "mavenPublishLock", MavenPublishLock::class
+) {
+ maxParallelUsages = 1
+}
+
+tasks.withType<PublishToMavenRepository>().configureEach {
+ usesService(mavenPublishLock)
+}
+
+// --- GPG signing ---
+signing {
+ isRequired = !version.toString().endsWith("-SNAPSHOT")
+
+ val useGpgCmd = providers.gradleProperty("useGpgCmd").orNull?.toBoolean() ?: false
+ if (useGpgCmd) {
+ useGpgCmd()
+ }
+
+ sign(publishing.publications)
+}
+
+// Disable signing tasks when no signing configuration is present (local dev without signing).
+// With -PuseGpgCmd=true, an explicit key isn't needed: the gpg command uses its default key
+// unless -Psigning.gnupg.keyName=<key id> selects one.
+tasks.withType<Sign>().configureEach {
+ enabled = providers.gradleProperty("signing.keyId").isPresent ||
+ providers.gradleProperty("signing.gnupg.keyName").isPresent ||
+ (providers.gradleProperty("useGpgCmd").orNull?.toBoolean() ?: false)
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index 624e5259b11..9f4c7770ce7 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -27,8 +27,9 @@ plugins {
alias(libs.plugins.crlf) apply false
alias(libs.plugins.idea.ext)
alias(libs.plugins.spotless) apply false // workaround for https://github.com/diffplug/spotless/issues/2877
- `maven-publish`
- signing
+ // Publish repositories (local + ASF Nexus), signing, upload lock and snapshot/release validation
+ // for the org.apache.pulsar:pulsar parent POM published below — shared with every module.
+ id("pulsar.publish-repositories-conventions")
}
versionCatalogUpdate {
@@ -48,8 +49,6 @@ tasks.named<com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask>("
}
}
-val pulsarVersion = version.toString()
-
// ── Apache RAT (Release Audit Tool) ─────────────────────────────────────────
tasks.named<org.nosphere.apache.rat.RatTask>("rat").configure {
// Honour .gitignore exclusions so RAT skips untracked/generated files.
@@ -170,25 +169,4 @@ publishing {
}
}
}
-
- repositories {
- maven {
- name = "localDeploy"
- url = uri(layout.buildDirectory.dir("local-deploy-repo"))
- }
- }
}
-
-signing {
- isRequired = !pulsarVersion.endsWith("-SNAPSHOT")
- val useGpgCmd = providers.gradleProperty("useGpgCmd").orNull?.toBoolean() ?: false
- if (useGpgCmd) {
- useGpgCmd()
- }
- sign(publishing.publications)
-}
-
-tasks.withType<Sign>().configureEach {
- enabled = providers.gradleProperty("signing.keyId").isPresent ||
- providers.gradleProperty("signing.gnupg.keyName").isPresent
-}
\ No newline at end of file
diff --git a/distribution/shell/src/assemble/LICENSE.bin.txt b/distribution/shell/src/assemble/LICENSE.bin.txt
index 115851264bf..bb8322148a1 100644
--- a/distribution/shell/src/assemble/LICENSE.bin.txt
+++ b/distribution/shell/src/assemble/LICENSE.bin.txt
@@ -432,7 +432,6 @@ The Apache Software License, Version 2.0
BSD 3-clause "New" or "Revised" License
* JLine3 -- jline-3.21.0.jar -- ../licenses/LICENSE-JLine.txt
- * JSR305 -- jsr305-3.0.2.jar -- ../licenses/LICENSE-JSR305.txt
MIT License
* SLF4J -- ../licenses/LICENSE-SLF4J.txt
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e0c801ed654..2d689c92282 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -175,7 +175,7 @@ thrift = "0.23.0"
datasketches-memory = "4.1.0"
datasketches-java = "7.0.1"
# Shading
-shadow = "9.4.1"
+shadow = "9.4.2"
[libraries]
# SLF4J
diff --git a/microbench/build.gradle.kts b/microbench/build.gradle.kts
index 9a71b6397b6..9cea5eb6bb8 100644
--- a/microbench/build.gradle.kts
+++ b/microbench/build.gradle.kts
@@ -19,7 +19,9 @@
plugins {
id("pulsar.java-conventions")
- alias(libs.plugins.shadow)
+ // No version: the Shadow plugin is already on the classpath via the build-logic conventions,
+ // so a versioned request cannot be reconciled with it.
+ id("com.gradleup.shadow")
}
dependencies {
diff --git a/pulsar-client-admin-shaded/build.gradle.kts b/pulsar-client-admin-shaded/build.gradle.kts
index 8fe0120d3a2..ce67ac210f5 100644
--- a/pulsar-client-admin-shaded/build.gradle.kts
+++ b/pulsar-client-admin-shaded/build.gradle.kts
@@ -22,22 +22,35 @@ plugins {
id("pulsar.client-shade-conventions")
}
+// Published under the historical Maven artifactId "pulsar-client-admin" (the shaded admin client).
+// archivesName drives the shaded jar base name, the published artifactId, and this project's module
+// identity (so the BOM and NAR exclusions reference "pulsar-client-admin").
+base {
+ archivesName.set("pulsar-client-admin")
+}
+
dependencies {
// Bundled into the shaded jar (kept on the runtime classpath so shadowJar packs them):
implementation(project(":pulsar-client-admin-original"))
implementation(project(":pulsar-client-messagecrypto-bc"))
- // Non-bundled runtime dependencies for the dependency-reduced published POM/GMM (the `shadow`
- // component). Logging is slf4j + slog only — never log4j.
- "shadow"(project(":pulsar-client-api"))
- "shadow"(project(":pulsar-client-admin-api"))
- "shadow"(libs.protobuf.java)
+ // Non-bundled dependencies for the dependency-reduced published POM/GMM. Logging is slf4j + slog
+ // only — never log4j. `shadowApi` -> compile scope, `shadow` -> runtime scope (see
+ // pulsar.client-shade-conventions). compile = exposed in the public API a consumer compiles
+ // against (the *-api modules, OpenTelemetry via ClientBuilder/Stats); runtime = used only
+ // internally by the bundled code (BouncyCastle, the unshaded Jackson annotations, the OTel
+ // incubator, slog/slf4j logging, jspecify CLASS-retention annotations).
+ //
+ // protobuf-java is intentionally NOT published (it was `provided` in the pre-Gradle Maven build):
+ // consumers using Schema.PROTOBUF / PROTOBUF_NATIVE must add protobuf-java themselves.
+ "shadowApi"(project(":pulsar-client-api"))
+ "shadowApi"(project(":pulsar-client-admin-api"))
+ "shadowApi"(libs.opentelemetry.api)
"shadow"(libs.jackson.annotations)
"shadow"(libs.bcprov.jdk18on)
"shadow"(libs.bcpkix.jdk18on)
- "shadow"(libs.opentelemetry.api)
"shadow"(libs.opentelemetry.api.incubator)
- "shadow"(libs.jspecify)
- "shadow"(libs.slf4j.api)
"shadow"(libs.slog)
+ "shadow"(libs.slf4j.api)
+ "shadow"(libs.jspecify)
}
diff --git a/pulsar-client-all/build.gradle.kts b/pulsar-client-all/build.gradle.kts
index 7c49a7d5465..c7a6d934624 100644
--- a/pulsar-client-all/build.gradle.kts
+++ b/pulsar-client-all/build.gradle.kts
@@ -28,20 +28,26 @@ dependencies {
implementation(project(":pulsar-client-admin-original"))
implementation(project(":pulsar-client-messagecrypto-bc"))
- // Non-bundled runtime dependencies for the dependency-reduced published POM/GMM (the `shadow`
- // component) — union of the client and admin shaded sets. Logging is slf4j + slog only — never
- // log4j (the log4j entries below are test-only and never reach the publication).
- "shadow"(project(":pulsar-client-api"))
- "shadow"(project(":pulsar-client-admin-api"))
- "shadow"(libs.protobuf.java)
+ // Non-bundled dependencies for the dependency-reduced published POM/GMM — union of the client
+ // and admin shaded sets. Logging is slf4j + slog only — never log4j (the log4j entries below are
+ // test-only and never reach the publication). `shadowApi` -> compile scope, `shadow` -> runtime
+ // scope (see pulsar.client-shade-conventions). compile = exposed in the public API a consumer
+ // compiles against (the *-api modules, OpenTelemetry via ClientBuilder/Stats); runtime = used
+ // only internally by the bundled code (BouncyCastle, the unshaded Jackson annotations, the OTel
+ // incubator, slog/slf4j logging, jspecify CLASS-retention annotations).
+ //
+ // protobuf-java is intentionally NOT published (it was `provided` in the pre-Gradle Maven build):
+ // consumers using Schema.PROTOBUF / PROTOBUF_NATIVE must add protobuf-java themselves.
+ "shadowApi"(project(":pulsar-client-api"))
+ "shadowApi"(project(":pulsar-client-admin-api"))
+ "shadowApi"(libs.opentelemetry.api)
"shadow"(libs.jackson.annotations)
"shadow"(libs.bcprov.jdk18on)
"shadow"(libs.bcpkix.jdk18on)
- "shadow"(libs.opentelemetry.api)
"shadow"(libs.opentelemetry.api.incubator)
- "shadow"(libs.jspecify)
- "shadow"(libs.slf4j.api)
"shadow"(libs.slog)
+ "shadow"(libs.slf4j.api)
+ "shadow"(libs.jspecify)
testImplementation(libs.log4j.api)
testImplementation(libs.log4j.core)
diff --git a/pulsar-client-shaded/build.gradle.kts b/pulsar-client-shaded/build.gradle.kts
index 654e50f7273..5967e502258 100644
--- a/pulsar-client-shaded/build.gradle.kts
+++ b/pulsar-client-shaded/build.gradle.kts
@@ -22,25 +22,45 @@ plugins {
id("pulsar.client-shade-conventions")
}
+// Published under the historical Maven artifactId "pulsar-client" (the shaded client). The Gradle
+// project is ":pulsar-client-shaded" because the "pulsar-client" directory holds pulsar-client-original.
+// archivesName drives the shaded jar base name, the published artifactId, and this project's module
+// identity (so the BOM and NAR exclusions reference "pulsar-client").
+base {
+ archivesName.set("pulsar-client")
+}
+
dependencies {
// Bundled into the shaded jar (kept on the runtime classpath so shadowJar packs them):
implementation(project(":pulsar-client-original"))
implementation(project(":pulsar-client-messagecrypto-bc"))
- // Non-bundled runtime dependencies. These are the ONLY entries in the dependency-reduced
- // published POM/Gradle Module Metadata (the `shadow` component). The bundled modules above and
- // their relocated third-party deps live inside the jar and must NOT appear as POM dependencies.
- // pulsar-client-api, protobuf, jackson-annotations, the BouncyCastle jars (signed, not
- // relocatable), OpenTelemetry and jspecify are deliberately not bundled. Logging is slf4j + slog
- // only — never log4j.
- "shadow"(project(":pulsar-client-api"))
- "shadow"(libs.protobuf.java)
+ // Non-bundled dependencies. These are the ONLY entries in the dependency-reduced published
+ // POM/Gradle Module Metadata. The bundled modules above and their relocated third-party deps
+ // live inside the jar and must NOT appear as POM dependencies. pulsar-client-api,
+ // jackson-annotations, the BouncyCastle jars (signed, not relocatable), OpenTelemetry and
+ // jspecify are deliberately not bundled. Logging is slf4j + slog only — never log4j.
+ //
+ // `shadowApi` -> compile scope, `shadow` -> runtime scope (see pulsar.client-shade-conventions).
+ // The split follows what the SHADED artifact's public API exposes vs. what is only used
+ // internally by the bundled code:
+ // - compile: types reachable from the public API a consumer compiles against —
+ // pulsar-client-api and io.opentelemetry:opentelemetry-api (ClientBuilder.openTelemetry / *Stats).
+ // - runtime: only used internally by the bundled implementation (BouncyCastle crypto, relocated
+ // Jackson's unshaded annotations, the OpenTelemetry incubator, slog/slf4j logging, jspecify
+ // CLASS-retention annotations) — never on a consumer's compile classpath.
+ //
+ // protobuf-java is intentionally NOT published (neither bundled nor declared as a dependency):
+ // the generated protobuf classes reference unshaded com.google.protobuf at runtime, but Pulsar
+ // does not force a protobuf version on consumers. Anyone using Schema.PROTOBUF / PROTOBUF_NATIVE
+ // must add protobuf-java themselves — matching the `provided` scope of the pre-Gradle Maven build.
+ "shadowApi"(project(":pulsar-client-api"))
+ "shadowApi"(libs.opentelemetry.api)
"shadow"(libs.jackson.annotations)
"shadow"(libs.bcprov.jdk18on)
"shadow"(libs.bcpkix.jdk18on)
- "shadow"(libs.opentelemetry.api)
"shadow"(libs.opentelemetry.api.incubator)
- "shadow"(libs.jspecify)
- "shadow"(libs.slf4j.api)
"shadow"(libs.slog)
+ "shadow"(libs.slf4j.api)
+ "shadow"(libs.jspecify)
}
diff --git a/pulsar-client/build.gradle.kts b/pulsar-client/build.gradle.kts
index c6c623a9b82..50a06a5a8dc 100644
--- a/pulsar-client/build.gradle.kts
+++ b/pulsar-client/build.gradle.kts
@@ -58,7 +58,6 @@ dependencies {
exclude(group = "com.google.protobuf", module = "protobuf-java")
}
implementation(libs.jackson.module.jsonSchema)
- implementation(libs.jsr305)
api(libs.jspecify)
implementation(libs.roaringbitmap)
@@ -66,6 +65,11 @@ dependencies {
compileOnly(libs.protobuf.java)
compileOnly(libs.joda.time)
compileOnly(libs.spotbugs.annotations)
+ // JSR-305 (javax.annotation.*) is a compile-time/static-analysis-only annotation library with no
+ // runtime use. Keep it off the runtime classpath so it is not bundled into the shaded client jar
+ // (Pulsar 5.0 drops javax.* from the runtime; JSpecify covers nullness). Consumers that want to run
+ // SpotBugs/FindBugs analysis can add jsr305 themselves.
+ compileOnly(libs.jsr305)
testImplementation(libs.jsonassert)
testImplementation(libs.awaitility)
diff --git a/pulsar-common/build.gradle.kts b/pulsar-common/build.gradle.kts
index e3b9595ab1f..ad7b37ca3f6 100644
--- a/pulsar-common/build.gradle.kts
+++ b/pulsar-common/build.gradle.kts
@@ -181,6 +181,7 @@ dependencies {
implementation(variantOf(libs.netty.tcnative.boringssl.static) { classifier("linux-aarch_64") })
implementation(variantOf(libs.netty.tcnative.boringssl.static) { classifier("osx-x86_64") })
implementation(variantOf(libs.netty.tcnative.boringssl.static) { classifier("osx-aarch_64") })
+ implementation(variantOf(libs.netty.tcnative.boringssl.static) { classifier("windows-x86_64") })
implementation(libs.netty.transport.classes.io.uring)
implementation(variantOf(libs.netty.transport.native.io.uring) { classifier("linux-x86_64") })
implementation(variantOf(libs.netty.transport.native.io.uring) { classifier("linux-aarch_64") })
diff --git a/pulsar-functions/runtime-all/build.gradle.kts b/pulsar-functions/runtime-all/build.gradle.kts
index 767f9f5eff3..5e56512904e 100644
--- a/pulsar-functions/runtime-all/build.gradle.kts
+++ b/pulsar-functions/runtime-all/build.gradle.kts
@@ -19,7 +19,9 @@
plugins {
id("pulsar.java-conventions")
- alias(libs.plugins.shadow)
+ // No version: the Shadow plugin is already on the classpath via the build-logic conventions,
+ // so a versioned request cannot be reconciled with it.
+ id("com.gradleup.shadow")
}
dependencies {