diff --git a/build.gradle.kts b/build.gradle.kts index e48630122d..e5fadafd4a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -67,6 +67,9 @@ tasks.named("rat").configure { excludes.add("LICENSE") excludes.add("NOTICE") + // Manifest files do not allow comments + excludes.add("tools/version/src/jarTest/resources/META-INF/FAKE_MANIFEST.MF") + excludes.add("ide-name.txt") excludes.add("version.txt") excludes.add(".git") diff --git a/gradle/projects.main.properties b/gradle/projects.main.properties index 0d09894a6b..bf7feec169 100644 --- a/gradle/projects.main.properties +++ b/gradle/projects.main.properties @@ -27,3 +27,4 @@ polaris-dropwizard-service=dropwizard/service polaris-eclipselink=extension/persistence/eclipselink polaris-jpa-model=extension/persistence/jpa-model aggregated-license-report=aggregated-license-report +polaris-version=tools/version diff --git a/tools/version/build.gradle.kts b/tools/version/build.gradle.kts new file mode 100644 index 0000000000..1ebb5bc3e8 --- /dev/null +++ b/tools/version/build.gradle.kts @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.tools.ant.filters.ReplaceTokens + +plugins { + id("polaris-client") + `java-library` + `java-test-fixtures` + `jvm-test-suite` +} + +dependencies { testFixturesApi(libs.assertj.core) } + +description = + "Provides Polaris version information programmatically, includes the NOTICE/LICENSE* files" + +val syncNoticeAndLicense by + tasks.registering(Sync::class) { + // Have to manually declare the inputs of this task here on top of the from/include below + inputs.files(rootProject.layout.files("NOTICE*", "LICENSE*", "version.txt")) + inputs.property("version", project.version) + destinationDir = project.layout.buildDirectory.dir("notice-licenses").get().asFile + from(rootProject.rootDir) { + include("NOTICE*", "LICENSE*") + // Put NOTICE/LICENSE* files under META-INF/resources/ so those files can be directly + // accessed as static web resources in Quarkus. + eachFile { path = "META-INF/resources/apache-polaris/${file.name}.txt" } + } + from(rootProject.rootDir) { + include("version.txt") + // Put NOTICE/LICENSE* files under META-INF/resources/ so those files can be directly + // accessed as static web resources in Quarkus. + eachFile { path = "META-INF/resources/apache-polaris/${file.name}" } + } + } + +val versionProperties by + tasks.registering(Sync::class) { + destinationDir = project.layout.buildDirectory.dir("version").get().asFile + from(project.layout.files("src/main/version")) + eachFile { path = "org/apache/polaris/version/$path" } + inputs.property("projectVersion", project.version) + filter(ReplaceTokens::class, mapOf("tokens" to mapOf("projectVersion" to project.version))) + } + +sourceSets.main.configure { + resources { + srcDir(syncNoticeAndLicense) + srcDir(versionProperties) + } +} + +// Build a jar for `jarTest` having both the production and test sources including the "fake +// manifest" - the production implementation expects all resources to be in the jar containing +// the `polaris-version.properties` file. +val jarTestJar by + tasks.registering(Jar::class) { + archiveClassifier.set("jarTest") + from(sourceSets.main.get().output) + from(sourceSets.getByName("jarTest").output) + } + +// Add a test-suite to run against the built polaris-version*.jar, not the classes/, because we +// need to test the `jar:` scheme/protocol resolution. +testing { + suites { + withType { useJUnitJupiter(libs.junit.bom.map { it.version!! }) } + + register("jarTest") { + dependencies { + compileOnly(project()) + runtimeOnly(files(jarTestJar.get().archiveFile.get().asFile)) + implementation(libs.assertj.core) + } + + targets.all { + testTask.configure { + dependsOn("jar", jarTestJar) + systemProperty("rootProjectDir", rootProject.rootDir.relativeTo(project.projectDir)) + systemProperty("polarisVersion", project.version) + } + } + } + } +} + +tasks.named("test") { dependsOn("jarTest") } diff --git a/tools/version/src/jarTest/java/org/apache/polaris/version/TestPolarisVersion.java b/tools/version/src/jarTest/java/org/apache/polaris/version/TestPolarisVersion.java new file mode 100644 index 0000000000..f50a779fb5 --- /dev/null +++ b/tools/version/src/jarTest/java/org/apache/polaris/version/TestPolarisVersion.java @@ -0,0 +1,114 @@ +/* + * 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 org.apache.polaris.version; + +import static java.lang.String.format; +import static org.apache.polaris.version.PolarisVersion.getBuildGitTag; +import static org.apache.polaris.version.PolarisVersion.getBuildGitHead; +import static org.apache.polaris.version.PolarisVersion.getBuildJavaVersion; +import static org.apache.polaris.version.PolarisVersion.getBuildReleasedVersion; +import static org.apache.polaris.version.PolarisVersion.getBuildSystem; +import static org.apache.polaris.version.PolarisVersion.getBuildTimestamp; +import static org.apache.polaris.version.PolarisVersion.isReleaseBuild; +import static org.apache.polaris.version.PolarisVersion.polarisVersionString; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.function.Supplier; +import java.util.stream.Stream; +import org.assertj.core.api.SoftAssertions; +import org.assertj.core.api.junit.jupiter.InjectSoftAssertions; +import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(SoftAssertionsExtension.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TestPolarisVersion { + @InjectSoftAssertions private SoftAssertions soft; + + /** + * Test runs using a "non release" build, so the MANIFEST.MF file has no release version + * information. + */ + @Test + @Order(1) + public void versionAvailable() { + soft.assertThat(polarisVersionString()).isEqualTo(System.getProperty("polarisVersion")); + soft.assertThat(isReleaseBuild()).isFalse(); + soft.assertThat(getBuildReleasedVersion()).isEmpty(); + soft.assertThat(getBuildTimestamp()).isEmpty(); + soft.assertThat(getBuildGitHead()).isEmpty(); + soft.assertThat(getBuildGitTag()).isEmpty(); + soft.assertThat(getBuildSystem()).isEmpty(); + soft.assertThat(getBuildJavaVersion()).isEmpty(); + } + + /** + * Test runs using a static "release" build manifest, {@link + * org.apache.polaris.version.PolarisVersion.PolarisVersionJarInfo#loadManifest(String)} + * overwrites the already loaded build-info, so this test has to run after {@link + * #versionAvailable()}. + */ + @Test + @Order(2) + public void fakeReleaseManifest() { + PolarisVersion.PolarisVersionJarInfo.loadManifest("FAKE_MANIFEST.MF"); + + soft.assertThat(polarisVersionString()).isEqualTo(System.getProperty("polarisVersion")); + soft.assertThat(isReleaseBuild()).isTrue(); + soft.assertThat(getBuildReleasedVersion()).contains("0.1.2-incubating-SNAPSHOT"); + soft.assertThat(getBuildTimestamp()).contains("2024-12-26-10:31:19+01:00"); + soft.assertThat(getBuildGitHead()).contains("27cf81929cbb08e545c8fcb1ed27a53d7ef1af79"); + soft.assertThat(getBuildGitTag()).contains("foo-tag-bar"); + soft.assertThat(getBuildSystem()) + .contains( + "Linux myawesomehost 6.12.6 #81 SMP PREEMPT_DYNAMIC Fri Dec 20 09:22:38 CET 2024 x86_64 x86_64 x86_64 GNU/Linux"); + soft.assertThat(getBuildJavaVersion()).contains("21.0.5"); + } + + @Test + public void versionTxtResource() { + soft.assertThat(PolarisVersion.readResource("version").trim()) + .isEqualTo(System.getProperty("polarisVersion")); + } + + @ParameterizedTest + @MethodSource + public void noticeLicense(String name, Supplier supplier) throws Exception { + var supplied = supplier.get(); + var expected = + Files.readString(Paths.get(format("%s/%s", System.getProperty("rootProjectDir"), name))); + soft.assertThat(supplied).isEqualTo(expected); + } + + static Stream noticeLicense() { + return Stream.of( + Arguments.arguments("NOTICE", (Supplier) PolarisVersion::readNoticeFile), + Arguments.arguments("LICENSE", (Supplier) PolarisVersion::readSourceLicenseFile), + Arguments.arguments( + "LICENSE-BINARY-DIST", (Supplier) PolarisVersion::readBinaryLicenseFile)); + } +} diff --git a/tools/version/src/jarTest/resources/META-INF/FAKE_MANIFEST.MF b/tools/version/src/jarTest/resources/META-INF/FAKE_MANIFEST.MF new file mode 100644 index 0000000000..beebc06d9e --- /dev/null +++ b/tools/version/src/jarTest/resources/META-INF/FAKE_MANIFEST.MF @@ -0,0 +1,9 @@ +Manifest-Version: 1.0 +Apache-Polaris-Version: 0.1.2-incubating-SNAPSHOT +Apache-Polaris-Is-Release: true +Apache-Polaris-Build-Git-Head: 27cf81929cbb08e545c8fcb1ed27a53d7ef1af79 +Apache-Polaris-Build-Git-Describe: foo-tag-bar +Apache-Polaris-Build-Timestamp: 2024-12-26-10:31:19+01:00 +Apache-Polaris-Build-System: Linux myawesomehost 6.12.6 #81 SMP PREEMPT_DY + NAMIC Fri Dec 20 09:22:38 CET 2024 x86_64 x86_64 x86_64 GNU/Linux +Apache-Polaris-Build-Java-Version: 21.0.5 diff --git a/tools/version/src/main/java/org/apache/polaris/version/PolarisVersion.java b/tools/version/src/main/java/org/apache/polaris/version/PolarisVersion.java new file mode 100644 index 0000000000..8c4ebc8dff --- /dev/null +++ b/tools/version/src/main/java/org/apache/polaris/version/PolarisVersion.java @@ -0,0 +1,246 @@ +/* + * 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 org.apache.polaris.version; + +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +/** + * Utility class to retrieve the current Polaris version and the contents of the {@code NOTICE} and + * {@code LICENSE*} files. + * + *

If built as a release using the Gradle {@code -Prelease} project property, more information + * like the Git tag, Git commit ID, build system and more is available as well. + */ +public final class PolarisVersion { + private PolarisVersion() {} + + /** The version string as in the file {@code version.txt} in the project's root directory. */ + public static String polarisVersionString() { + return PolarisVersionNumber.POLARIS_VERSION; + } + + /** + * Flag whether the build is a released version, when Polaris has been built with the Gradle + * {@code -Prelease} project property. If {@code true}, the {@code getBuild*} functions return + * meaningful values. + */ + public static boolean isReleaseBuild() { + return "true".equals(PolarisVersionJarInfo.buildInfo(MF_IS_RELEASE).orElse("false")); + } + + /** + * Returns the version as in the jar manifest, if {@linkplain #isReleaseBuild() build-time Git + * information} is available, when Polaris has been built with the Gradle {@code -Prelease} + * project property.. + * + *

Example values: {@code 1.0.0-incubating-SNAPSHOT}, {@code 1.0.0-incubating}, {@code 1.0.0} + * + * @see #isReleaseBuild() + */ + public static Optional getBuildReleasedVersion() { + return PolarisVersionJarInfo.buildInfo(MF_VERSION); + } + + /** + * Returns the commit ID as in the jar manifest, if {@linkplain #isReleaseBuild() build-time Git + * information} is available, when Polaris has been built with the Gradle {@code -Prelease} + * project property. + * + *

Example value: {@code d417725ec7c88c1ee8f940ffb2ce72aec7fb2a17} + * + * @see #isReleaseBuild() + */ + public static Optional getBuildGitHead() { + return PolarisVersionJarInfo.buildInfo(MF_BUILD_GIT_HEAD); + } + + /** + * Returns the output of {@code git describe --tags} as in the jar manifest, if {@linkplain + * #isReleaseBuild() build-time Git information} is available, when Polaris has been built with + * the Gradle {@code -Prelease} project property. + * + *

Example value: {@code apache-polaris-0.1.2} + * + * @see #isReleaseBuild() + */ + public static Optional getBuildGitTag() { + return PolarisVersionJarInfo.buildInfo(MF_BUILD_GIT_DESCRIBE); + } + + /** + * Returns the Java version used during the build as in the jar manifest, if {@linkplain + * #isReleaseBuild() build-time Git information} is available, when Polaris has been built with + * the Gradle {@code -Prelease} project property. + * + *

Example value: {@code 21.0.5} + * + * @see #isReleaseBuild() + */ + public static Optional getBuildJavaVersion() { + return PolarisVersionJarInfo.buildInfo(MF_BUILD_JAVA_VERSION); + } + + /** + * Returns information about the system that performed the build, if {@linkplain #isReleaseBuild() + * build-time Git information} is available, when Polaris has been built with the Gradle {@code + * -Prelease} project property. + * + *

Example value: {@code Linux myawesomehost 6.12.6 #81 SMP PREEMPT_DYNAMIC Fri Dec 20 09:22:38 + * CET 2024 x86_64 x86_64 x86_64 GNU/Linux} + * + * @see #isReleaseBuild() + */ + public static Optional getBuildSystem() { + return PolarisVersionJarInfo.buildInfo(MF_BUILD_SYSTEM); + } + + /** + * Returns the build timestamp as in the jar manifest, if {@linkplain #isReleaseBuild() build-time + * Git information} is available, when Polaris has been built with the Gradle {@code -Prelease} + * project property. + * + *

Example value: {@code 2024-12-16-11:54:05+01:00} + * + * @see #isReleaseBuild() + */ + public static Optional getBuildTimestamp() { + return PolarisVersionJarInfo.buildInfo(MF_BUILD_TIMESTAMP); + } + + public static String readNoticeFile() { + return readResource("NOTICE"); + } + + public static String readSourceLicenseFile() { + return readResource("LICENSE"); + } + + public static String readBinaryLicenseFile() { + return readResource("LICENSE-BINARY-DIST"); + } + + private static final String MF_VERSION = "Apache-Polaris-Version"; + private static final String MF_IS_RELEASE = "Apache-Polaris-Is-Release"; + private static final String MF_BUILD_GIT_HEAD = "Apache-Polaris-Build-Git-Head"; + private static final String MF_BUILD_GIT_DESCRIBE = "Apache-Polaris-Build-Git-Describe"; + private static final String MF_BUILD_TIMESTAMP = "Apache-Polaris-Build-Timestamp"; + private static final String MF_BUILD_SYSTEM = "Apache-Polaris-Build-System"; + private static final String MF_BUILD_JAVA_VERSION = "Apache-Polaris-Build-Java-Version"; + private static final List MF_ALL = + List.of( + MF_VERSION, + MF_IS_RELEASE, + MF_BUILD_GIT_HEAD, + MF_BUILD_GIT_DESCRIBE, + MF_BUILD_TIMESTAMP, + MF_BUILD_SYSTEM, + MF_BUILD_JAVA_VERSION); + + static String readResource(String resource) { + var fullResource = format("/META-INF/resources/apache-polaris/%s.txt", resource); + var resourceUrl = + requireNonNull( + PolarisVersion.class.getResource(fullResource), + "Resource " + fullResource + " does not exist"); + try (InputStream in = resourceUrl.openConnection().getInputStream()) { + return new String(in.readAllBytes(), UTF_8); + } catch (IOException e) { + throw new RuntimeException("Failed to load resource " + fullResource, e); + } + } + + // Couple inner classes to leverage Java's class loading mechanism as singletons and for + // initialization. Package-protected for testing. + + static final class PolarisVersionJarInfo { + static final Map BUILD_INFO = new HashMap<>(); + + static Optional buildInfo(String key) { + return Optional.ofNullable(BUILD_INFO.get(key)); + } + + static { + loadManifest("MANIFEST.MF"); + } + + static void loadManifest(String manifestFile) { + var polarisVersionResource = PolarisVersionResource.POLARIS_VERSION_RESOURCE; + if ("jar".equals(polarisVersionResource.getProtocol())) { + var path = polarisVersionResource.toString(); + var jarSep = path.lastIndexOf('!'); + if (jarSep == -1) { + throw new IllegalStateException( + "Could not determine the jar of the Apache Polaris version artifact: " + path); + } + var manifestPath = path.substring(0, jarSep + 1) + "/META-INF/" + manifestFile; + try { + try (InputStream in = + URI.create(manifestPath).toURL().openConnection().getInputStream()) { + var manifest = new Manifest(in); + var attributes = manifest.getMainAttributes(); + MF_ALL.stream() + .map(Attributes.Name::new) + .filter(attributes::containsKey) + .forEach(k -> BUILD_INFO.put(k.toString(), attributes.getValue(k))); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + } + + static final class PolarisVersionNumber { + static final String POLARIS_VERSION; + + static { + try (InputStream in = + PolarisVersionResource.POLARIS_VERSION_RESOURCE.openConnection().getInputStream()) { + Properties p = new Properties(); + p.load(in); + POLARIS_VERSION = p.getProperty("polaris.version"); + } catch (IOException e) { + throw new RuntimeException( + "Failed to load Apache Polaris version from org/apache/polaris/version/polaris-version.properties resource", + e); + } + } + } + + static final class PolarisVersionResource { + static final URL POLARIS_VERSION_RESOURCE = + requireNonNull( + PolarisVersion.class.getResource("polaris-version.properties"), + "Resource org/apache/polaris/version/polaris-version.properties containing the Apache Polaris version does not exist"); + } +} diff --git a/tools/version/src/main/version/polaris-version.properties b/tools/version/src/main/version/polaris-version.properties new file mode 100644 index 0000000000..5a1bc8be39 --- /dev/null +++ b/tools/version/src/main/version/polaris-version.properties @@ -0,0 +1,20 @@ +# +# 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. +# + +polaris.version=@projectVersion@