Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
56b2b6c
Native CLI launcher
ldematte Feb 17, 2026
088d705
Refactor graalvm native-image build to be docker-based and Linux-only
mark-vieira Mar 5, 2026
5742880
[CI] Auto commit changes from spotless
Mar 5, 2026
026c060
Fix classpath issue
mark-vieira Mar 5, 2026
e93f17e
Simplify windows batch script
mark-vieira Mar 5, 2026
65a2580
Add launch descriptor round-trip tests
mark-vieira Mar 6, 2026
a4cd8dc
Add option to NativeImageBuildTask to support static binaries
mark-vieira Mar 6, 2026
06148c1
Use OL8 native image variant to support older glibc versions
mark-vieira Mar 6, 2026
583132d
Default static binary to false
mark-vieira Mar 6, 2026
482e9f4
Skip docker tasks when they don't support the requested architecture
mark-vieira Mar 6, 2026
1bc4883
[CI] Auto commit changes from spotless
Mar 6, 2026
44208e3
Ingore forbidden APIs
mark-vieira Mar 6, 2026
25dd1e0
Remove graalvm from verification metadata
mark-vieira Mar 6, 2026
a4ebeb1
Add back server process tests
mark-vieira Mar 6, 2026
66b38da
Disable all forbidden api tasks
mark-vieira Mar 7, 2026
bc7e013
Read launch descriptor from standard out and redirect stdout to stderr
mark-vieira Mar 9, 2026
8fa8ffb
Fix packaging tests
mark-vieira Mar 9, 2026
090a47b
Fix windows launcher script
mark-vieira Mar 9, 2026
11e9009
Make sure we drain preparer stream before waiting on exit to avoid de…
mark-vieira Mar 10, 2026
85abb79
Fix windows service launcher classpath
mark-vieira Mar 10, 2026
f9233b0
No need to override close method in ServerCli
mark-vieira Mar 10, 2026
56d5ec8
Fix typo in aarch64 native image build
mark-vieira Mar 10, 2026
69bb88f
Add packaging test assertion to ensure linux distributions include na…
mark-vieira Mar 10, 2026
120dd29
Make test resilient to stdout potentially containing other output
mark-vieira Mar 10, 2026
274e16c
Remove refactoring plan documents
mark-vieira Mar 10, 2026
b33fd51
Merge branch 'main' into native-cli-launcher
mark-vieira Mar 10, 2026
eef6f76
Make methods visible to subclasses
mark-vieira Mar 10, 2026
97bf121
Refactor server-launcher for better reusability
mark-vieira Mar 10, 2026
fbf8f3b
Merge branch 'main' into native-cli-launcher
mark-vieira Mar 10, 2026
0386d84
Merge branch 'main' into native-cli-launcher
mark-vieira Mar 10, 2026
b479b48
Remove unused configuration
mark-vieira Mar 10, 2026
64a46eb
Merge branch 'main' into native-cli-launcher
mark-vieira Mar 11, 2026
a1d1d48
Skip native image build task when docker is unavailable
mark-vieira Mar 11, 2026
6da9d15
[CI] Auto commit changes from spotless
Mar 11, 2026
94d90e2
Fix arm platform string
mark-vieira Mar 11, 2026
f84e09e
Remove OverheadSystemMemoryInfo
mark-vieira Mar 11, 2026
6b2671e
Merge branch 'main' into native-cli-launcher
mark-vieira Mar 11, 2026
dd755ca
Remove unused configuration
mark-vieira Mar 12, 2026
4a11c96
Merge branch 'main' into native-cli-launcher
mark-vieira Mar 13, 2026
c568b8a
Fix race condition in test
mark-vieira Mar 13, 2026
c47996a
Add filtering back to ErrorPumpThread and unit tests
mark-vieira Mar 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.SetProperty;
import org.gradle.api.services.ServiceReference;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputDirectory;
import org.gradle.api.tasks.Optional;
Expand All @@ -39,6 +40,7 @@
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -68,10 +70,20 @@ public DockerBuildTask(WorkerExecutor workerExecutor, ObjectFactory objectFactor
this.dockerContext = objectFactory.directoryProperty();
this.buildArgs = objectFactory.mapProperty(String.class, String.class);
this.markerFile.set(projectLayout.getBuildDirectory().file("markers/" + this.getName() + ".marker"));
onlyIf("Docker supports all requested platforms", task -> {
var platforms = getPlatforms().getOrElse(Collections.emptySet());
if (platforms.isEmpty()) {
return false;
}
DockerSupportService support = getDockerSupport().get();
return platforms.stream()
.allMatch(platform -> Architecture.fromDockerPlatform(platform).map(support::isArchitectureSupported).orElse(false));
});
}

