Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ plugins {
}

group 'com.formkiq.gradle'
version '1.7.5'
version '1.7.6'

spotless {
java {
Expand Down
66 changes: 30 additions & 36 deletions src/main/java/com/formkiq/gradle/GraalvmNativePlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,48 +34,42 @@ public void apply(final Project project) {
Provider<GraalvmBuildService> svc = project.getGradle().getSharedServices().registerIfAbsent(
"web", GraalvmBuildService.class, spec -> spec.getMaxParallelUsages().set(1));

project.afterEvaluate(p -> {
// Treat "configured" as: user set mainClassName (adjust the predicate if you prefer)
boolean configured = ext.getMainClassName().isPresent();
// ✅ Register task immediately so tasks.named(...) always works
TaskProvider<GraalvmNativeTask> nativeImage =
project.getTasks().register("graalvmNativeImage", GraalvmNativeTask.class, task -> {
task.setGroup("Graalvm");
task.setDescription("Build GraalVM Native Image");
task.setExtension(ext);
task.usesService(svc);
task.getBuildDirectory().set(project.getLayout().getBuildDirectory().dir("graalvm"));

if (!configured) {
// User didn't declare nativeImage { ... } in this subproject — do nothing.
return;
}

// Register the task now that we know it's wanted in this project
TaskProvider<GraalvmNativeTask> nativeImage =
project.getTasks().register("graalvmNativeImage", GraalvmNativeTask.class, task -> {
task.setGroup("Graalvm");
task.setDescription("Build GraalVM Native Image");
task.setExtension(ext); // inject the extension (nested inputs)
task.usesService(svc);
task.getBuildDirectory().set(project.getLayout().getBuildDirectory().dir("graalvm"));
// ✅ Opt-in: task will only run if configured
task.onlyIf(t -> {
String dockerFile = ext.getDockerFile();
return ext.getMainClassName().isPresent()
|| (dockerFile != null && !dockerFile.isBlank());
});

// Wire only if the Java plugin is applied
project.getPlugins().withType(JavaPlugin.class, jp -> {
SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
SourceSet main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);

// Inputs
nativeImage.configure(t -> {
t.getSources().from(main.getAllSource()); // java + resources
t.getRuntimeClasspath().from(main.getRuntimeClasspath()); // runtime jars/classes
});

// Ensure producers run first (jar is enough; avoids assemble cycles)
nativeImage.configure(t -> t.dependsOn(project.getTasks().named(JavaPlugin.JAR_TASK_NAME)));
// Wire only if Java plugin is applied
project.getPlugins().withType(JavaPlugin.class, jp -> {
SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
SourceSet main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);

nativeImage.configure(t -> {
t.getSources().from(main.getAllSource());
t.getRuntimeClasspath().from(main.getRuntimeClasspath());
t.dependsOn(project.getTasks().named(JavaPlugin.JAR_TASK_NAME));
t.dependsOn(project.getTasks().named(JavaPlugin.TEST_TASK_NAME));
});
});

// If you want assemble to run AFTER native image in projects that opted in
project.getTasks()
.named(org.gradle.language.base.plugins.LifecycleBasePlugin.ASSEMBLE_TASK_NAME)
.configure(t -> t.dependsOn(nativeImage));
// Safe: assemble depends on nativeImage, but nativeImage will be SKIPPED if not configured
project.getTasks()
.named(org.gradle.language.base.plugins.LifecycleBasePlugin.ASSEMBLE_TASK_NAME)
.configure(t -> t.dependsOn(nativeImage));

// If the Distribution plugin is applied, make distZip wait for native image
project.getPlugins().withId("distribution",
__ -> project.getTasks().named("distZip").configure(t -> t.dependsOn(nativeImage)));
});
project.getPlugins().withId("distribution",
__ -> project.getTasks().named("distZip").configure(t -> t.dependsOn(nativeImage)));
}
}
27 changes: 19 additions & 8 deletions src/main/java/com/formkiq/gradle/GraalvmNativeTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
*/
package com.formkiq.gradle;

import static com.formkiq.gradle.internal.NativeImageExecutor.GRAALVM_JAVA_MAIN;

import com.formkiq.gradle.internal.ArchiveUtils;
import com.formkiq.gradle.internal.Downloader;
import com.formkiq.gradle.internal.NativeImageExecutor;
Expand Down Expand Up @@ -169,12 +171,17 @@ public void setExtension(final GraalvmNativeExtension params) {
@TaskAction
public void createImage() {

if (extension.getMainClassName() != null) {
String mainClassName = this.extension.getMainClassName().getOrNull();
String dockerFile = this.extension.getDockerFile();
boolean hasMainClass = mainClassName != null && !mainClassName.isBlank();
boolean hasDockerFile = dockerFile != null && !dockerFile.isBlank();

if (hasMainClass || hasDockerFile) {
try {

NativeImageExecutor executor = new NativeImageExecutor(this.extension);

if (this.extension.getDockerFile() != null) {
if (hasDockerFile) {
executeDockerFile();
} else if (this.extension.getDockerImage() != null) {

Expand Down Expand Up @@ -223,26 +230,28 @@ private Path getBuildDirectoryAsPath() {

private void executeDockerFile() throws IOException, InterruptedException {

DockerService service = new DefaultDockerService();
DockerService service = new DefaultDockerService(getLogger());
if (!service.isDockerRunning()) {
throw new ResourceException("Docker is not running");
}

String dockerfileContent = Files.readString(Path.of(this.extension.getDockerFile()));
getLogger().info("Generating Dockerfile");
getLogger().info("{}", dockerfileContent);
getLogger().info("Building Dockerfile: " + this.extension.getDockerFile());

service.removeDockerImage(this.extension.getOutputImageTag());

Path buildDir = getBuildDirectoryAsPath();
service.buildDockerImage(buildDir, this.extension.getOutputImageTag(), dockerfileContent);
Path contextDir = Path.of(this.extension.getDockerFile()).getParent();
contextDir = contextDir != null ? contextDir : Path.of(".");
service.buildDockerImage(buildDir, this.extension.getOutputImageTag(), dockerfileContent,
contextDir);
service.runDockerImage(buildDir, this.extension.getOutputImageTag());
}

private void executeDockerImage(NativeImageExecutor executor)
throws IOException, InterruptedException {

DockerService service = new DefaultDockerService();
DockerService service = new DefaultDockerService(getLogger());
if (!service.isDockerRunning()) {
throw new ResourceException("Docker is not running");
}
Expand All @@ -264,7 +273,9 @@ private void executeDockerImage(NativeImageExecutor executor)
service.removeDockerImage(this.extension.getOutputImageTag());

Path buildDir = getBuildDirectoryAsPath();
service.buildDockerImage(buildDir, this.extension.getOutputImageTag(), dockerfileContent);
Path contextDir = buildDir.resolve(GRAALVM_JAVA_MAIN);
service.buildDockerImage(buildDir, this.extension.getOutputImageTag(), dockerfileContent,
contextDir);
service.runDockerImage(buildDir, this.extension.getOutputImageTag());
}

Expand Down
62 changes: 46 additions & 16 deletions src/main/java/com/formkiq/gradle/services/DefaultDockerService.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.formkiq.gradle.services;

import static com.formkiq.gradle.internal.NativeImageExecutor.GRAALVM_JAVA_MAIN;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.BuildImageResultCallback;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.exception.NotFoundException;
import com.github.dockerjava.api.model.Bind;
Expand All @@ -14,20 +11,32 @@
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.github.dockerjava.transport.DockerHttpClient;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Collections;
import org.gradle.api.logging.Logger;

/** Default implementation using docker-java client (socket first, then TCP). */
public final class DefaultDockerService implements DockerService {
private final DockerClient dockerClient;

/** constructor. */
public DefaultDockerService() {
/** {@link Logger}. */
private final Logger log;

/** {@link LoggingBuildImageResultCallback}. */
private final LoggingBuildImageResultCallback callback;

/**
* constructor.
*
* @param logger {@link Logger}
*/
public DefaultDockerService(final Logger logger) {
this.dockerClient = initClient();
this.log = logger;
this.callback = new LoggingBuildImageResultCallback(this.log);
}

private DockerClient initClient() {
Expand Down Expand Up @@ -68,40 +77,61 @@ public boolean isDockerRunning() {

@Override
public Path buildDockerImage(final Path buildDir, final String imageTag,
final String dockerFileContent) throws IOException {
final String dockerFileContent, final Path contextDir) throws IOException {

Path dockerfile = writeDockerFile(buildDir, dockerFileContent);

File contextDir = buildDir.resolve(GRAALVM_JAVA_MAIN).toFile();
String dockerCommand =
String.format("docker build -f %s -t %s %s", dockerfile, imageTag, contextDir);

dockerClient.buildImageCmd().withBaseDirectory(contextDir).withDockerfile(dockerfile.toFile())
.withTags(Collections.singleton(imageTag)).exec(new BuildImageResultCallback())
.awaitImageId();
log(dockerCommand);
dockerClient.buildImageCmd().withBaseDirectory(contextDir.toFile())
.withDockerfile(dockerfile.toFile()).withTags(Collections.singleton(imageTag))
.exec(this.callback).awaitImageId();

return dockerfile;
}

private void log(final String log) {
if (this.log != null) {
this.log.info(log);
}
}

@Override
public void runDockerImage(final Path buildDir, final String imageTag)
throws IOException, InterruptedException {

Path path = buildDir.resolve("output");
Files.createDirectories(path);

String containerName = "copy-file-container-" + System.currentTimeMillis();
String hostPath = path.toAbsolutePath().toString();

log(String.format("docker run --name %s -v %s:/output %s", containerName, hostPath, imageTag));

Volume containerOutputVolume = new Volume("/output");
HostConfig hostConfig = HostConfig.newHostConfig()
.withBinds(new Bind(path.toAbsolutePath().toString(), containerOutputVolume));
HostConfig hostConfig =
HostConfig.newHostConfig().withBinds(new Bind(hostPath, containerOutputVolume));

log("Creating container '" + containerName + "' from image '" + imageTag + "'");
CreateContainerResponse container = dockerClient.createContainerCmd(imageTag)
.withName("copy-file-container-" + System.currentTimeMillis()).withHostConfig(hostConfig)
.exec();
.withName(containerName).withHostConfig(hostConfig).exec();

String containerId = container.getId();
dockerClient.startContainerCmd(containerId).exec();
log("Container created with ID: " + containerId);

try {
log("Starting container " + containerId);
dockerClient.startContainerCmd(containerId).exec();

log("Waiting for container " + containerId + " to finish");
dockerClient.waitContainerCmd(containerId).start().awaitCompletion();
log("Container " + containerId + " finished successfully");
} finally {
log("Removing container " + containerId + " (force=true)");
dockerClient.removeContainerCmd(containerId).withForce(true).exec();
log("Container " + containerId + " removed");
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/formkiq/gradle/services/DockerService.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ public interface DockerService {
* @param buildDir {@link Path}
* @param imageTag {@link String}
* @param dockerFileContent Docker file content
* @param contextDir Docker Context dir
* @return Path
* @throws IOException IOException
*/
Path buildDockerImage(Path buildDir, String imageTag, String dockerFileContent)
Path buildDockerImage(Path buildDir, String imageTag, String dockerFileContent, Path contextDir)
throws IOException;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.formkiq.gradle.services;

import com.github.dockerjava.api.command.BuildImageResultCallback;
import com.github.dockerjava.api.model.BuildResponseItem;
import com.github.dockerjava.api.model.ResponseItem;
import org.gradle.api.logging.Logger;

/** {@link BuildImageResultCallback} logger. */
public class LoggingBuildImageResultCallback extends BuildImageResultCallback {

private long lastPercent = -1;
private final Logger log;

/**
* Create a Docker build logger.
*
* @param logger logging function (e.g. this::log)
*/
public LoggingBuildImageResultCallback(final Logger logger) {
this.log = logger;
}

@Override
public void onNext(final BuildResponseItem item) {

// Dockerfile step output
if (log != null) {
String s = item.getStream();
if (s != null) {
log.info(s.trim());
}
}

// Layer download progress
// (NO direct ProgressDetail reference)
ResponseItem.ProgressDetail pd = item.getProgressDetail();
if (pd != null && pd.getTotal() != null && pd.getCurrent() != null) {

long current = pd.getCurrent();
long total = pd.getTotal();

if (total > 0) {
long percent = (current * 100) / total;
if (percent != lastPercent) {
if (log != null) {
log.info("Progress: {}%", percent);
}

lastPercent = percent;
}
}
}

// Error output
if (log != null) {
ResponseItem.ErrorDetail errorDetail = item.getErrorDetail();
if (errorDetail != null) {
log.error("ERROR: {}", errorDetail.getMessage());
}
}

super.onNext(item);
}

@Override
public void onError(final Throwable throwable) {
if (log != null) {
log.error("Docker build failed: {}", throwable.getMessage());
}
super.onError(throwable);
}

@Override
public void onComplete() {
if (log != null) {
log.info("Docker build completed");
}
super.onComplete();
}
}
Loading