From 3b825faa8f9c7bdbccddce5ea4755198f7d96925 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Sat, 10 Aug 2024 16:30:44 +0200 Subject: [PATCH] Add Maven publication Adds publishing for Maven artifacts, including signing. Includes all prerequisites to publish Maven artifacts to Maven Central. The build-code parts have been taken from Nessie, including the necessary special treatment of shadow-jars and support to publish a bom. `./gradlew publishToMavenLocal` works out of the box. On top of the Nessie parts, this change can also build a source tarball from using `git archive`. Fully signed invocation example, assuming GPG agent (there are alternative ways to provide the GPG key+passphrase): ```bash ./ gradlew \ publishToMavenLocal \ sourceTarball \ -Prelease \ -PuseGpgAgent ``` --- .github/workflows/gradle.yml | 5 +- Dockerfile | 6 +- build-logic/build.gradle.kts | 2 + .../src/main/kotlin/polaris-java.gradle.kts | 8 + .../src/main/kotlin/polaris-root.gradle.kts | 13 + .../main/kotlin/polaris-shadow-jar.gradle.kts | 40 +++ .../main/kotlin/publishing/MemoizedGitInfo.kt | 85 +++++++ .../publishing/PublishingHelperExtension.kt | 60 +++++ .../publishing/PublishingHelperPlugin.kt | 147 +++++++++++ .../main/kotlin/publishing/configurePom.kt | 234 ++++++++++++++++++ .../src/main/kotlin/publishing/rootProject.kt | 198 +++++++++++++++ .../src/main/kotlin/publishing/shadowPub.kt | 125 ++++++++++ .../src/main/kotlin/publishing/util.kt | 109 ++++++++ build.gradle.kts | 28 +++ gradle.properties | 1 - gradle/baselibs.versions.toml | 2 + gradle/libs.versions.toml | 1 - polaris-core/build.gradle.kts | 51 ++-- polaris-service/build.gradle.kts | 80 +++--- settings.gradle.kts | 4 + version.txt | 1 + 21 files changed, 1135 insertions(+), 65 deletions(-) create mode 100644 build-logic/src/main/kotlin/polaris-shadow-jar.gradle.kts create mode 100644 build-logic/src/main/kotlin/publishing/MemoizedGitInfo.kt create mode 100644 build-logic/src/main/kotlin/publishing/PublishingHelperExtension.kt create mode 100644 build-logic/src/main/kotlin/publishing/PublishingHelperPlugin.kt create mode 100644 build-logic/src/main/kotlin/publishing/configurePom.kt create mode 100644 build-logic/src/main/kotlin/publishing/rootProject.kt create mode 100644 build-logic/src/main/kotlin/publishing/shadowPub.kt create mode 100644 build-logic/src/main/kotlin/publishing/util.kt create mode 100644 version.txt diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 97ce4e2d7..f7f35058c 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -58,7 +58,10 @@ jobs: - name: Check formatting run: ./gradlew check - + + - name: Check Maven publication + run: ./gradlew publishToMavenLocal + - name: Build with Gradle Wrapper run: ./gradlew test diff --git a/Dockerfile b/Dockerfile index 8fc73438c..0bc4f54b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,13 +31,13 @@ WORKDIR /app RUN rm -rf build # Build the rest catalog -RUN ./gradlew --no-daemon --info -PeclipseLink=$ECLIPSELINK clean shadowJar startScripts +RUN ./gradlew --no-daemon --info -PeclipseLink=$ECLIPSELINK clean prepareDockerDist FROM registry.access.redhat.com/ubi9/openjdk-21-runtime:1.20-2.1721752928 WORKDIR /app -COPY --from=build /app/polaris-service/build/libs/polaris-service-all.jar /app/lib/polaris-service-all.jar +COPY --from=build /app/polaris-service/build/docker-dist/bin /app/bin +COPY --from=build /app/polaris-service/build/docker-dist/lib /app/lib COPY --from=build /app/polaris-server.yml /app -COPY --from=build /app/polaris-service/build/scripts/polaris-service /app/bin/polaris-service EXPOSE 8181 diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index b61156e96..eade127b1 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -24,5 +24,7 @@ dependencies { implementation(baselibs.errorprone) implementation(baselibs.idea.ext) implementation(baselibs.license.report) + implementation(baselibs.nexus.publish) + implementation(baselibs.shadow) implementation(baselibs.spotless) } diff --git a/build-logic/src/main/kotlin/polaris-java.gradle.kts b/build-logic/src/main/kotlin/polaris-java.gradle.kts index 3690e1a95..7c5aebac5 100644 --- a/build-logic/src/main/kotlin/polaris-java.gradle.kts +++ b/build-logic/src/main/kotlin/polaris-java.gradle.kts @@ -21,6 +21,7 @@ import net.ltgt.gradle.errorprone.errorprone import org.gradle.api.tasks.compile.JavaCompile import org.gradle.api.tasks.testing.Test import org.gradle.kotlin.dsl.named +import publishing.PublishingHelperPlugin plugins { id("jacoco") @@ -30,6 +31,8 @@ plugins { id("net.ltgt.errorprone") } +apply() + tasks.withType(JavaCompile::class.java).configureEach { options.compilerArgs.addAll(listOf("-Xlint:unchecked", "-Xlint:deprecation")) options.errorprone.disableAllWarnings = true @@ -84,6 +87,11 @@ spotless { dependencies { errorprone(versionCatalogs.named("libs").findLibrary("errorprone").get()) } +java { + withJavadocJar() + withSourcesJar() +} + tasks.withType().configureEach { val opt = options as CoreJavadocOptions // don't spam log w/ "warning: no @param/@return" diff --git a/build-logic/src/main/kotlin/polaris-root.gradle.kts b/build-logic/src/main/kotlin/polaris-root.gradle.kts index ad0211c28..ab57fefea 100644 --- a/build-logic/src/main/kotlin/polaris-root.gradle.kts +++ b/build-logic/src/main/kotlin/polaris-root.gradle.kts @@ -20,12 +20,16 @@ import org.jetbrains.gradle.ext.copyright import org.jetbrains.gradle.ext.encodings import org.jetbrains.gradle.ext.settings +import publishing.PublishingHelperExtension +import publishing.PublishingHelperPlugin plugins { id("com.diffplug.spotless") id("org.jetbrains.gradle.plugin.idea-ext") } +apply() + spotless { kotlinGradle { ktfmt().googleStyle() @@ -57,3 +61,12 @@ if (System.getProperty("idea.sync.active").toBoolean()) { } } } + +extensions.getByType().apply { + asfProjectName = "polaris" + + mailingLists.addAll("dev", "issues", "commits") + + podlingPpmcAsfIds.addAll("anoop", "ashvin", "jackye", "russellspitzer", "snazy", "vvcephei") + podlingCommitterAsfIds.addAll() +} diff --git a/build-logic/src/main/kotlin/polaris-shadow-jar.gradle.kts b/build-logic/src/main/kotlin/polaris-shadow-jar.gradle.kts new file mode 100644 index 000000000..8ed23bee2 --- /dev/null +++ b/build-logic/src/main/kotlin/polaris-shadow-jar.gradle.kts @@ -0,0 +1,40 @@ +/* + * 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 com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { id("com.gradleup.shadow") } + +val shadowJar = tasks.named("shadowJar") + +shadowJar.configure { + outputs.cacheIf { false } // do not cache uber/shaded jars + archiveClassifier = "" + mergeServiceFiles() +} + +tasks.named("jar").configure { + dependsOn(shadowJar) + archiveClassifier = "raw" +} + +tasks.withType().configureEach { + exclude("META-INF/jandex.idx") + isZip64 = true +} diff --git a/build-logic/src/main/kotlin/publishing/MemoizedGitInfo.kt b/build-logic/src/main/kotlin/publishing/MemoizedGitInfo.kt new file mode 100644 index 000000000..df56de76b --- /dev/null +++ b/build-logic/src/main/kotlin/publishing/MemoizedGitInfo.kt @@ -0,0 +1,85 @@ +/* + * 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. + */ + +package publishing + +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.java.archives.Attributes +import org.gradle.kotlin.dsl.extra +import java.io.ByteArrayOutputStream +import java.nio.charset.StandardCharsets + +/** + * Container to memoize Git information retrieved via `git` command executions across all Gradle projects. + * Jar release artifacts get some attributes added to the jar manifest, which can be quite useful for released jars. + */ +internal class MemoizedGitInfo { + companion object { + private fun execProc(rootProject: Project, cmd: String, vararg args: Any): String { + val buf = ByteArrayOutputStream() + rootProject.exec { + executable = cmd + args(args.toList()) + standardOutput = buf + } + return buf.toString(StandardCharsets.UTF_8).trim() + } + + fun gitInfo(rootProject: Project, attribs: Attributes) { + val props = gitInfo(rootProject) + attribs.putAll(props) + } + + fun gitInfo(rootProject: Project): Map { + return if (rootProject.extra.has("gitReleaseInfo")) { + @Suppress("UNCHECKED_CAST") + rootProject.extra["gitReleaseInfo"] as Map + } else { + val isRelease = rootProject.hasProperty("release") + val gitHead = execProc(rootProject, "git", "rev-parse", "HEAD") + val gitDescribe = if (isRelease) { + try { + execProc(rootProject, "git", "describe", "--tags") + } catch (e: Exception) { + throw GradleException("'git describe --tags' failed - no Git tag?", e) + } + } else { + execProc(rootProject, "git", "describe", "--always", "--dirty") + } + val timestamp = execProc(rootProject, "date", "+%Y-%m-%d-%H:%M:%S%:z") + val system = execProc(rootProject, "uname", "-a") + val javaVersion = System.getProperty("java.version") + + val info = + mapOf( + "Apache-Polaris-Version" to rootProject.version.toString(), + "Apache-Polaris-Is-Release" to isRelease.toString(), + "Apache-Polaris-Build-Git-Head" to gitHead, + "Apache-Polaris-Build-Git-Describe" to gitDescribe, + "Apache-Polaris-Build-Timestamp" to timestamp, + "Apache-Polaris-Build-System" to system, + "Apache-Polaris-Build-Java-Version" to javaVersion + ) + rootProject.extra["gitReleaseInfo"] = info + return info + } + } + } +} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/publishing/PublishingHelperExtension.kt b/build-logic/src/main/kotlin/publishing/PublishingHelperExtension.kt new file mode 100644 index 000000000..aae156af7 --- /dev/null +++ b/build-logic/src/main/kotlin/publishing/PublishingHelperExtension.kt @@ -0,0 +1,60 @@ +/* + * 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. + */ + +package publishing + +import org.gradle.api.Project +import org.gradle.api.model.ObjectFactory +import org.gradle.kotlin.dsl.listProperty +import org.gradle.kotlin.dsl.property +import java.io.File +import javax.inject.Inject + +/** + * Gradle plugin extension object for the `PublishingHelperPlugin. Most attributes are likely never changed from the + * default values. + * + * Apache podlings need to specify the PPMC members and committers manually, Apache TLPs don't populate these + * properties. + */ +abstract class PublishingHelperExtension +@Inject +constructor(objectFactory: ObjectFactory, project: Project) +{ + // optional customization of the pom.xml element + val mavenName = objectFactory.property().convention(project.provider { project.name }) + + val licenseUrl = objectFactory.property().convention("https://www.apache.org/licenses/LICENSE-2.0.txt") + + // the following are only relevant on the root project + val asfProjectName = objectFactory.property() + val baseName = objectFactory.property().convention(project.provider { "apache-${asfProjectName.get()}-${project.version}" }) + val distributionDir = objectFactory.directoryProperty().convention(project.layout.buildDirectory.dir("distributions")) + val sourceTarball = objectFactory.fileProperty().convention(project.provider { distributionDir.get().file("${baseName.get()}.tar.gz") }) + val sourceTarballDigest = objectFactory.fileProperty().convention(project.provider { distributionDir.get().file("${baseName.get()}.sha512") }) + + val mailingLists = objectFactory.listProperty(String::class.java).convention(emptyList()) + + // override the list of developers (P)PMC members + committers, necessary for podlings + val podlingPpmcAsfIds = objectFactory.setProperty(String::class.java).convention(emptySet()) + val podlingCommitterAsfIds = objectFactory.setProperty(String::class.java).convention(emptySet()) + + fun distributionFile(ext: String): File = + distributionDir.get().file("${baseName.get()}.$ext").asFile +} diff --git a/build-logic/src/main/kotlin/publishing/PublishingHelperPlugin.kt b/build-logic/src/main/kotlin/publishing/PublishingHelperPlugin.kt new file mode 100644 index 000000000..5dbae0a5b --- /dev/null +++ b/build-logic/src/main/kotlin/publishing/PublishingHelperPlugin.kt @@ -0,0 +1,147 @@ +/* + * 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. + */ + +package publishing + +import com.github.jengelman.gradle.plugins.shadow.ShadowExtension +import org.gradle.api.* +import javax.inject.Inject +import org.gradle.api.component.SoftwareComponentFactory +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.publish.maven.plugins.MavenPublishPlugin +import org.gradle.api.publish.tasks.GenerateModuleMetadata +import org.gradle.kotlin.dsl.* +import org.gradle.plugins.signing.SigningExtension +import org.gradle.plugins.signing.SigningPlugin +import org.gradle.jvm.tasks.Jar + +/** Release-publishing helper plugin to generate publications that pass Sonatype validations, + * generate Apache release source tarball. + * + * The `release` Gradle project property triggers: signed artifacts + jars with Git information. + * The current Git HEAD must point to a Git tag. + * + * The `jarWithGitInfo` Gradle project property triggers: jars with Git information (not necessary + * with `release`). + * + * The task `sourceTarball` (available on the root project) generates a source tarball using + * `git archive`. + * + * The task `releaseEmailTemplate` generates the release-vote email subject + body. Outputs on + * the console and in the `build/distributions/` directory. + * + * Signing tip: If you want to use `gpg-agent`, set the `useGpgAgent` Gradle project property + * + * The following command publishes the project artifacts to your local maven repository, + * generates the source tarball - and uses `gpg-agent` to sign all artifacts and the tarball. + * Note that this requires a Git tag! + * ``` + * ./gradlew publishToMavenLocal sourceTarball -Prelease -PuseGpgAgent + * ``` + * + * You can generate signed artifacts when using the `signArtifacts` project property: + * ``` + * ./gradlew publishToMavenLocal sourceTarball -PsignArtifacts -PuseGpgAgent + * ``` + */ +@Suppress("unused") +class PublishingHelperPlugin +@Inject +constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Plugin { + override fun apply(project: Project): Unit = + project.run { + extensions.create("publishingHelper", PublishingHelperExtension::class.java) + + val isRelease = project.hasProperty("release") + val isSigning = isRelease || project.hasProperty("signArtifacts") + + // Adds Git/Build/System related information to the generated jars, if the `release` project + // property is present. Do not add that information in development builds, so that the + // generated jars are still cacheable for Gradle. + if (isRelease || project.hasProperty("jarWithGitInfo")) { + // Runs `git`, considered expensive, so guarded behind project properties. + tasks.withType().configureEach { + manifest { MemoizedGitInfo.gitInfo(rootProject, attributes) } + } + } + + apply(plugin = "maven-publish") + apply(plugin = "signing") + + // Generate a source tarball for a release to be uploaded to + // https://dist.apache.org/repos/dist/dev//apache--/ + if (project == rootProject) { + configureOnRootProject(project) + } + + if (isSigning) { + plugins.withType().configureEach { + configure { + val signingKey: String? by project + val signingPassword: String? by project + useInMemoryPgpKeys(signingKey, signingPassword) + val publishing = project.extensions.getByType(PublishingExtension::class.java) + afterEvaluate { sign(publishing.publications.getByName("maven")) } + + if (project.hasProperty("useGpgAgent")) { + useGpgCmd() + } + } + } + } + + // Gradle complains when a Gradle module metadata ("pom on steroids") is generated with an + // enforcedPlatform() dependency - but Quarkus requires enforcedPlatform(), so we have to + // allow it. + tasks.withType().configureEach { + suppressedValidationErrors.add("enforced-platform") + } + + plugins.withType().configureEach { + configure { + publications { + register("maven") { + val mavenPublication = this + afterEvaluate { + // This MUST happen in an 'afterEvaluate' to ensure that the Shadow*Plugin has + // been applied. + if (project.extensions.findByType(ShadowExtension::class.java) != null) { + configureShadowPublishing(project, mavenPublication, softwareComponentFactory) + } else { + from(components.firstOrNull { c -> c.name == "javaPlatform" || c.name == "java" }) + } + + suppressPomMetadataWarningsFor("testFixturesApiElements") + suppressPomMetadataWarningsFor("testFixturesRuntimeElements") + + mavenPublication.groupId = "$group" + mavenPublication.version = project.version.toString() + } + + tasks.named("generatePomFileForMavenPublication").configure { + configurePom(project, mavenPublication, this) + } + } + } + } + } + } +} + diff --git a/build-logic/src/main/kotlin/publishing/configurePom.kt b/build-logic/src/main/kotlin/publishing/configurePom.kt new file mode 100644 index 000000000..ab6c4cad2 --- /dev/null +++ b/build-logic/src/main/kotlin/publishing/configurePom.kt @@ -0,0 +1,234 @@ +/* + * 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. + */ + +package publishing + +import groovy.util.Node +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.artifacts.component.ModuleComponentSelector +import org.gradle.api.publish.maven.MavenPom +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.internal.extensions.stdlib.capitalized + +/** + * Configures the content of the generated `pom.xml` files. + * + * For all projects except the root project, the pom gets the ``, ``, ``, `` + * elements and fixes dependencies in `` to be consumable by Maven. + * + * The root project generates the parent pom, containing all the necessary elements to pass Sonatype validation + * and some more information like `` and ``. Most of the information is taken from publicly + * consumable Apache project information from `https://projects.apache.org/json/projects/>.json`. + * `` contains all (P)PMC members and committers from that project info JSON, ordered by real name. + * `` is taken from GitHub's `https://api.github.com/repos/apache//contributors?per_page=1000` + * endpoint to give all contributors credit, ordered by number of contributions (as returned by that endpoint). + */ +internal fun configurePom(project: Project, mavenPublication: MavenPublication, task: Task) = mavenPublication.run { + val e = project.extensions.getByType(PublishingHelperExtension::class.java) + + pom { + if (project != project.rootProject) { + name.set(e.mavenName.get()) + description.set(project.description) + + // Add the license to every pom to make it easier for downstream project to retrieve the license. + licenses { + license { + name.set("Apache-2.0") // SPDX identifier + url.set(e.licenseUrl.get()) + } + } + + withXml { + val projectNode = asNode() + + val parentNode = projectNode.appendNode("parent") + val parent = project.parent!! + parentNode.appendNode("groupId", parent.group) + parentNode.appendNode("artifactId", parent.name) + parentNode.appendNode("version", parent.version) + + addMissingMandatoryDependencyVersions(project, projectNode) + } + } else { + val mavenPom = this + + task.doFirst { + val asfName = e.asfProjectName.get() + val (asfPrj, fromPodlings) = fetchAsfProject(asfName) + + val asfProjectName = asfPrj["name"] as String + + mavenPom.name.set(asfProjectName) + mavenPom.description.set(asfPrj["description"] as String) + + inceptionYear.set((asfPrj["created"] ?: asfPrj["started"]!!).toString().replace("(\\d+)-.*", "\\1")) + url.set(asfPrj["homepage"] as String) + organization { + name.set("The Apache Software Foundation") + url.set("https://www.apache.org/") + } + licenses { + license { + name.set("Apache-2.0") // SPDX identifier + url.set(e.licenseUrl.get()) + } + } + mailingLists { + e.mailingLists.get().forEach { ml -> + mailingList { + name.set("${ml.capitalized()} Mailing List") + subscribe.set("$ml-subscribe@$asfName.apache.org") + unsubscribe.set("$ml-ubsubscribe@$asfName.apache.org") + post.set("$ml@$asfName.apache.org") + archive.set("https://lists.apache.org/list.html?$ml@$asfName.apache.org") + } + } + } + scm { + val codeRepo: String = if (asfPrj.contains("repository")) { + val repos: List = unsafeCast(asfPrj["repository"]) as List + repos[0] + } else { + "https://github.com/apache/$asfName.git" + } + connection.set("scm:git:$codeRepo") + developerConnection.set("scm:git:$codeRepo") + url.set("$codeRepo/tree/main") + tag.set("main") + } + issueManagement { + url.set(if (asfPrj.contains("repository")) { + asfPrj["bug-database"] as String + } else { + "https://github.com/apache/$asfName/issues" + }) + } + addDevelopersToPom(mavenPom, asfName, e, fromPodlings) + addContributorsToPom(mavenPom, asfName, asfProjectName) + } + } + } +} + +/** + * Adds contributors as returned by GitHub, in descending `contributions` order. + */ +fun addContributorsToPom(mavenPom: MavenPom, asfName: String, asfProjectName: String) = mavenPom.run { + contributors { + val contributors: List>? = + parseJson("https://api.github.com/repos/apache/$asfName/contributors?per_page=1000") + if (contributors != null) { + contributors.filter { contributor -> + contributor["type"] == "User" + }.forEach { contributor -> + contributor { + name.set(contributor["login"] as String) + url.set(contributor["url"] as String) + organization.set("$asfProjectName, GitHub contributors") + organizationUrl.set("https://github.com/apache/$asfName") + } + } + } + } +} + +/** + * Adds Apache (P)PMC members + committers, in `name` order. + */ +fun addDevelopersToPom(mavenPom: MavenPom, asfName: String, e: PublishingHelperExtension, fromPodlings: Boolean) = mavenPom.run { + developers { + // Cannot use check the 'groups' for podlings, because the (only) podling's group + // references the mentors, not the PPMC members/committers. There seems to be no + // way to automatically fetch the committers + PPMC members for a podling, except + // maybe + val people: Map> = + parseJson("https://projects.apache.org/json/foundation/people.json")!! + val filteredPeople: List>> + val pmc: (Pair>) -> Boolean + val pmcRole: String + if (!fromPodlings) { + val asfNamePmc = "$asfName-pmc" + filteredPeople = people.filter { entry -> + val groups: List = unsafeCast(entry.value["groups"]) + groups.any { it == asfName || it == asfNamePmc } + }.toList() + pmc = {(_, info) -> + val groups: List = unsafeCast(info["groups"]) + groups.contains(asfNamePmc) + } + pmcRole = "PMC Member" + } else { + val podlingPpmc = e.podlingPpmcAsfIds.get() + filteredPeople = (e.podlingCommitterAsfIds.get() + podlingPpmc).map { id -> + val info = people[id] + if (info == null) { + throw GradleException("Person with ASF id '%id' not found in people.json".format(id)) + } + Pair(id, info) + } + pmc = {(id, _) -> podlingPpmc.contains(id)} + pmcRole = "PPMC Member" + } + + val sortedPeople = filteredPeople.sortedBy { (id, info) -> "${info["name"] as String}_$id" } + + sortedPeople.forEach { (id, info) -> + developer { + this.id.set(id) + this.name.set(info["name"] as String) + this.organization.set("Apache Software Foundation") + this.email.set("$id@apache.org") + this.roles.add("Committer") + if (pmc.invoke(Pair(id, info))) { + this.roles.add(pmcRole) + } + } + } + } +} + +/** + * Scans the generated `pom.xml` for `` in `` that do not have a + * `` and adds one, if possible. Maven kinda requires `` tags there, even if the + * `` without a `` is a bom and that bom's version is available transitively. + */ +fun addMissingMandatoryDependencyVersions(project: Project, projectNode: Node) { + xmlNode(xmlNode(projectNode, "dependencyManagement"), "dependencies")?.children()?.forEach { + val dependency = it as Node + if (xmlNode(dependency, "version") == null) { + val depGroup = xmlNode(dependency, "groupId")!!.text() + val depName = xmlNode(dependency, "artifactId")!!.text() + + var depResult = + findDependency(project.configurations.findByName("runtimeClasspath"), depGroup, depName) + if (depResult == null) { + depResult = + findDependency(project.configurations.findByName("testRuntimeClasspath"), depGroup, depName) + } + + if (depResult != null) { + val req = depResult.requested as ModuleComponentSelector + dependency.appendNode("version", req.version) + } + } + } +} diff --git a/build-logic/src/main/kotlin/publishing/rootProject.kt b/build-logic/src/main/kotlin/publishing/rootProject.kt new file mode 100644 index 000000000..3d14a1cbf --- /dev/null +++ b/build-logic/src/main/kotlin/publishing/rootProject.kt @@ -0,0 +1,198 @@ +/* + * 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. + */ + +package publishing + +import io.github.gradlenexus.publishplugin.NexusPublishExtension +import io.github.gradlenexus.publishplugin.NexusPublishPlugin +import io.github.gradlenexus.publishplugin.internal.StagingRepositoryDescriptorRegistryBuildService +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.services.BuildServiceRegistration +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.named +import org.gradle.kotlin.dsl.register +import org.gradle.plugins.signing.Sign + +/** + * Configures Apache project specific publishing tasks on the root project, for example the source-tarball related tasks. + */ +internal fun configureOnRootProject(project: Project) = project.run { + apply() + + val isRelease = project.hasProperty("release") + val isSigning = isRelease || project.hasProperty("signArtifacts") + + val sourceTarball = tasks.register("sourceTarball") + sourceTarball.configure(){ + group = "build" + description = "Generate a source tarball for a release to be uploaded to dist.apache.org/repos/dist" + + doFirst { + val e = project.extensions.getByType(PublishingHelperExtension::class.java) + mkdir(e.distributionDir) + exec { + executable = "git" + args("archive", "--prefix=${e.baseName.get()}/", "--format=tar.gz", "--output=${e.sourceTarball.get().asFile.relativeTo(projectDir)}", "HEAD") + workingDir(project.projectDir) + } + } + } + + val digestSourceTarball = tasks.register("digestSourceTarball") + digestSourceTarball.configure { + mustRunAfter(sourceTarball) + + doFirst { + val e = project.extensions.getByType(PublishingHelperExtension::class.java) + generateDigest(e.sourceTarball.get().asFile, e.sourceTarballDigest.get().asFile, "SHA-512") + } + } + + sourceTarball.configure { + finalizedBy(digestSourceTarball) + } + + if (isSigning) { + val signSourceTarball = tasks.register("signSourceTarball") + signSourceTarball.configure { + mustRunAfter(sourceTarball) + doFirst { + val e = project.extensions.getByType(PublishingHelperExtension::class.java) + sign(e.sourceTarball.get().asFile) + } + } + sourceTarball.configure { + finalizedBy(signSourceTarball) + } + } + + val releaseEmailTemplate = tasks.register("releaseEmailTemplate") + releaseEmailTemplate.configure { + group = "publishing" + description = "Generate release-vote email subject + body, including the staging repository URL, if run during the Maven release." + + mustRunAfter("initializeApacheStagingRepository") + + doFirst { + val e = project.extensions.getByType(PublishingHelperExtension::class.java) + val asfName = e.asfProjectName.get() + + val gitInfo = MemoizedGitInfo.gitInfo(rootProject) + val gitCommitId = gitInfo["Apache-Polaris-Build-Git-Head"] + + val repos = project.extensions.getByType(NexusPublishExtension::class.java).repositories + val repo = repos.iterator().next() + + val stagingRepositoryUrlRegistryRegistration = gradle.sharedServices.registrations.named>("stagingRepositoryUrlRegistry") + val staginRepoUrl = if (stagingRepositoryUrlRegistryRegistration.isPresent) { + val stagingRepositoryUrlRegistryBuildServiceRegistration = stagingRepositoryUrlRegistryRegistration.get() + val stagingRepositoryUrlRegistryService = stagingRepositoryUrlRegistryBuildServiceRegistration.getService() + if (stagingRepositoryUrlRegistryService.isPresent) { + val registry = stagingRepositoryUrlRegistryService.get().registry + try { + val stagingRepoDesc = registry.get(repo.name) + val stagingRepoId = stagingRepoDesc.stagingRepositoryId + "https://repository.apache.org/content/repositories/$stagingRepoId/" + } catch (e: IllegalStateException) { + "NO STAGING REPOSITORY ($e)" + } + } else { + "NO STAGING REPOSITORY (no registry service) !!" + } + } else { + "NO STAGING REPOSITORY (no build service) !!" + } + + val (asfPrj, _) = fetchAsfProject(asfName) + val asfProjectName = asfPrj["name"] as String + + val versionNoRc = version.toString().replace("-rc-?[0-9]+".toRegex(), "") + + val subjectFile = e.distributionFile("vote-email-subject.txt").relativeTo(projectDir) + val bodyFile = e.distributionFile("vote-email-body.txt").relativeTo(projectDir) + + val emailSubject = "[VOTE] Release $asfProjectName $version" + subjectFile.writeText(emailSubject) + + val emailBody = """ + Hi everyone, + + I propose that we release the following RC as the official + $asfProjectName $versionNoRc release. + + The commit ID is 229d8f6fcd109e6c8943ea7cbb41dab746c6d0ed + * This corresponds to the tag: apache-$asfName-$version + * https://github.com/apache/$asfName/commits/apache-$asfName-$version + * https://github.com/apache/$asfName/tree/$gitCommitId + + The release tarball, signature, and checksums are here: + * https://dist.apache.org/repos/dist/dev/$asfName/apache-$asfName-$version + + You can find the KEYS file here: + * https://dist.apache.org/repos/dist/dev/$asfName/KEYS + + Convenience binary artifacts are staged on Nexus. The Maven repository URL is: + * $staginRepoUrl + + Please download, verify, and test. + + Please vote in the next 72 hours. + + [ ] +1 Release this as Apache Iceberg 1.6.0 + [ ] +0 + [ ] -1 Do not release this because... + + Only PMC members have binding votes, but other community members are + encouraged to cast non-binding votes. This vote will pass if there are + 3 binding +1 votes and more binding +1 votes than -1 votes. + + Thanks + Regards + """ + + logger.lifecycle(""" + + + The email for your release vote mail: + ------------------------------------- + + Suggested subject: (also in file $subjectFile) + + $emailSubject + + Suggested body: (also in file $bodyFile) + + $emailBody + + """.trimIndent()) + bodyFile.writeText(emailBody.trimIndent()) + } + } + + if (isRelease) { + sourceTarball.configure { + finalizedBy(releaseEmailTemplate) + } + } + + afterEvaluate { + tasks.named("closeApacheStagingRepository") { mustRunAfter(releaseEmailTemplate) } + } +} diff --git a/build-logic/src/main/kotlin/publishing/shadowPub.kt b/build-logic/src/main/kotlin/publishing/shadowPub.kt new file mode 100644 index 000000000..4fb78410e --- /dev/null +++ b/build-logic/src/main/kotlin/publishing/shadowPub.kt @@ -0,0 +1,125 @@ +/* + * 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. + */ + +package publishing + +import groovy.util.Node +import groovy.util.NodeList +import org.gradle.api.Project +import org.gradle.api.artifacts.ConfigurationVariant +import org.gradle.api.artifacts.ProjectDependency +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.SoftwareComponentFactory +import org.gradle.api.plugins.JavaBasePlugin +import org.gradle.api.publish.maven.MavenPublication + + +/** + * "Proper" publication of shadow-jar instead of the "main" jar, with "the right" Gradle's module + * metadata that refers to the shadow-jar instead of the "main" jar, which is not published by + * Nessie. + * + * Pieces of this function are taken from the `Java(Base)Plugin` and `ShadowExtension`. + */ +internal fun configureShadowPublishing(project: Project, mavenPublication: MavenPublication, softwareComponentFactory: SoftwareComponentFactory) = project.run { + fun isPublishable(element: ConfigurationVariant): Boolean { + for (artifact in element.artifacts) { + if (JavaBasePlugin.UNPUBLISHABLE_VARIANT_ARTIFACTS.contains(artifact.type)) { + return false + } + } + return true + } + + val shadowJar = project.tasks.named("shadowJar") + + val shadowApiElements = + project.configurations.create("shadowApiElements") { + isCanBeConsumed = true + isCanBeResolved = false + attributes { + attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage::class.java, Usage.JAVA_API)) + attribute( + Category.CATEGORY_ATTRIBUTE, + project.objects.named(Category::class.java, Category.LIBRARY) + ) + attribute( + LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, + project.objects.named(LibraryElements::class.java, LibraryElements.JAR) + ) + attribute( + Bundling.BUNDLING_ATTRIBUTE, + project.objects.named(Bundling::class.java, Bundling.SHADOWED) + ) + } + outgoing.artifact(shadowJar) + } + + val component = softwareComponentFactory.adhoc("shadow") + component.addVariantsFromConfiguration(shadowApiElements) { + if (isPublishable(configurationVariant)) { + mapToMavenScope("compile") + } else { + skip() + } + } + // component.addVariantsFromConfiguration(configurations.getByName("runtimeElements")) { + component.addVariantsFromConfiguration( + project.configurations.getByName("shadowRuntimeElements") + ) { + if (isPublishable(configurationVariant)) { + mapToMavenScope("runtime") + } else { + skip() + } + } + // Sonatype requires the javadoc and sources jar to be present, but the + // Shadow extension does not publish those. + component.addVariantsFromConfiguration(project.configurations.getByName("javadocElements")) {} + component.addVariantsFromConfiguration(project.configurations.getByName("sourcesElements")) {} + mavenPublication.from(component) + + // This a replacement to add dependencies to the pom, if necessary. Equivalent to + // 'shadowExtension.component(mavenPublication)', which we cannot use. + + mavenPublication.pom { + withXml { + val node = asNode() + val depNode = node.get("dependencies") + val dependenciesNode = + if ((depNode as NodeList).isNotEmpty()) depNode[0] as Node + else node.appendNode("dependencies") + project.configurations.getByName("shadow").allDependencies.forEach { + @Suppress("DEPRECATION") + if ( + (it is ProjectDependency) || it !is org.gradle.api.artifacts.SelfResolvingDependency + ) { + val dependencyNode = dependenciesNode.appendNode("dependency") + dependencyNode.appendNode("groupId", it.group) + dependencyNode.appendNode("artifactId", it.name) + dependencyNode.appendNode("version", it.version) + dependencyNode.appendNode("scope", "runtime") + } + } + } + } +} diff --git a/build-logic/src/main/kotlin/publishing/util.kt b/build-logic/src/main/kotlin/publishing/util.kt new file mode 100644 index 000000000..ee5919ce7 --- /dev/null +++ b/build-logic/src/main/kotlin/publishing/util.kt @@ -0,0 +1,109 @@ +/* + * 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. + */ + +package publishing + +import groovy.json.JsonException +import groovy.json.JsonSlurper +import groovy.util.Node +import groovy.util.NodeList +import groovy.xml.XmlSlurper +import groovy.xml.slurpersupport.GPathResult +import org.gradle.api.GradleException +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.component.ModuleComponentSelector +import org.gradle.api.artifacts.result.DependencyResult +import java.io.File +import java.io.FileNotFoundException +import java.net.URI +import java.security.MessageDigest + +internal fun findDependency( + config: Configuration?, + depGroup: String, + depName: String +): DependencyResult? { + if (config != null) { + val depResult = + config.incoming.resolutionResult.allDependencies.find { depResult -> + val req = depResult.requested + if (req is ModuleComponentSelector) req.group == depGroup && req.module == depName + else false + } + return depResult + } + return null +} + +internal fun xmlNode(node: Node?, child: String): Node? { + val found = node?.get(child) + if (found is NodeList) { + if (found.isNotEmpty()) { + return found[0] as Node + } + } + return null +} + +internal fun generateDigest(input: File, output: File, algorithm: String) { + val md = MessageDigest.getInstance(algorithm) + input.inputStream().use { + val buffered = it.buffered(8192) + val buf = ByteArray(8192) + var rd: Int + while (true) { + rd = buffered.read(buf) + if (rd == -1) break + md.update(buf, 0, rd) + } + + output.writeText(md.digest().joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }) + } +} + +internal fun fetchAsfProject(asfName: String): Pair, Boolean> { + val tlpPrj: Map? = parseJson("https://projects.apache.org/json/projects/$asfName.json") + return if (tlpPrj != null) { + Pair(tlpPrj, false) + } else { + val podlings: Map> = parseJson("https://projects.apache.org/json/foundation/podlings.json")!! + val podling = podlings[asfName] + if (podling == null) { + throw GradleException("Neither a project nor a podling for $asfName could be found at Apache") + } + Pair(podling, true) + } +} + +internal fun unsafeCast(o: Any?): T { + @Suppress("UNCHECKED_CAST") + return o as T +} + +internal fun parseJson(url: String): T? { + try { + @Suppress("UNCHECKED_CAST") + return JsonSlurper().parse(URI(url).toURL()) as T + } catch (e: JsonException) { + if (e.cause is FileNotFoundException) { + return null + } + throw e + } +} diff --git a/build.gradle.kts b/build.gradle.kts index e761dfcd7..b0ba99d6f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -106,3 +106,31 @@ tasks.named("rat").configure { excludes.add("**/*.lock") } + +// Pass environment variables: +// ORG_GRADLE_PROJECT_apacheUsername +// ORG_GRADLE_PROJECT_apachePassword +// OR in ~/.gradle/gradle.properties set +// apacheUsername +// apachePassword +// Call targets: +// publishToApache +// closeApacheStagingRepository +// releaseApacheStagingRepository +// or closeAndReleaseApacheStagingRepository +nexusPublishing { + transitionCheckOptions { + // default==60 (10 minutes), wait up to 120 minutes + maxRetries = 720 + // default 10s + delayBetween = java.time.Duration.ofSeconds(10) + } + + repositories { + register("apache") { + this.nexusUrl // TODO configure + this.snapshotRepositoryUrl // TODO configure + this.stagingProfileId // TODO configure if necessary + } + } +} diff --git a/gradle.properties b/gradle.properties index a934ca712..9e1de428d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,6 @@ # under the License. # group=org.apache.polaris -version=1.0.0 # enable the Gradle build cache org.gradle.caching=true diff --git a/gradle/baselibs.versions.toml b/gradle/baselibs.versions.toml index 5391dcd62..72da54e13 100644 --- a/gradle/baselibs.versions.toml +++ b/gradle/baselibs.versions.toml @@ -21,4 +21,6 @@ errorprone = { module = "net.ltgt.gradle:gradle-errorprone-plugin", version = "4.0.1" } idea-ext = { module = "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle-idea-ext", version = "1.1.8" } license-report = { module = "com.github.jk1:gradle-license-report", version = "2.8" } +nexus-publish = { module = "io.github.gradle-nexus:publish-plugin", version = "2.0.0" } +shadow = { module = "com.gradleup.shadow:shadow-gradle-plugin", version = "8.3.0" } spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "6.25.0" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ba713945f..3eecb103b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -76,4 +76,3 @@ threeten-extra = { module = "org.threeten:threeten-extra", version = "1.8.0" } openapi-generator = { id = "org.openapi.generator", version = "7.6.0" } rat = { id = "org.nosphere.apache.rat", version = "0.8.1" } spotless = { id = "com.diffplug.spotless", version = "6.25.0" } -shadow = { id = "com.gradleup.shadow", version = "8.3.0" } diff --git a/polaris-core/build.gradle.kts b/polaris-core/build.gradle.kts index c1850b313..b07a18767 100644 --- a/polaris-core/build.gradle.kts +++ b/polaris-core/build.gradle.kts @@ -127,32 +127,33 @@ dependencies { openApiValidate { inputSpec = "$rootDir/spec/polaris-management-service.yml" } -tasks.register("generatePolarisService").configure { - inputSpec = "$rootDir/spec/polaris-management-service.yml" - generatorName = "jaxrs-resteasy" - outputDir = "$projectDir/build/generated" - modelPackage = "org.apache.polaris.core.admin.model" - ignoreFileOverride = "$rootDir/.openapi-generator-ignore" - removeOperationIdPrefix = true - templateDir = "$rootDir/server-templates" - globalProperties.put("apis", "false") - globalProperties.put("models", "") - globalProperties.put("apiDocs", "false") - globalProperties.put("modelTests", "false") - configOptions.put("useBeanValidation", "true") - configOptions.put("sourceFolder", "src/main/java") - configOptions.put("useJakartaEe", "true") - configOptions.put("generateBuilders", "true") - configOptions.put("generateConstructorWithAllArgs", "true") - additionalProperties.put("apiNamePrefix", "Polaris") - additionalProperties.put("apiNameSuffix", "Api") - additionalProperties.put("metricsPrefix", "polaris") - serverVariables = mapOf("basePath" to "api/v1") - - doFirst { delete(outputDir.get()) } -} +val generatePolarisService by + tasks.registering(GenerateTask::class) { + inputSpec = "$rootDir/spec/polaris-management-service.yml" + generatorName = "jaxrs-resteasy" + outputDir = "$projectDir/build/generated" + modelPackage = "org.apache.polaris.core.admin.model" + ignoreFileOverride = "$rootDir/.openapi-generator-ignore" + removeOperationIdPrefix = true + templateDir = "$rootDir/server-templates" + globalProperties.put("apis", "false") + globalProperties.put("models", "") + globalProperties.put("apiDocs", "false") + globalProperties.put("modelTests", "false") + configOptions.put("useBeanValidation", "true") + configOptions.put("sourceFolder", "src/main/java") + configOptions.put("useJakartaEe", "true") + configOptions.put("generateBuilders", "true") + configOptions.put("generateConstructorWithAllArgs", "true") + additionalProperties.put("apiNamePrefix", "Polaris") + additionalProperties.put("apiNameSuffix", "Api") + additionalProperties.put("metricsPrefix", "polaris") + serverVariables = mapOf("basePath" to "api/v1") + } -tasks.named("compileJava").configure { dependsOn("generatePolarisService") } +listOf("sourcesJar", "compileJava").forEach { task -> + tasks.named(task) { dependsOn(generatePolarisService) } +} sourceSets { main { java { srcDir(project.layout.buildDirectory.dir("generated/src/main/java")) } } diff --git a/polaris-service/build.gradle.kts b/polaris-service/build.gradle.kts index e0f990ae3..c0cfa60f7 100644 --- a/polaris-service/build.gradle.kts +++ b/polaris-service/build.gradle.kts @@ -21,10 +21,10 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.openapitools.generator.gradle.plugin.tasks.GenerateTask plugins { - alias(libs.plugins.shadow) alias(libs.plugins.openapi.generator) id("polaris-server") id("polaris-license-report") + id("polaris-shadow-jar") id("application") } @@ -174,34 +174,35 @@ openApiGenerate { ) } -tasks.register("generatePolarisService").configure { - inputSpec = "$rootDir/spec/polaris-management-service.yml" - generatorName = "jaxrs-resteasy" - outputDir = "$projectDir/build/generated" - apiPackage = "org.apache.polaris.service.admin.api" - modelPackage = "org.apache.polaris.core.admin.model" - ignoreFileOverride = "$rootDir/.openapi-generator-ignore" - removeOperationIdPrefix = true - templateDir = "$rootDir/server-templates" - globalProperties.put("apis", "") - globalProperties.put("models", "false") - globalProperties.put("apiDocs", "false") - globalProperties.put("modelTests", "false") - configOptions.put("useBeanValidation", "true") - configOptions.put("sourceFolder", "src/main/java") - configOptions.put("useJakartaEe", "true") - configOptions.put("generateBuilders", "true") - configOptions.put("generateConstructorWithAllArgs", "true") - additionalProperties.put("apiNamePrefix", "Polaris") - additionalProperties.put("apiNameSuffix", "Api") - additionalProperties.put("metricsPrefix", "polaris") - serverVariables.put("basePath", "api/v1") +val generatePolarisService by + tasks.registering(GenerateTask::class) { + inputSpec = "$rootDir/spec/polaris-management-service.yml" + generatorName = "jaxrs-resteasy" + outputDir = "$projectDir/build/generated" + apiPackage = "org.apache.polaris.service.admin.api" + modelPackage = "org.apache.polaris.core.admin.model" + ignoreFileOverride = "$rootDir/.openapi-generator-ignore" + removeOperationIdPrefix = true + templateDir = "$rootDir/server-templates" + globalProperties.put("apis", "") + globalProperties.put("models", "false") + globalProperties.put("apiDocs", "false") + globalProperties.put("modelTests", "false") + configOptions.put("useBeanValidation", "true") + configOptions.put("sourceFolder", "src/main/java") + configOptions.put("useJakartaEe", "true") + configOptions.put("generateBuilders", "true") + configOptions.put("generateConstructorWithAllArgs", "true") + additionalProperties.put("apiNamePrefix", "Polaris") + additionalProperties.put("apiNameSuffix", "Api") + additionalProperties.put("metricsPrefix", "polaris") + serverVariables.put("basePath", "api/v1") + } - doFirst { delete(outputDir.get()) } +listOf("sourcesJar", "compileJava").forEach { task -> + tasks.named(task) { dependsOn("openApiGenerate", generatePolarisService) } } -tasks.named("compileJava").configure { dependsOn("openApiGenerate", "generatePolarisService") } - sourceSets { main { java { srcDir(project.layout.buildDirectory.dir("generated/src/main/java")) } } } @@ -230,13 +231,24 @@ tasks.named("jar") { manifest { attributes["Main-Class"] = "org.apache.polaris.service.PolarisApplication" } } -tasks.named("shadowJar") { - manifest { attributes["Main-Class"] = "org.apache.polaris.service.PolarisApplication" } - archiveVersion.set("") - mergeServiceFiles() - isZip64 = true -} +val shadowJar = + tasks.named("shadowJar") { + manifest { attributes["Main-Class"] = "org.apache.polaris.service.PolarisApplication" } + mergeServiceFiles() + isZip64 = true + finalizedBy("startScripts") + } -tasks.named("startScripts") { classpath = files("polaris-service-all.jar") } +val startScripts = + tasks.named("startScripts") { + classpath = files(provider { shadowJar.get().archiveFileName }) + } + +tasks.register("prepareDockerDist") { + into(project.layout.buildDirectory.dir("docker-dist")) + from(startScripts) { into("bin") } + from(shadowJar) { into("lib") } + doFirst { delete(project.layout.buildDirectory.dir("regtest-dist")) } +} -tasks.named("build").configure { dependsOn("shadowJar") } +tasks.named("build").configure { dependsOn("prepareDockerDist") } diff --git a/settings.gradle.kts b/settings.gradle.kts index fad172deb..e23cf56c4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,6 +36,8 @@ if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) { rootProject.name = "polaris" +val baseVersion = file("version.txt").readText().trim() + fun loadProperties(file: File): Properties { val props = Properties() file.reader().use { reader -> props.load(reader) } @@ -69,3 +71,5 @@ dependencyResolutionManagement { gradlePluginPortal() } } + +gradle.beforeProject { version = baseVersion } diff --git a/version.txt b/version.txt new file mode 100644 index 000000000..3db3d09d3 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +999-SNAPSHOT \ No newline at end of file