@TaskAction
public void build() {
String dockerExecutable = getDockerSupport().get().getResolvedDockerExecutable();
workerExecutor.noIsolation().submit(DockerBuildAction.class, params -> {
params.getDockerContext().set(dockerContext);
params.getMarkerFile().set(markerFile);
Expand All @@ -82,6 +94,7 @@ public void build() {
params.getBaseImages().set(Arrays.asList(baseImages));
params.getBuildArgs().set(buildArgs);
params.getPlatforms().set(getPlatforms());
params.getDockerExecutable().set(dockerExecutable);
});
}

Expand Down Expand Up @@ -148,6 +161,9 @@ public RegularFileProperty getMarkerFile() {
return markerFile;
}

@ServiceReference(DockerSupportPlugin.DOCKER_SUPPORT_SERVICE_NAME)
public abstract Property<DockerSupportService> getDockerSupport();

public abstract static class DockerBuildAction implements WorkAction<Parameters> {
private final ExecOperations execOperations;

Expand All @@ -163,12 +179,13 @@ public DockerBuildAction(ExecOperations execOperations) {
*/
private void pullBaseImage(String baseImage) {
final int maxAttempts = 10;
String docker = getParameters().getDockerExecutable().get();

for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
LoggedExec.exec(execOperations, spec -> {
maybeConfigureDockerConfig(spec);
spec.executable("docker");
spec.executable(docker);
spec.args("pull");
spec.environment("DOCKER_BUILDKIT", "1");
spec.args(baseImage);
Expand Down Expand Up @@ -205,7 +222,7 @@ public void execute() {
LoggedExec.exec(execOperations, spec -> {
maybeConfigureDockerConfig(spec);

spec.executable("docker");
spec.executable(parameters.getDockerExecutable().get());
spec.environment("DOCKER_BUILDKIT", "1");
if (isCrossPlatform) {
spec.args("buildx");
Expand Down Expand Up @@ -260,9 +277,10 @@ private boolean isCrossPlatform() {

private String getImageChecksum(String imageTag) {
final ByteArrayOutputStream stdout = new ByteArrayOutputStream();
String docker = getParameters().getDockerExecutable().get();

execOperations.exec(spec -> {
spec.setCommandLine("docker", "inspect", "--format", "{{ .Id }}", imageTag);
spec.setCommandLine(docker, "inspect", "--format", "{{ .Id }}", imageTag);
spec.setStandardOutput(stdout);
spec.setIgnoreExitValue(false);
});
Expand All @@ -289,5 +307,7 @@ interface Parameters extends WorkParameters {
SetProperty<String> getPlatforms();

Property<Boolean> getPush();

Property<String> getDockerExecutable();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public void apply(Project project) {
params.getIsCI().set(buildParams.getCi());
}));

// Ensure that if we are trying to run any DockerBuildTask tasks, we assert an available Docker installation exists
// Ensure that if we are trying to run Docker build tasks, we assert an available Docker installation exists
project.getGradle().getTaskGraph().whenReady(graph -> {
List<String> dockerTasks = graph.getAllTasks()
.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
public abstract class DockerSupportService implements BuildService<DockerSupportService.Parameters> {

private static final Logger LOGGER = Logging.getLogger(DockerSupportService.class);
// Defines the possible locations of the Docker CLI. These will be searched in order.
private static final String[] DOCKER_BINARIES = { "/usr/bin/docker", "/usr/local/bin/docker" };
// Defines the possible locations of the Docker CLI. Searched in order for resolution and availability.
private static final String[] DOCKER_BINARIES = { "/usr/local/bin/docker", "/usr/bin/docker", "/opt/homebrew/bin/docker" };
private static final String[] DOCKER_COMPOSE_BINARIES = {
"/usr/local/bin/docker-compose",
"/usr/bin/docker-compose",
Expand All @@ -72,7 +72,7 @@ public DockerSupportService(ProviderFactory providerFactory) {
*
* @return the results of the search.
*/
public DockerAvailability getDockerAvailability() {
public synchronized DockerAvailability getDockerAvailability() {
if (this.dockerAvailability == null) {
String dockerPath;
String dockerComposePath = null;
Expand Down Expand Up @@ -305,15 +305,43 @@ static Map<String, String> parseOsRelease(final List<String> osReleaseLines) {
}

/**
* Searches the entries in {@link #DOCKER_BINARIES} for the Docker CLI. This method does
* not check whether the Docker installation appears usable, see {@link #getDockerAvailability()}
* instead.
* Resolves the Docker executable so it can be found even when PATH is minimal (e.g. Gradle
* workers or IDE). Searches PATH first for an executable named "docker", then falls back to
* {@link #DOCKER_BINARIES}. Use this when invoking docker from tasks so the binary is found
* regardless of worker environment.
*
* @return the absolute path to the Docker CLI if found and executable, otherwise "docker".
*/
public String getResolvedDockerExecutable() {
String pathEnv = System.getenv("PATH");
if (pathEnv != null && pathEnv.isEmpty() == false) {
String separator = System.getProperty("path.separator", ":");
for (String dir : pathEnv.split(separator)) {
File candidate = new File(dir.trim(), "docker");
if (candidate.isFile() && candidate.canExecute()) {
return candidate.getAbsolutePath();
}
}
}
for (String path : DOCKER_BINARIES) {
File f = new File(path);
if (f.isFile() && f.canExecute()) {
return f.getAbsolutePath();
}
}
return "docker";
}

/**
* Searches for the Docker CLI using the same logic as {@link #getResolvedDockerExecutable()}.
* This method does not check whether the Docker installation appears usable, see
* {@link #getDockerAvailability()} instead.
*
* @return the path to a CLI, if available.
*/
private Optional<String> getDockerPath() {
// Check if the Docker binary exists
return Stream.of(DOCKER_BINARIES).filter(path -> new File(path).exists()).findFirst();
String resolved = getResolvedDockerExecutable();
return "docker".equals(resolved) ? Optional.empty() : Optional.of(resolved);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*
* 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.docker;

import org.elasticsearch.gradle.Architecture;
import org.elasticsearch.gradle.LoggedExec;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.services.ServiceReference;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.process.ExecOperations;
import org.gradle.process.ExecSpec;
import org.gradle.workers.WorkAction;
import org.gradle.workers.WorkParameters;
import org.gradle.workers.WorkerExecutor;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import javax.inject.Inject;

/**
* Builds a GraalVM native-image binary by running native-image inside a Docker
* container. Uses the Gradle worker API so multiple architecture builds can
* run in parallel, and is cacheable for remote build cache.
*
* We assume native images to be optional, and a fallback to be available, so
* this task will be skipped if Docker is not available or the target platform
* is not supported rather than failing.
*/
@CacheableTask
public abstract class NativeImageBuildTask extends DefaultTask {

private FileCollection classpath;

@Inject
public NativeImageBuildTask() {
onlyIf(
"Docker supports target platform",
task -> Architecture.fromDockerPlatform(getPlatform().getOrNull())
.map(arch -> getDockerSupport().get().isArchitectureSupported(arch))
.orElse(false)
&& getDockerSupport().get().getDockerAvailability().isAvailable()
);
}

@Classpath
public FileCollection getClasspath() {
return classpath;
}

public void setClasspath(FileCollection classpath) {
this.classpath = classpath;
}

@Input
public abstract Property<String> getImageTag();

@Input
public abstract Property<String> getPlatform();

@Input
public abstract Property<String> getMainClass();

/**
* When true, pass {@code --static} to native-image to produce a fully static binary.
* Defaults to false.
*/
@Input
@Optional
public abstract Property<Boolean> getStatic();

@OutputFile
public abstract RegularFileProperty getOutputFile();

@ServiceReference(DockerSupportPlugin.DOCKER_SUPPORT_SERVICE_NAME)
public abstract Property<DockerSupportService> getDockerSupport();

@Inject
public abstract WorkerExecutor getWorkerExecutor();

@TaskAction
public void execute() {
String dockerExecutable = getDockerSupport().get().getResolvedDockerExecutable();
getWorkerExecutor().noIsolation().submit(NativeImageBuildAction.class, params -> {
params.getClasspath().setFrom(getClasspath());
params.getImageTag().set(getImageTag());
params.getPlatform().set(getPlatform());
params.getMainClass().set(getMainClass());
params.getStatic().set(getStatic().getOrElse(false));
params.getOutputFile().set(getOutputFile());
params.getDockerExecutable().set(dockerExecutable);
});
}

interface Parameters extends WorkParameters {
ConfigurableFileCollection getClasspath();

Property<String> getImageTag();

Property<String> getPlatform();

Property<String> getMainClass();

Property<Boolean> getStatic();

RegularFileProperty getOutputFile();

Property<String> getDockerExecutable();
}

public abstract static class NativeImageBuildAction implements WorkAction<Parameters> {

private final ExecOperations execOperations;

@Inject
public NativeImageBuildAction(ExecOperations execOperations) {
this.execOperations = execOperations;
}

@Override
public void execute() {
Parameters params = getParameters();
String imageTag = params.getImageTag().get();
String platform = params.getPlatform().get();
String mainClass = params.getMainClass().get();
File outputFile = params.getOutputFile().get().getAsFile();
File outputDir = outputFile.getParentFile();

if (outputDir.exists() == false && outputDir.mkdirs() == false) {
throw new GradleException("Failed to create output directory: " + outputDir);
}

List<File> classpathFiles = params.getClasspath().getFiles().stream().filter(File::exists).collect(Collectors.toList());
if (classpathFiles.isEmpty()) {
throw new GradleException("Native-image classpath is empty");
}

// Build classpath string for inside the container: /cp/0:/cp/1:...
List<String> cpPaths = new ArrayList<>();
for (int i = 0; i < classpathFiles.size(); i++) {
cpPaths.add("/cp/" + i);
}
// Container is always Linux
String cpString = String.join(":", cpPaths);

List<String> args = new ArrayList<>();
args.add("run");
args.add("--rm");
for (int i = 0; i < classpathFiles.size(); i++) {
File f = classpathFiles.get(i);
String path = f.getAbsolutePath();
if (File.separatorChar == '\\') {
path = path.replace("\\", "/");
}
args.add("-v");
args.add(path + ":/cp/" + i + ":ro");
}
args.add("-v");
String outPath = outputDir.getAbsolutePath();
if (File.separatorChar == '\\') {
outPath = outPath.replace("\\", "/");
}
args.add(outPath + ":/output");
args.add("--platform");
args.add(platform);
args.add(imageTag);
args.add("--no-fallback");
if (params.getStatic().get()) {
args.add("--static");
}
args.add("-cp");
args.add(cpString);
args.add("-o");
args.add("/output/" + outputFile.getName());
args.add(mainClass);

LoggedExec.exec(execOperations, spec -> {
maybeConfigureDockerConfig(spec);
spec.executable(params.getDockerExecutable().get());
spec.args(args);
});
}

private void maybeConfigureDockerConfig(ExecSpec spec) {
String dockerConfig = System.getenv("DOCKER_CONFIG");
if (dockerConfig != null) {
spec.environment("DOCKER_CONFIG", dockerConfig);
}
}
}
}
Loading
Loading