Browse Source

Fix reproducability of builds against Java EA versions (#132847)

* Introduce dedicated toolchain resolver for EA java versions

* Fix reproducability of builds against Java EA versions

This fixes a reproducability issue when using the gradle javaToolChain api.

There is no way to test toolchain candidates reliable against the build number in use.

This meant that ones you e.g. have resolved some version of java 25, gradle toolchain detection
does not detect the difference between certain builds (or even ea vs. released version)
Therefore we fallback to rely on our custom JDKDownloadPlugin for now here.

Syncing with the gradle team, they want to fix this somewhen in 9.x. We will revisit our solution
ones something is available. Ultimately we want to get rid of usages of the JDK download plugin.

* Minor cleanup and fixing non set distribution type in jdk

* Fix ea jdk selection not just based on major version

explicitly requires to add a ea postfix to the runtime sys property

* Fix test reproduce linies when using java ea version

* Apply review feedback

* Use runtime jdk when patching

* More cleanup
Rene Groeschke 2 months ago
parent
commit
16fe7db137
21 changed files with 420 additions and 129 deletions
  1. 22 3
      BUILDING.md
  2. 0 1
      build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/fixtures/AbstractGitAwareGradleFuncTest.groovy
  3. 36 20
      build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/JdkDownloadPluginFuncTest.groovy
  4. 15 4
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/Jdk.java
  5. 9 0
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/JdkDownloadPlugin.java
  6. 36 2
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java
  7. 160 0
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/toolchain/EarlyAccessCatalogJdkToolchainResolver.java
  8. 1 0
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/toolchain/JavaToolChainResolverPlugin.java
  9. 1 36
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/toolchain/OracleOpenJdkToolchainResolver.java
  10. 46 0
      build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/JdkSpec.groovy
  11. 1 0
      build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/toolchain/AbstractToolchainResolverSpec.groovy
  12. 1 1
      build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/toolchain/AdoptiumJdkToolchainResolverSpec.groovy
  13. 62 0
      build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/toolchain/EarlyAccessCatalogJdkToolchainResolverSpec.groovy
  14. 2 49
      build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/toolchain/OracleOpenJdkToolchainResolverSpec.groovy
  15. 5 3
      build-tools/src/main/java/org/elasticsearch/gradle/Architecture.java
  16. 6 4
      build-tools/src/main/java/org/elasticsearch/gradle/DistributionDownloadPlugin.java
  17. 10 3
      build-tools/src/main/java/org/elasticsearch/gradle/OS.java
  18. 2 2
      build.gradle
  19. 1 0
      gradle/verification-metadata.xml
  20. 1 0
      settings.gradle
  21. 3 1
      test/framework/src/main/java/org/elasticsearch/test/junit/listeners/ReproduceInfoPrinter.java

+ 22 - 3
BUILDING.md

@@ -92,7 +92,7 @@ uses the changed dependencies. In most cases, `precommit` or `check` are good ca
 We prefer sha256 checksums as md5 and sha1 are not considered safe anymore these days. The generated entry
 will have the `origin` attribute been set to `Generated by Gradle`.
 
-> [!Tip]  
+> [!Tip]
 > A manual confirmation of the Gradle generated checksums is currently not mandatory.
 > If you want to add a level of verification you can manually confirm the checksum (e.g. by looking it up on the website of the library)
 > Please replace the content of the `origin` attribute by `official site` in that case.
@@ -186,6 +186,25 @@ dependencies {
 
 To test an unreleased development version of a third party dependency you have several options.
 
+### How do I test against java early access (ea) versions?
+
+Currently only openjdk EA builds by oracle are supported.
+To test against an early access version java version you can pass the major
+java version appended with `-ea` as a system property (e.g. -Druntime.java=26-ea) to the Gradle build:
+
+```
+./gradlew clean test -Druntime.java=26-ea
+```
+
+This will run the tests using the JDK 26 EA version and pick the latest available build of the matching JDK EA version we expose
+in our custom jdk catalogue at `https://storage.googleapis.com/elasticsearch-jdk-archive/jdks/openjdk/latest.json`.
+
+To run against a specific build number of the EA build you can pass a second system property (e.g. `-Druntime.java.build=6`):
+
+```
+./gradlew clean test -Druntime.java=26-ea -Druntime.java.build=6
+```
+
 #### How to use a Maven based third party dependency via `mavenlocal`?
 
 1. Clone the third party repository locally
@@ -229,7 +248,7 @@ In addition to snapshot builds JitPack supports building Pull Requests. Simply u
 3. Run the Gradle build as needed. Keep in mind the initial resolution might take a bit longer as this needs to be built
 by JitPack in the background before we can resolve the adhoc built dependency.
 
-> [!Note]  
+> [!Note]
 > You should only use that approach locally or on a developer branch for production dependencies as we do
 not want to ship unreleased libraries into our releases.
 
@@ -261,7 +280,7 @@ allprojects {
   ```
 4. Run the Gradle build as needed with `--write-verification-metadata` to ensure the Gradle dependency verification does not fail on your custom dependency.
 
-> [!Note]  
+> [!Note]
 > As Gradle prefers to use modules whose descriptor has been created from real meta-data rather than being generated,
 flat directory repositories cannot be used to override artifacts with real meta-data from other repositories declared in the build.
 > For example, if Gradle finds only `jmxri-1.2.1.jar` in a flat directory repository, but `jmxri-1.2.1.pom` in another repository

+ 0 - 1
build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/fixtures/AbstractGitAwareGradleFuncTest.groovy

@@ -10,7 +10,6 @@
 package org.elasticsearch.gradle.fixtures
 
 import org.apache.commons.io.FileUtils
-import org.elasticsearch.gradle.internal.test.InternalAwareGradleRunner
 import org.gradle.testkit.runner.GradleRunner
 import org.junit.Rule
 import org.junit.rules.TemporaryFolder

+ 36 - 20
build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/JdkDownloadPluginFuncTest.groovy

@@ -33,13 +33,18 @@ class JdkDownloadPluginFuncTest extends AbstractGradleFuncTest {
     private static final String ADOPT_JDK_VERSION_15 = "15.0.2+7"
     private static final String AZUL_JDK_VERSION_8 = "8u302+b08"
     private static final String AZUL_8_DISTRO_VERSION = "8.56.0.23"
+    private static final String CATALOG_EA_VERSION = "25-ea+30"
     private static final String OPEN_JDK_VERSION = "12.0.1+99@123456789123456789123456789abcde"
     private static final Pattern JDK_HOME_LOGLINE = Pattern.compile("JDK HOME: (.*)")
 
+    def setup() {
+        configurationCacheCompatible = false // JDK class references configurations which break configuration cache
+    }
+
     @Unroll
-    def "jdk #jdkVendor for #platform#suffix are downloaded and extracted"() {
+    def "jdk #distributionVersion #jdkVendor for #platform#suffix are downloaded and extracted"() {
         given:
-        def mockRepoUrl = urlPath(jdkVendor, jdkVersion, platform, arch);
+        def mockRepoUrl = urlPath(jdkVendor, jdkVersion, platform, arch, distributionVersion);
         def mockedContent = filebytes(jdkVendor, platform)
         buildFile.text = """
             plugins {
@@ -77,21 +82,26 @@ class JdkDownloadPluginFuncTest extends AbstractGradleFuncTest {
 
         where:
         platform  | arch      | jdkVendor       | jdkVersion           | distributionVersion   | expectedJavaBin          | suffix
-        "linux"   | "x64"     | VENDOR_ADOPTIUM | ADOPT_JDK_VERSION    | null                  | "bin/java"               | ""
-        "linux"   | "x64"     | VENDOR_OPENJDK  | OPEN_JDK_VERSION     | null                  | "bin/java"               | ""
-        "linux"   | "x64"     | VENDOR_OPENJDK  | OPENJDK_VERSION_OLD  | null                  | "bin/java"               | "(old version)"
-        "windows" | "x64"     | VENDOR_ADOPTIUM | ADOPT_JDK_VERSION    | null                  | "bin/java"               | ""
-        "windows" | "x64"     | VENDOR_OPENJDK  | OPEN_JDK_VERSION     | null                  | "bin/java"               | ""
-        "windows" | "x64"     | VENDOR_OPENJDK  | OPENJDK_VERSION_OLD  | null                  | "bin/java"               | "(old version)"
-        "darwin"  | "x64"     | VENDOR_ADOPTIUM | ADOPT_JDK_VERSION    | null                  | "Contents/Home/bin/java" | ""
-        "darwin"  | "x64"     | VENDOR_OPENJDK  | OPEN_JDK_VERSION     | null                  | "Contents/Home/bin/java" | ""
-        "darwin"  | "x64"     | VENDOR_OPENJDK  | OPENJDK_VERSION_OLD  | null                  | "Contents/Home/bin/java" | "(old version)"
-        "mac"     | "x64"     | VENDOR_OPENJDK  | OPEN_JDK_VERSION     | null                  | "Contents/Home/bin/java" | ""
-        "mac"     | "x64"     | VENDOR_OPENJDK  | OPENJDK_VERSION_OLD  | null                  | "Contents/Home/bin/java" | "(old version)"
-        "darwin"  | "aarch64" | VENDOR_ADOPTIUM | ADOPT_JDK_VERSION    | null                  | "Contents/Home/bin/java" | ""
-        "linux"   | "aarch64" | VENDOR_ADOPTIUM | ADOPT_JDK_VERSION    | null                  | "bin/java"               | ""
-        "linux"   | "aarch64" | VENDOR_ADOPTIUM | ADOPT_JDK_VERSION_11 | null                  | "bin/java"               | "(jdk 11)"
-        "linux"   | "aarch64" | VENDOR_ADOPTIUM | ADOPT_JDK_VERSION_15 | null                  | "bin/java"               | "(jdk 15)"
+        "linux"   | "aarch64" | VENDOR_OPENJDK  | CATALOG_EA_VERSION   | "ea"                  | "bin/java"               | ""
+        "linux"   | "x64"     | VENDOR_OPENJDK  | CATALOG_EA_VERSION   | "ea"                  | "bin/java"               | ""
+        "darwin"  | "aarch64" | VENDOR_OPENJDK  | CATALOG_EA_VERSION   | "ea"                  | "Contents/Home/bin/java" | ""
+        "darwin"  | "x64"     | VENDOR_OPENJDK  | CATALOG_EA_VERSION   | "ea"                  | "Contents/Home/bin/java" | ""
+        "windows" | "x64"     | VENDOR_OPENJDK  | CATALOG_EA_VERSION   | "ea"                  | "bin/java"               | ""
+        "linux"   | "x64"     | VENDOR_ADOPTIUM | ADOPT_JDK_VERSION    | ""                    | "bin/java"               | ""
+        "linux"   | "x64"     | VENDOR_OPENJDK  | OPEN_JDK_VERSION     | ""                    | "bin/java"               | ""
+        "linux"   | "x64"     | VENDOR_OPENJDK  | OPENJDK_VERSION_OLD  | ""                    | "bin/java"               | "(old version)"
+        "windows" | "x64"     | VENDOR_ADOPTIUM | ADOPT_JDK_VERSION    | ""                    | "bin/java"               | ""
+        "windows" | "x64"     | VENDOR_OPENJDK  | OPEN_JDK_VERSION     | ""                    | "bin/java"               | ""
+        "windows" | "x64"     | VENDOR_OPENJDK  | OPENJDK_VERSION_OLD  | ""                    | "bin/java"               | "(old version)"
+        "darwin"  | "x64"     | VENDOR_ADOPTIUM | ADOPT_JDK_VERSION    | ""                    | "Contents/Home/bin/java" | ""
+        "darwin"  | "x64"     | VENDOR_OPENJDK  | OPEN_JDK_VERSION     | ""                    | "Contents/Home/bin/java" | ""
+        "darwin"  | "x64"     | VENDOR_OPENJDK  | OPENJDK_VERSION_OLD  | ""                    | "Contents/Home/bin/java" | "(old version)"
+        "mac"     | "x64"     | VENDOR_OPENJDK  | OPEN_JDK_VERSION     | ""                    | "Contents/Home/bin/java" | ""
+        "mac"     | "x64"     | VENDOR_OPENJDK  | OPENJDK_VERSION_OLD  | ""                    | "Contents/Home/bin/java" | "(old version)"
+        "darwin"  | "aarch64" | VENDOR_ADOPTIUM | ADOPT_JDK_VERSION    | ""                    | "Contents/Home/bin/java" | ""
+        "linux"   | "aarch64" | VENDOR_ADOPTIUM | ADOPT_JDK_VERSION    | ""                    | "bin/java"               | ""
+        "linux"   | "aarch64" | VENDOR_ADOPTIUM | ADOPT_JDK_VERSION_11 | ""                    | "bin/java"               | "(jdk 11)"
+        "linux"   | "aarch64" | VENDOR_ADOPTIUM | ADOPT_JDK_VERSION_15 | ""                    | "bin/java"               | "(jdk 15)"
         "darwin"  | "aarch64" | VENDOR_ZULU     | AZUL_JDK_VERSION_8   | AZUL_8_DISTRO_VERSION | "Contents/Home/bin/java" | "(jdk 8)"
     }
 
@@ -214,13 +224,19 @@ class JdkDownloadPluginFuncTest extends AbstractGradleFuncTest {
     private static String urlPath(final String vendor,
                                   final String version,
                                   final String platform,
-                                  final String arch = 'x64') {
-        if (vendor.equals(VENDOR_ADOPTIUM)) {
+                                  final String arch = 'x64',
+                                  final String distributedVersion = '') {
+        final boolean isOld = version.equals(OPENJDK_VERSION_OLD);
+
+        if (distributedVersion.equals("ea")) {
+            def effectivePlatform = isMac(platform) ? "macos" : platform;
+            def fileExtension = platform.toLowerCase().equals("windows") ? "zip" : "tar.gz";
+            return "/jdks/openjdk/25/openjdk-${version}/openjdk-${version}_$effectivePlatform-${arch}_bin.$fileExtension";
+        } else if (vendor.equals(VENDOR_ADOPTIUM)) {
             final String module = isMac(platform) ? "mac" : platform;
             return "/jdk-" + version + "/" + module + "/${arch}/jdk/hotspot/normal/adoptium";
         } else if (vendor.equals(VENDOR_OPENJDK)) {
             final String effectivePlatform = isMac(platform) ? "macos" : platform;
-            final boolean isOld = version.equals(OPENJDK_VERSION_OLD);
             final String versionPath = isOld ? "jdk1/99" : "jdk12.0.1/123456789123456789123456789abcde/99";
             final String filename = "openjdk-" + (isOld ? "1" : "12.0.1") + "_" + effectivePlatform + "-x64_bin." + extension(platform);
             return "/java/GA/" + versionPath + "/GPL/" + filename;

+ 15 - 4
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/Jdk.java

@@ -32,6 +32,7 @@ public class Jdk implements Buildable, Iterable<File> {
         "(\\d+)(\\.\\d+\\.\\d+(?:\\.\\d+)?)?\\+(\\d+(?:\\.\\d+)?)(@([a-f0-9]{32}))?"
     );
     private static final Pattern LEGACY_VERSION_PATTERN = Pattern.compile("(\\d)(u\\d+)\\+(b\\d+?)(@([a-f0-9]{32}))?");
+    private static final Pattern EA_VERSION_PATTERN = Pattern.compile("(\\d+)-ea\\+(\\d+)(@([a-f0-9]{32}))?");
 
     private final String name;
     private final FileCollection configuration;
@@ -78,7 +79,9 @@ public class Jdk implements Buildable, Iterable<File> {
     }
 
     public void setVersion(String version) {
-        if (VERSION_PATTERN.matcher(version).matches() == false && LEGACY_VERSION_PATTERN.matcher(version).matches() == false) {
+        if (VERSION_PATTERN.matcher(version).matches() == false
+            && LEGACY_VERSION_PATTERN.matcher(version).matches() == false
+            && EA_VERSION_PATTERN.matcher(version).matches() == false) {
             throw new IllegalArgumentException("malformed version [" + version + "] for jdk [" + name + "]");
         }
         parseVersion(version);
@@ -112,7 +115,7 @@ public class Jdk implements Buildable, Iterable<File> {
     }
 
     public String getDistributionVersion() {
-        return distributionVersion.get();
+        return distributionVersion.getOrNull();
     }
 
     public void setDistributionVersion(String distributionVersion) {
@@ -218,9 +221,17 @@ public class Jdk implements Buildable, Iterable<File> {
         if (jdkVersionMatcher.matches() == false) {
             // Try again with the pre-Java9 version format
             jdkVersionMatcher = LEGACY_VERSION_PATTERN.matcher(version);
-
             if (jdkVersionMatcher.matches() == false) {
-                throw new IllegalArgumentException("Malformed jdk version [" + version + "]");
+                // Try again with the pre-Java9 version format
+                jdkVersionMatcher = EA_VERSION_PATTERN.matcher(version);
+                if (jdkVersionMatcher.matches() == false) {
+                    throw new IllegalArgumentException("Malformed jdk version [" + version + "]");
+                }
+                baseVersion = version;
+                major = jdkVersionMatcher.group(1);
+                build = jdkVersionMatcher.group(2);
+                hash = null;
+                return;
             }
         }
 

+ 9 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/JdkDownloadPlugin.java

@@ -114,6 +114,15 @@ public class JdkDownloadPlugin implements Plugin<Project> {
                     + jdk.getBuild()
                     + "/[module]/[classifier]/jdk/hotspot/normal/adoptium";
             }
+        } else if (jdk.getVendor().equals(VENDOR_OPENJDK) && "ea".equals(jdk.getDistributionVersion())) {
+            repoUrl = "https://builds.es-jdk-archive.com/";
+            // current pattern since 12.0.1
+            artifactPattern = "jdks/openjdk/"
+                + jdk.getMajor()
+                + "/openjdk-"
+                + jdk.getBaseVersion()
+                + "/"
+                + "openjdk-[revision]_[module]-[classifier]_bin.[ext]";
         } else if (jdk.getVendor().equals(VENDOR_OPENJDK)) {
             repoUrl = "https://download.oracle.com";
             if (jdk.getHash() != null) {

+ 36 - 2
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java

@@ -12,8 +12,12 @@ import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 import org.apache.commons.io.IOUtils;
+import org.elasticsearch.gradle.Architecture;
+import org.elasticsearch.gradle.OS;
 import org.elasticsearch.gradle.VersionProperties;
 import org.elasticsearch.gradle.internal.BwcVersions;
+import org.elasticsearch.gradle.internal.Jdk;
+import org.elasticsearch.gradle.internal.JdkDownloadPlugin;
 import org.elasticsearch.gradle.internal.conventions.GitInfoPlugin;
 import org.elasticsearch.gradle.internal.conventions.info.GitInfo;
 import org.elasticsearch.gradle.internal.conventions.info.ParallelDetector;
@@ -22,6 +26,7 @@ import org.elasticsearch.gradle.util.GradleUtils;
 import org.gradle.api.Action;
 import org.gradle.api.GradleException;
 import org.gradle.api.JavaVersion;
+import org.gradle.api.NamedDomainObjectContainer;
 import org.gradle.api.Plugin;
 import org.gradle.api.Project;
 import org.gradle.api.logging.Logger;
@@ -63,6 +68,7 @@ import java.util.stream.Stream;
 import javax.inject.Inject;
 
 import static org.elasticsearch.gradle.internal.conventions.GUtils.elvis;
+import static org.elasticsearch.gradle.internal.toolchain.EarlyAccessCatalogJdkToolchainResolver.findLatestEABuildNumber;
 
 public class GlobalBuildInfoPlugin implements Plugin<Project> {
     private static final Logger LOGGER = Logging.getLogger(GlobalBuildInfoPlugin.class);
@@ -92,11 +98,13 @@ public class GlobalBuildInfoPlugin implements Plugin<Project> {
 
     @Override
     public void apply(Project project) {
+
         if (project != project.getRootProject()) {
             throw new IllegalStateException(this.getClass().getName() + " can only be applied to the root project.");
         }
         this.project = project;
         project.getPlugins().apply(JvmToolchainsPlugin.class);
+        project.getPlugins().apply(JdkDownloadPlugin.class);
         Provider<GitInfo> gitInfo = project.getPlugins().apply(GitInfoPlugin.class).getGitInfo();
 
         toolChainService = project.getExtensions().getByType(JavaToolchainService.class);
@@ -270,7 +278,9 @@ public class GlobalBuildInfoPlugin implements Plugin<Project> {
     private InstallationLocation getJavaInstallation(File javaHome) {
         return getAvailableJavaInstallationLocationSteam().filter(installationLocation -> isSameFile(javaHome, installationLocation))
             .findFirst()
-            .orElseThrow(() -> new GradleException("Could not locate available Java installation in Gradle registry at: " + javaHome));
+            .orElse(
+                InstallationLocation.userDefined(javaHome, "Manually resolved JavaHome (not auto-detected by Gradle toolchain service)")
+            );
     }
 
     private boolean isSameFile(File javaHome, InstallationLocation installationLocation) {
@@ -338,7 +348,14 @@ public class GlobalBuildInfoPlugin implements Plugin<Project> {
         String runtimeJavaProperty = System.getProperty("runtime.java");
 
         if (runtimeJavaProperty != null) {
-            return resolveJavaHomeFromToolChainService(runtimeJavaProperty);
+            if (runtimeJavaProperty.toLowerCase().endsWith("-ea")) {
+                // handle EA builds differently due to lack of support in Gradle toolchain service
+                // we resolve them using JdkDownloadPlugin for now.
+                Integer major = Integer.parseInt(runtimeJavaProperty.substring(0, runtimeJavaProperty.length() - 3));
+                return resolveEarlyAccessJavaHome(major);
+            } else {
+                return resolveJavaHomeFromToolChainService(runtimeJavaProperty);
+            }
         }
         if (System.getenv("RUNTIME_JAVA_HOME") != null) {
             return providers.provider(() -> new File(System.getenv("RUNTIME_JAVA_HOME")));
@@ -353,6 +370,23 @@ public class GlobalBuildInfoPlugin implements Plugin<Project> {
         });
     }
 
+    private Provider<File> resolveEarlyAccessJavaHome(Integer runtimeJavaProperty) {
+        NamedDomainObjectContainer<Jdk> container = (NamedDomainObjectContainer<Jdk>) project.getExtensions().getByName("jdks");
+        Integer buildNumber = Integer.getInteger("runtime.java.build");
+        if (buildNumber == null) {
+            buildNumber = findLatestEABuildNumber(runtimeJavaProperty);
+        }
+        String eaVersionString = String.format("%d-ea+%d", runtimeJavaProperty, buildNumber);
+        Jdk jdk = container.create("ea_jdk_" + runtimeJavaProperty, j -> {
+            j.setVersion(eaVersionString);
+            j.setVendor("openjdk");
+            j.setPlatform(OS.current().javaOsReference);
+            j.setArchitecture(Architecture.current().javaClassifier);
+            j.setDistributionVersion("ea");
+        });
+        return providers.provider(() -> new File(jdk.getJavaHomePath().toString()));
+    }
+
     @NotNull
     private Provider<File> resolveJavaHomeFromToolChainService(String version) {
         Property<JavaLanguageVersion> value = objectFactory.property(JavaLanguageVersion.class).value(JavaLanguageVersion.of(version));

+ 160 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/toolchain/EarlyAccessCatalogJdkToolchainResolver.java

@@ -0,0 +1,160 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.gradle.internal.toolchain;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+
+import org.elasticsearch.gradle.VersionProperties;
+import org.gradle.jvm.toolchain.JavaLanguageVersion;
+import org.gradle.jvm.toolchain.JavaToolchainDownload;
+import org.gradle.jvm.toolchain.JavaToolchainRequest;
+import org.gradle.jvm.toolchain.JavaToolchainSpec;
+import org.gradle.platform.Architecture;
+import org.gradle.platform.BuildPlatform;
+import org.gradle.platform.OperatingSystem;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * A toolchain resolver that resolves early access JDKs from the Elasticsearch JDK archive.
+ * <p>
+ * This resolver can used to resolve JDKs that are not bundled with Elasticsearch but are available in the early access catalog.
+ * It supports resolving JDKs based on their language version and build number.
+ *
+ * Currently the gradle toolchain support does not support querying specific versions (e.g. 26-ea+6) so. For now
+ * this only supports resolving the latest early access build for a given language version.
+ * <p>
+ */
+public abstract class EarlyAccessCatalogJdkToolchainResolver extends AbstractCustomJavaToolchainResolver {
+
+    interface JdkBuild {
+        JavaLanguageVersion languageVersion();
+
+        String url(String os, String arch, String extension);
+    }
+
+    @FunctionalInterface
+    interface EarlyAccessJdkBuildResolver {
+        Optional<EarlyAccessJdkBuild> findLatestEABuild(JavaLanguageVersion languageVersion);
+    }
+
+    // allow overriding for testing
+    EarlyAccessJdkBuildResolver earlyAccessJdkBuildResolver = (languageVersion) -> findLatestEABuild(languageVersion);
+
+    record EarlyAccessJdkBuild(JavaLanguageVersion languageVersion, int buildNumber) implements JdkBuild {
+        @Override
+        public String url(String os, String arch, String extension) {
+            // example:
+            // https://builds.es-jdk-archive.com/jdks/openjdk/26/openjdk-26-ea+6/openjdk-26-ea+6_linux-aarch64_bin.tar.gz
+            return "https://builds.es-jdk-archive.com/jdks/openjdk/"
+                + languageVersion.asInt()
+                + "/"
+                + "openjdk-"
+                + languageVersion.asInt()
+                + "-ea+"
+                + buildNumber
+                + "/"
+                + "openjdk-"
+                + languageVersion.asInt()
+                + "-ea+"
+                + buildNumber
+                + "_"
+                + os
+                + "-"
+                + arch
+                + "_bin."
+                + extension;
+        }
+    }
+
+    private static final List<OperatingSystem> supportedOperatingSystems = List.of(
+        OperatingSystem.MAC_OS,
+        OperatingSystem.LINUX,
+        OperatingSystem.WINDOWS
+    );
+
+    /**
+     * We need some place to map JavaLanguageVersion to buildNumber, minor version etc.
+     * */
+    @Override
+    public Optional<JavaToolchainDownload> resolve(JavaToolchainRequest request) {
+        if (Integer.parseInt(VersionProperties.getBundledJdkMajorVersion()) >= request.getJavaToolchainSpec()
+            .getLanguageVersion()
+            .get()
+            .asInt()) {
+            // This resolver only handles early access builds, that are beyond the last bundled jdk version
+        }
+        return findSupportedBuild(request).map(build -> {
+            OperatingSystem operatingSystem = request.getBuildPlatform().getOperatingSystem();
+            String extension = operatingSystem.equals(OperatingSystem.WINDOWS) ? "zip" : "tar.gz";
+            String arch = toArchString(request.getBuildPlatform().getArchitecture());
+            String os = toOsString(operatingSystem);
+            return (JavaToolchainDownload) () -> URI.create(build.url(os, arch, extension));
+        });
+    }
+
+    /**
+     * Check if request can be full-filled by this resolver:
+     * 1. Aarch64 windows images are not supported
+     */
+    private Optional<EarlyAccessJdkBuild> findSupportedBuild(JavaToolchainRequest request) {
+        JavaToolchainSpec javaToolchainSpec = request.getJavaToolchainSpec();
+        BuildPlatform buildPlatform = request.getBuildPlatform();
+        Architecture architecture = buildPlatform.getArchitecture();
+        OperatingSystem operatingSystem = buildPlatform.getOperatingSystem();
+
+        if (supportedOperatingSystems.contains(operatingSystem) == false
+            || Architecture.AARCH64 == architecture && OperatingSystem.WINDOWS == operatingSystem) {
+            return Optional.empty();
+        }
+
+        JavaLanguageVersion languageVersion = javaToolchainSpec.getLanguageVersion().get();
+        return earlyAccessJdkBuildResolver.findLatestEABuild(languageVersion);
+    }
+
+    private static Optional<EarlyAccessJdkBuild> findLatestEABuild(JavaLanguageVersion languageVersion) {
+        try {
+            URL url = new URL("https://storage.googleapis.com/elasticsearch-jdk-archive/jdks/openjdk/latest.json");
+            try (InputStream is = url.openStream()) {
+                ObjectMapper mapper = new ObjectMapper();
+                JsonNode node = mapper.readTree(is);
+                ArrayNode buildsNode = (ArrayNode) node.get("builds");
+                List<JsonNode> buildsList = new ArrayList<>();
+                buildsNode.forEach(buildsList::add);
+                List<EarlyAccessJdkBuild> eaBuilds = buildsList.stream()
+                    .map(
+                        n -> new EarlyAccessJdkBuild(
+                            JavaLanguageVersion.of(n.get("major").asText()),
+                            Integer.parseInt(n.get("build").asText())
+                        )
+                    )
+                    .toList();
+                return eaBuilds.stream().filter(ea -> ea.languageVersion().equals(languageVersion)).findFirst();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        } catch (MalformedURLException e) {
+            return Optional.empty();
+        }
+    }
+
+    public static int findLatestEABuildNumber(int languageVersion) {
+        return findLatestEABuild(JavaLanguageVersion.of(languageVersion)).map(ea -> ea.buildNumber()).get();
+    }
+}

+ 1 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/toolchain/JavaToolChainResolverPlugin.java

@@ -23,6 +23,7 @@ public abstract class JavaToolChainResolverPlugin implements Plugin<Settings> {
         settings.getPlugins().apply("jvm-toolchain-management");
         JavaToolchainResolverRegistry registry = getToolchainResolverRegistry();
         registry.register(OracleOpenJdkToolchainResolver.class);
+        registry.register(EarlyAccessCatalogJdkToolchainResolver.class);
         registry.register(AdoptiumJdkToolchainResolver.class);
         registry.register(ArchivedOracleJdkToolchainResolver.class);
     }

+ 1 - 36
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/toolchain/OracleOpenJdkToolchainResolver.java

@@ -58,32 +58,6 @@ public abstract class OracleOpenJdkToolchainResolver extends AbstractCustomJavaT
         }
     }
 
-    record EarlyAccessJdkBuild(JavaLanguageVersion languageVersion, String buildNumber) implements JdkBuild {
-        @Override
-        public String url(String os, String arch, String extension) {
-            // example:
-            // https://builds.es-jdk-archive.com/jdks/openjdk/26/openjdk-26-ea+6/openjdk-26-ea+6_linux-aarch64_bin.tar.gz
-            return "https://builds.es-jdk-archive.com/jdks/openjdk/"
-                + languageVersion.asInt()
-                + "/"
-                + "openjdk-"
-                + languageVersion.asInt()
-                + "-ea+"
-                + buildNumber
-                + "/"
-                + "openjdk-"
-                + languageVersion.asInt()
-                + "-ea+"
-                + buildNumber
-                + "_"
-                + os
-                + "-"
-                + arch
-                + "_bin."
-                + extension;
-        }
-    }
-
     private static final Pattern VERSION_PATTERN = Pattern.compile(
         "(\\d+)(\\.\\d+\\.\\d+(?:\\.\\d+)?)?\\+(\\d+(?:\\.\\d+)?)(@([a-f0-9]{32}))?"
     );
@@ -96,18 +70,9 @@ public abstract class OracleOpenJdkToolchainResolver extends AbstractCustomJavaT
 
     // package private so it can be replaced by tests
     List<JdkBuild> builds = List.of(
-        getBundledJdkBuild(VersionProperties.getBundledJdkVersion(), VersionProperties.getBundledJdkMajorVersion()),
-        getEarlyAccessBuild(JavaLanguageVersion.of(25), "3")
+        getBundledJdkBuild(VersionProperties.getBundledJdkVersion(), VersionProperties.getBundledJdkMajorVersion())
     );
 
-    static EarlyAccessJdkBuild getEarlyAccessBuild(JavaLanguageVersion languageVersion, String buildNumber) {
-        // first try the unversioned override, then the versioned override which has higher precedence
-        buildNumber = System.getProperty("runtime.java.build", buildNumber);
-        buildNumber = System.getProperty("runtime.java." + languageVersion.asInt() + ".build", buildNumber);
-
-        return new EarlyAccessJdkBuild(languageVersion, buildNumber);
-    }
-
     static JdkBuild getBundledJdkBuild(String bundledJdkVersion, String bundledJkdMajorVersionString) {
         JavaLanguageVersion bundledJdkMajorVersion = JavaLanguageVersion.of(bundledJkdMajorVersionString);
         Matcher jdkVersionMatcher = VERSION_PATTERN.matcher(bundledJdkVersion);

+ 46 - 0
build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/JdkSpec.groovy

@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.gradle.internal
+
+import spock.lang.Specification
+
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.Property
+
+class JdkSpec extends Specification {
+
+    def "jdk version is parsed correctly"() {
+        given:
+        Jdk jdk = newJdk()
+
+        when:
+        jdk.setVersion(version)
+        then:
+        jdk.getBaseVersion() == baseVersion
+        jdk.getBuild() == buildNumber
+
+        where:
+        version    | baseVersion | major | buildNumber
+        "25-ea+30" | "25-ea+30"  | "25"  | "30"
+        "26-ea+6"  | "26-ea+6"   | "26"  | "6"
+    }
+
+    Object newJdk(String name = "jdk") {
+        Configuration configuration = Mock()
+        _ * configuration.getName() >> name + "Config"
+
+        ObjectFactory objectFactory = Mock()
+        Property<String> stringProperty = Mock()
+        _ * objectFactory.property(String.class) >> stringProperty
+
+        return new Jdk(name, configuration, objectFactory)
+    }
+}

+ 1 - 0
build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/toolchain/AbstractToolchainResolverSpec.groovy

@@ -73,6 +73,7 @@ abstract class AbstractToolchainResolverSpec extends Specification {
         _ * languageVersionProperty.get() >> languageVersion
 
         Property<JvmVendorSpec> vendorSpecProperty = Mock()
+        _ * vendorSpecProperty.isPresent() >> true
         _ * vendorSpecProperty.get() >> vendorSpec
         _ * toolchainSpec.getVendor() >> vendorSpecProperty
 

+ 1 - 1
build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/toolchain/AdoptiumJdkToolchainResolverSpec.groovy

@@ -47,7 +47,7 @@ class AdoptiumJdkToolchainResolverSpec extends AbstractToolchainResolverSpec {
     @Override
     def supportedRequests() {
         return [
-                [19, ADOPTIUM, MAC_OS, X86_64, "https://api.adoptium.net/v3/binary/version/jdk-19.1.1.1+37.1/mac/x64/jdk/hotspot/normal/eclipse?project=jdk"],
+
                 [19, ADOPTIUM, LINUX, X86_64, "https://api.adoptium.net/v3/binary/version/jdk-19.1.1.1+37.1/linux/x64/jdk/hotspot/normal/eclipse?project=jdk"],
                 [19, ADOPTIUM, WINDOWS, X86_64, "https://api.adoptium.net/v3/binary/version/jdk-19.1.1.1+37.1/windows/x64/jdk/hotspot/normal/eclipse?project=jdk"],
                 [19, ADOPTIUM, MAC_OS, AARCH64, "https://api.adoptium.net/v3/binary/version/jdk-19.1.1.1+37.1/mac/aarch64/jdk/hotspot/normal/eclipse?project=jdk"],

+ 62 - 0
build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/toolchain/EarlyAccessCatalogJdkToolchainResolverSpec.groovy

@@ -0,0 +1,62 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.gradle.internal.toolchain
+
+import org.elasticsearch.gradle.VersionProperties
+import org.gradle.api.services.BuildServiceParameters
+import org.gradle.jvm.toolchain.JavaLanguageVersion
+import org.gradle.jvm.toolchain.JavaToolchainResolver
+import org.gradle.jvm.toolchain.JvmVendorSpec
+
+import static org.gradle.platform.Architecture.AARCH64
+import static org.gradle.platform.Architecture.X86_64
+import static org.gradle.platform.OperatingSystem.*
+
+class EarlyAccessCatalogJdkToolchainResolverSpec extends AbstractToolchainResolverSpec {
+    @Override
+    JavaToolchainResolver resolverImplementation() {
+        def resolver = new EarlyAccessCatalogJdkToolchainResolver() {
+            @Override
+            BuildServiceParameters.None getParameters() {
+                return null
+            }
+        }
+        resolver.earlyAccessJdkBuildResolver = (JavaLanguageVersion version) -> {
+            return Optional.of(
+                new EarlyAccessCatalogJdkToolchainResolver.EarlyAccessJdkBuild(version, 30)
+            )
+        }
+        return resolver
+    }
+
+    @Override
+    def supportedRequests() {
+        return [
+            [25, anyVendor(), LINUX, X86_64, "https://builds.es-jdk-archive.com/jdks/openjdk/25/openjdk-25-ea+30/openjdk-25-ea+30_linux-x64_bin.tar.gz"],
+            [25, anyVendor(), LINUX, AARCH64, "https://builds.es-jdk-archive.com/jdks/openjdk/25/openjdk-25-ea+30/openjdk-25-ea+30_linux-aarch64_bin.tar.gz"],
+            [25, anyVendor(), MAC_OS, X86_64, "https://builds.es-jdk-archive.com/jdks/openjdk/25/openjdk-25-ea+30/openjdk-25-ea+30_macos-x64_bin.tar.gz"],
+            [25, anyVendor(), MAC_OS, AARCH64, "https://builds.es-jdk-archive.com/jdks/openjdk/25/openjdk-25-ea+30/openjdk-25-ea+30_macos-aarch64_bin.tar.gz"],
+            [25, anyVendor(), WINDOWS, X86_64, "https://builds.es-jdk-archive.com/jdks/openjdk/25/openjdk-25-ea+30/openjdk-25-ea+30_windows-x64_bin.zip"],
+
+            [26, anyVendor(), LINUX, X86_64, "https://builds.es-jdk-archive.com/jdks/openjdk/26/openjdk-26-ea+30/openjdk-26-ea+30_linux-x64_bin.tar.gz"],
+            [26, anyVendor(), LINUX, AARCH64, "https://builds.es-jdk-archive.com/jdks/openjdk/26/openjdk-26-ea+30/openjdk-26-ea+30_linux-aarch64_bin.tar.gz"],
+            [26, anyVendor(), MAC_OS, X86_64, "https://builds.es-jdk-archive.com/jdks/openjdk/26/openjdk-26-ea+30/openjdk-26-ea+30_macos-x64_bin.tar.gz"],
+            [26, anyVendor(), MAC_OS, AARCH64, "https://builds.es-jdk-archive.com/jdks/openjdk/26/openjdk-26-ea+30/openjdk-26-ea+30_macos-aarch64_bin.tar.gz"],
+            [26, anyVendor(), WINDOWS, X86_64, "https://builds.es-jdk-archive.com/jdks/openjdk/26/openjdk-26-ea+30/openjdk-26-ea+30_windows-x64_bin.zip"]
+        ]
+    }
+
+    @Override
+    def unsupportedRequests() {
+        [
+            [Integer.parseInt(VersionProperties.bundledJdkMajorVersion) + 1, anyVendor(), WINDOWS, AARCH64],
+        ]
+    }
+}

+ 2 - 49
build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/toolchain/OracleOpenJdkToolchainResolverSpec.groovy

@@ -9,11 +9,9 @@
 
 package org.elasticsearch.gradle.internal.toolchain
 
-import spock.util.environment.RestoreSystemProperties
 
 import org.gradle.api.services.BuildServiceParameters
 import org.gradle.jvm.toolchain.JavaLanguageVersion
-import org.gradle.jvm.toolchain.JavaToolchainDownload
 
 import static org.gradle.jvm.toolchain.JvmVendorSpec.ORACLE
 import static org.gradle.platform.Architecture.AARCH64
@@ -22,7 +20,6 @@ import static org.gradle.platform.OperatingSystem.*
 
 class OracleOpenJdkToolchainResolverSpec extends AbstractToolchainResolverSpec {
 
-
     OracleOpenJdkToolchainResolver resolverImplementation() {
         var toolChain = new OracleOpenJdkToolchainResolver() {
             @Override
@@ -39,7 +36,6 @@ class OracleOpenJdkToolchainResolverSpec extends AbstractToolchainResolverSpec {
                 "bdc68b4b9cbc4ebcb30745c85038d91d"
             ),
             OracleOpenJdkToolchainResolver.getBundledJdkBuild("24+36@1f9ff9062db4449d8ca828c504ffae90", "24"),
-            OracleOpenJdkToolchainResolver.getEarlyAccessBuild(JavaLanguageVersion.of(25), "3")
         ]
         toolChain
     }
@@ -65,51 +61,8 @@ class OracleOpenJdkToolchainResolverSpec extends AbstractToolchainResolverSpec {
          [24, anyVendor(), MAC_OS, AARCH64, "https://download.oracle.com/java/GA/jdk24/1f9ff9062db4449d8ca828c504ffae90/36/GPL/openjdk-24_macos-aarch64_bin.tar.gz"],
          [24, anyVendor(), LINUX, X86_64, "https://download.oracle.com/java/GA/jdk24/1f9ff9062db4449d8ca828c504ffae90/36/GPL/openjdk-24_linux-x64_bin.tar.gz"],
          [24, anyVendor(), LINUX, AARCH64, "https://download.oracle.com/java/GA/jdk24/1f9ff9062db4449d8ca828c504ffae90/36/GPL/openjdk-24_linux-aarch64_bin.tar.gz"],
-         [24, anyVendor(), WINDOWS, X86_64, "https://download.oracle.com/java/GA/jdk24/1f9ff9062db4449d8ca828c504ffae90/36/GPL/openjdk-24_windows-x64_bin.zip"],
-         // EA build
-         [25, ORACLE, MAC_OS, X86_64, "https://builds.es-jdk-archive.com/jdks/openjdk/25/openjdk-25-ea+3/openjdk-25-ea+3_macos-x64_bin.tar.gz"],
-         [25, ORACLE, MAC_OS, AARCH64, "https://builds.es-jdk-archive.com/jdks/openjdk/25/openjdk-25-ea+3/openjdk-25-ea+3_macos-aarch64_bin.tar.gz"],
-         [25, ORACLE, LINUX, X86_64, "https://builds.es-jdk-archive.com/jdks/openjdk/25/openjdk-25-ea+3/openjdk-25-ea+3_linux-x64_bin.tar.gz"],
-         [25, ORACLE, LINUX, AARCH64, "https://builds.es-jdk-archive.com/jdks/openjdk/25/openjdk-25-ea+3/openjdk-25-ea+3_linux-aarch64_bin.tar.gz"],
-         [25, ORACLE, WINDOWS, X86_64, "https://builds.es-jdk-archive.com/jdks/openjdk/25/openjdk-25-ea+3/openjdk-25-ea+3_windows-x64_bin.zip"],
-         [25, anyVendor(), MAC_OS, X86_64, "https://builds.es-jdk-archive.com/jdks/openjdk/25/openjdk-25-ea+3/openjdk-25-ea+3_macos-x64_bin.tar.gz"],
-         [25, anyVendor(), MAC_OS, AARCH64, "https://builds.es-jdk-archive.com/jdks/openjdk/25/openjdk-25-ea+3/openjdk-25-ea+3_macos-aarch64_bin.tar.gz"],
-         [25, anyVendor(), LINUX, X86_64, "https://builds.es-jdk-archive.com/jdks/openjdk/25/openjdk-25-ea+3/openjdk-25-ea+3_linux-x64_bin.tar.gz"],
-         [25, anyVendor(), LINUX, AARCH64, "https://builds.es-jdk-archive.com/jdks/openjdk/25/openjdk-25-ea+3/openjdk-25-ea+3_linux-aarch64_bin.tar.gz"],
-         [25, anyVendor(), WINDOWS, X86_64, "https://builds.es-jdk-archive.com/jdks/openjdk/25/openjdk-25-ea+3/openjdk-25-ea+3_windows-x64_bin.zip"]]
-    }
-
-    @RestoreSystemProperties
-    def "can provide build number for ea versions"() {
-        given:
-        System.setProperty('runtime.java.build', "42")
-        System.setProperty('runtime.java.25.build', "13")
-        def resolver = resolverImplementation()
-
-        when:
-        Optional<JavaToolchainDownload> download = resolver.resolve(
-            request(
-                JavaLanguageVersion.of(version),
-                vendor,
-                platform(os, arch)
-            )
-        )
-
-        then:
-        download.get().uri == URI.create(expectedUrl)
-
-        where:
-        version | vendor      | os      | arch    | expectedUrl
-        25      | ORACLE      | MAC_OS  | X86_64  | urlPrefix(25) + "openjdk-25-ea+13/openjdk-25-ea+13_macos-x64_bin.tar.gz"
-        25      | ORACLE      | MAC_OS  | AARCH64 | urlPrefix(25) + "openjdk-25-ea+13/openjdk-25-ea+13_macos-aarch64_bin.tar.gz"
-        25      | ORACLE      | LINUX   | X86_64  | urlPrefix(25) + "openjdk-25-ea+13/openjdk-25-ea+13_linux-x64_bin.tar.gz"
-        25      | ORACLE      | LINUX   | AARCH64 | urlPrefix(25) + "openjdk-25-ea+13/openjdk-25-ea+13_linux-aarch64_bin.tar.gz"
-        25      | ORACLE      | WINDOWS | X86_64  | urlPrefix(25) + "openjdk-25-ea+13/openjdk-25-ea+13_windows-x64_bin.zip"
-        25      | anyVendor() | MAC_OS  | X86_64  | urlPrefix(25) + "openjdk-25-ea+13/openjdk-25-ea+13_macos-x64_bin.tar.gz"
-        25      | anyVendor() | MAC_OS  | AARCH64 | urlPrefix(25) + "openjdk-25-ea+13/openjdk-25-ea+13_macos-aarch64_bin.tar.gz"
-        25      | anyVendor() | LINUX   | X86_64  | urlPrefix(25) + "openjdk-25-ea+13/openjdk-25-ea+13_linux-x64_bin.tar.gz"
-        25      | anyVendor() | LINUX   | AARCH64 | urlPrefix(25) + "openjdk-25-ea+13/openjdk-25-ea+13_linux-aarch64_bin.tar.gz"
-        25      | anyVendor() | WINDOWS | X86_64  | urlPrefix(25) + "openjdk-25-ea+13/openjdk-25-ea+13_windows-x64_bin.zip"
+         [24, anyVendor(), WINDOWS, X86_64, "https://download.oracle.com/java/GA/jdk24/1f9ff9062db4449d8ca828c504ffae90/36/GPL/openjdk-24_windows-x64_bin.zip"]
+        ]
     }
 
     private static String urlPrefix(int i) {

+ 5 - 3
build-tools/src/main/java/org/elasticsearch/gradle/Architecture.java

@@ -11,17 +11,19 @@ package org.elasticsearch.gradle;
 
 public enum Architecture {
 
-    X64("x86_64", "linux/amd64", "amd64"),
-    AARCH64("aarch64", "linux/arm64", "arm64");
+    X64("x86_64", "linux/amd64", "amd64", "x64"),
+    AARCH64("aarch64", "linux/arm64", "arm64", "aarch64");
 
     public final String classifier;
     public final String dockerPlatform;
     public final String dockerClassifier;
+    public final String javaClassifier;
 
-    Architecture(String classifier, String dockerPlatform, String dockerClassifier) {
+    Architecture(String classifier, String dockerPlatform, String dockerClassifier, String javaClassifier) {
         this.classifier = classifier;
         this.dockerPlatform = dockerPlatform;
         this.dockerClassifier = dockerClassifier;
+        this.javaClassifier = javaClassifier;
     }
 
     public static Architecture current() {

+ 6 - 4
build-tools/src/main/java/org/elasticsearch/gradle/DistributionDownloadPlugin.java

@@ -191,10 +191,12 @@ public class DistributionDownloadPlugin implements Plugin<Project> {
             repo.metadataSources(IvyArtifactRepository.MetadataSources::artifact);
             repo.patternLayout(layout -> layout.artifact("/downloads/elasticsearch/[module]-[revision](-[classifier]).[ext]"));
         });
-        project.getRepositories().exclusiveContent(exclusiveContentRepository -> {
-            exclusiveContentRepository.filter(config -> config.includeGroup(group));
-            exclusiveContentRepository.forRepositories(ivyRepo);
-        });
+        if (project != project.getRootProject()) {
+            project.getRepositories().exclusiveContent(exclusiveContentRepository -> {
+                exclusiveContentRepository.filter(config -> config.includeGroup(group));
+                exclusiveContentRepository.forRepositories(ivyRepo);
+            });
+        }
     }
 
     private static void setupDownloadServiceRepo(Project project) {

+ 10 - 3
build-tools/src/main/java/org/elasticsearch/gradle/OS.java

@@ -15,9 +15,16 @@ import java.util.Set;
 import java.util.function.Supplier;
 
 public enum OS {
-    WINDOWS,
-    MAC,
-    LINUX;
+    WINDOWS("windows"),
+    MAC("darwin"),
+    LINUX("linux");
+
+    public final String javaOsReference;
+
+    OS(String javaOsReference) {
+        // This constructor is intentionally empty, but it can be used to set up any necessary state.
+        this.javaOsReference = javaOsReference;
+    }
 
     public static OS current() {
         String os = System.getProperty("os.name", "");

+ 2 - 2
build.gradle

@@ -35,11 +35,11 @@ buildscript {
 plugins {
   id 'lifecycle-base'
   id 'elasticsearch.docker-support'
+  id 'elasticsearch.internal-distribution-download'
+  id 'elasticsearch.jdk-download'
   id 'elasticsearch.global-build-info'
   id 'elasticsearch.build-complete'
   id 'elasticsearch.build-scan'
-  id 'elasticsearch.jdk-download'
-  id 'elasticsearch.internal-distribution-download'
   id 'elasticsearch.runtime-jdk-provision'
   id 'elasticsearch.ide'
   id 'elasticsearch.forbidden-dependencies'

+ 1 - 0
gradle/verification-metadata.xml

@@ -9,6 +9,7 @@
          <trust group="beats" name="metricbeat"/>
          <trust group="beats" name="metricbeat-fips"/>
          <trust group="elasticsearch-distribution" name="elasticsearch"/>
+         <trust group="openjdk_.*" regex="true"/>
          <trust group="org.elasticsearch"/>
          <trust group="org.elasticsearch.distribution.zip" name="elasticsearch"/>
          <trust group="org.elasticsearch.ml"/>

+ 1 - 0
settings.gradle

@@ -1,6 +1,7 @@
 import org.elasticsearch.gradle.internal.toolchain.OracleOpenJdkToolchainResolver
 import org.elasticsearch.gradle.internal.toolchain.ArchivedOracleJdkToolchainResolver
 import org.elasticsearch.gradle.internal.toolchain.AdoptiumJdkToolchainResolver
+import org.elasticsearch.gradle.internal.toolchain.EarlyAccessCatalogJdkToolchainResolver
 
 pluginManagement {
   repositories {

+ 3 - 1
test/framework/src/main/java/org/elasticsearch/test/junit/listeners/ReproduceInfoPrinter.java

@@ -190,9 +190,11 @@ public class ReproduceInfoPrinter extends RunListener {
             appendOpt("tests.locale", Locale.getDefault().toLanguageTag());
             appendOpt("tests.timezone", TimeZone.getDefault().getID());
             appendOpt("tests.distribution", System.getProperty("tests.distribution"));
-            appendOpt("runtime.java", Integer.toString(Runtime.version().feature()));
             if (Runtime.version().build().isPresent() && "ea".equalsIgnoreCase(Runtime.version().pre().orElse(""))) {
+                appendOpt("runtime.java", Runtime.version().feature() + "-ea");
                 appendOpt("runtime.java.build", Integer.toString(Runtime.version().build().get()));
+            } else {
+                appendOpt("runtime.java", Integer.toString(Runtime.version().feature()));
             }
             appendOpt(ESTestCase.FIPS_SYSPROP, System.getProperty(ESTestCase.FIPS_SYSPROP));
             return this;