Skip to content

Commit 41a3932

Browse files
authored
[improve][build] Add Maven publishing conventions for ASF release (#25457)
1 parent d96da97 commit 41a3932

66 files changed

Lines changed: 492 additions & 77 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bouncy-castle/bc/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*/
1919

2020
plugins {
21-
id("pulsar.java-conventions")
21+
id("pulsar.public-java-library-conventions")
2222
}
2323

2424
dependencies {

bouncy-castle/bcfips/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*/
1919

2020
plugins {
21-
id("pulsar.java-conventions")
21+
id("pulsar.public-java-library-conventions")
2222
}
2323

2424
dependencies {

build-logic/conventions/src/main/kotlin/pulsar.java-conventions.gradle.kts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ plugins {
2424

2525
val catalog = the<VersionCatalogsExtension>().named("libs")
2626

27-
group = "org.apache.pulsar"
28-
version = catalog.findVersion("pulsar").get().requiredVersion
29-
3027
tasks.withType<JavaCompile>().configureEach {
3128
options.encoding = "UTF-8"
3229
options.release.set(17)

build-logic/conventions/src/main/kotlin/pulsar.nar-conventions.gradle.kts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919

2020
// Convention plugin for NAR (Nifi Archive) modules.
2121
// Configures platform module exclusions from runtimeClasspath, forces JAR artifacts
22-
// for bundled-dependencies, and handles archive name qualification.
22+
// for bundled-dependencies, handles archive name qualification, and publishes the
23+
// NAR artifact with an empty POM (no dependencies — everything is bundled).
2324

2425
plugins {
2526
id("io.github.merlimat.nar")
27+
id("pulsar.publish-conventions")
2628
}
2729

2830
// NAR modules should not bundle Pulsar platform dependencies — they are provided
@@ -91,3 +93,26 @@ if (parentProject != null && parentProject != rootProject && parentProject.paren
9193
val narIdProp = narExt.javaClass.getMethod("getNarId").invoke(narExt) as org.gradle.api.provider.Property<String>
9294
narIdProp.set(qualifiedName)
9395
}
96+
97+
// --- NAR publishing: publish only the .nar artifact with an empty POM ---
98+
// NAR modules bundle all dependencies, so the POM should have no <dependencies> section.
99+
publishing {
100+
publications {
101+
named<MavenPublication>("maven") {
102+
// Replace component-based artifacts with just the NAR file
103+
artifacts.clear()
104+
artifact(tasks.named("nar"))
105+
pom {
106+
packaging = "nar"
107+
// Remove all dependencies — NAR bundles everything
108+
withXml {
109+
val root = asNode()
110+
root.children().removeAll { node ->
111+
val name = (node as groovy.util.Node).name()
112+
name.toString().contains("dependencies")
113+
}
114+
}
115+
}
116+
}
117+
}
118+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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 for public Pulsar Java libraries that are published to Maven repositories.
21+
// Combines java-conventions (compilation, testing) with publish-conventions (Maven publishing,
22+
// signing, POM metadata). Internal-only modules should use pulsar.java-conventions directly.
23+
24+
plugins {
25+
id("pulsar.java-conventions")
26+
id("pulsar.publish-conventions")
27+
}
28+
29+
// Validate that public java-library modules only depend on other published modules
30+
// in scopes that end up in the published POM (api, implementation, runtimeOnly).
31+
// Test/compileOnly scoped dependencies are excluded since they don't appear in the POM.
32+
// NAR modules are not validated here — they bundle all dependencies and have empty POMs.
33+
run {
34+
val publishedScopes = listOf("api", "implementation", "runtimeOnly")
35+
val configsToCheck = publishedScopes.mapNotNull { name ->
36+
configurations.findByName(name)?.let { name to it }
37+
}
38+
val currentProjectPath = project.path
39+
40+
val unpublishedDeps = provider {
41+
val errors = mutableListOf<String>()
42+
for ((configName, config) in configsToCheck) {
43+
for (dep in config.dependencies) {
44+
if (dep is ProjectDependency) {
45+
val depPath = dep.path
46+
val depProject = project.rootProject.project(depPath)
47+
if (!depProject.plugins.hasPlugin("maven-publish")) {
48+
errors.add(" - $configName -> $depPath (not published)")
49+
}
50+
}
51+
}
52+
}
53+
errors
54+
}
55+
56+
tasks.withType<PublishToMavenRepository>().configureEach {
57+
val errorList = unpublishedDeps
58+
doFirst {
59+
val errors = errorList.get()
60+
if (errors.isNotEmpty()) {
61+
throw GradleException(
62+
"Published module '$currentProjectPath' depends on unpublished projects:\n" +
63+
errors.joinToString("\n") + "\n" +
64+
"Either publish the dependency or move it to a test/compileOnly scope."
65+
)
66+
}
67+
}
68+
}
69+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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 for publishing Pulsar modules to Maven repositories.
21+
// Configures maven-publish, GPG signing, POM metadata, sources/javadoc JARs,
22+
// and a local deploy repository for testing.
23+
24+
plugins {
25+
`maven-publish`
26+
signing
27+
}
28+
29+
// --- java-library projects: JAR + sources + javadoc ---
30+
pluginManager.withPlugin("java-library") {
31+
val sourceSets = the<SourceSetContainer>()
32+
33+
// Match Maven's javadoc configuration: no doclint, don't fail on errors
34+
tasks.withType<Javadoc>().configureEach {
35+
(options as StandardJavadocDocletOptions).apply {
36+
addStringOption("Xdoclint:none", "-quiet")
37+
}
38+
isFailOnError = false
39+
}
40+
41+
val sourcesJar by tasks.registering(Jar::class) {
42+
archiveClassifier.set("sources")
43+
from(sourceSets["main"].allJava)
44+
}
45+
46+
val javadocJar by tasks.registering(Jar::class) {
47+
archiveClassifier.set("javadoc")
48+
from(tasks.named(JavaPlugin.JAVADOC_TASK_NAME))
49+
}
50+
51+
// Standard java-library modules: publish from components["java"]
52+
publishing {
53+
publications {
54+
create<MavenPublication>("maven") {
55+
from(components["java"])
56+
artifact(sourcesJar)
57+
artifact(javadocJar)
58+
59+
versionMapping {
60+
usage(Usage.JAVA_RUNTIME) {
61+
fromResolutionResult()
62+
}
63+
usage(Usage.JAVA_API) {
64+
fromResolutionOf("runtimeClasspath")
65+
}
66+
}
67+
}
68+
}
69+
}
70+
71+
}
72+
73+
// --- java-platform projects (BOM, dependencies): POM-only, no JAR ---
74+
pluginManager.withPlugin("java-platform") {
75+
publishing {
76+
publications {
77+
create<MavenPublication>("maven") {
78+
from(components["javaPlatform"])
79+
}
80+
}
81+
}
82+
}
83+
84+
// --- Common POM configuration for all publications ---
85+
run {
86+
// Capture values in a local scope so withXml closures don't capture the script object
87+
// (which would break configuration cache serialization)
88+
val projectName = project.name
89+
val projectDescription = project.description
90+
val archivesNameValue = the<BasePluginExtension>().archivesName.get()
91+
val isPlatformProject = plugins.hasPlugin("java-platform")
92+
val isRootProject = project == rootProject
93+
val pulsarVersion = version.toString()
94+
val localDeployRepoDir = rootProject.layout.buildDirectory.dir("local-deploy-repo")
95+
96+
publishing {
97+
publications {
98+
withType<MavenPublication>().configureEach {
99+
artifactId = archivesNameValue
100+
101+
pom {
102+
// Per-module name and description
103+
if (!isRootProject) {
104+
name.set("Apache Pulsar :: $projectName")
105+
description.set(projectDescription ?: "Apache Pulsar :: $projectName")
106+
}
107+
108+
// Clean up POM XML and inject <parent> reference
109+
withXml {
110+
val sb = asString()
111+
var s = sb.toString()
112+
// <scope>compile</scope> is the Maven default — remove for cleaner POM
113+
s = s.replace("<scope>compile</scope>", "")
114+
// Remove dependencyManagement from non-platform POMs
115+
// (platform POMs need it — their dependencies ARE the management section)
116+
if (!isPlatformProject) {
117+
s = s.replace(
118+
Regex(
119+
"<dependencyManagement>.*?</dependencyManagement>",
120+
RegexOption.DOT_MATCHES_ALL
121+
),
122+
""
123+
)
124+
}
125+
// Inject <parent> reference for child modules (not the root/parent POM itself).
126+
// Metadata (license, SCM, etc.) is inherited from the parent POM.
127+
if (!isRootProject) {
128+
s = s.replace(
129+
"<modelVersion>4.0.0</modelVersion>",
130+
"<modelVersion>4.0.0</modelVersion>\n <parent>\n" +
131+
" <groupId>org.apache.pulsar</groupId>\n" +
132+
" <artifactId>pulsar</artifactId>\n" +
133+
" <version>$pulsarVersion</version>\n" +
134+
" </parent>"
135+
)
136+
}
137+
sb.setLength(0)
138+
sb.append(s)
139+
// Re-format the XML
140+
asNode()
141+
}
142+
}
143+
}
144+
}
145+
146+
// Local Maven repository for testing/comparison
147+
repositories {
148+
maven {
149+
name = "localDeploy"
150+
url = uri(localDeployRepoDir)
151+
}
152+
}
153+
}
154+
}
155+
156+
// --- GPG signing ---
157+
signing {
158+
isRequired = !version.toString().endsWith("-SNAPSHOT")
159+
160+
val useGpgCmd = providers.gradleProperty("useGpgCmd").orNull?.toBoolean() ?: false
161+
if (useGpgCmd) {
162+
useGpgCmd()
163+
}
164+
165+
sign(publishing.publications)
166+
}
167+
168+
// Disable signing tasks when no key is configured (local dev without signing)
169+
tasks.withType<Sign>().configureEach {
170+
enabled = providers.gradleProperty("signing.keyId").isPresent ||
171+
providers.gradleProperty("signing.gnupg.keyName").isPresent
172+
}
173+
174+
// Suppress enforced-platform validation: all java-library modules use
175+
// enforcedPlatform(":pulsar-dependencies") for internal version alignment,
176+
// but this should not leak to consumers. The dependencyManagement section
177+
// is stripped from published POMs via withXml above.
178+
tasks.withType<GenerateModuleMetadata>().configureEach {
179+
suppressedValidationErrors.add("enforced-platform")
180+
}
181+

0 commit comments

Comments
 (0)