From f855a14f1253f06537a1b4bc5bccd63abede6f1e Mon Sep 17 00:00:00 2001 From: Mike Friesen Date: Mon, 22 Dec 2025 16:21:43 -0600 Subject: [PATCH] Fixed 'dockerFile' parameter --- build.gradle | 2 +- .../formkiq/gradle/GraalvmNativePlugin.java | 66 +++++++-------- .../com/formkiq/gradle/GraalvmNativeTask.java | 27 +++++-- .../gradle/services/DefaultDockerService.java | 62 ++++++++++---- .../gradle/services/DockerService.java | 3 +- .../LoggingBuildImageResultCallback.java | 80 +++++++++++++++++++ .../gradle/services/ShellDockerService.java | 8 +- .../gradle/GraalvmNativePluginTest.java | 22 ++++- .../gradle/services/DockerServiceTests.java | 21 +++-- 9 files changed, 214 insertions(+), 77 deletions(-) create mode 100644 src/main/java/com/formkiq/gradle/services/LoggingBuildImageResultCallback.java diff --git a/build.gradle b/build.gradle index 9db9b17..3952975 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ plugins { } group 'com.formkiq.gradle' -version '1.7.5' +version '1.7.6' spotless { java { diff --git a/src/main/java/com/formkiq/gradle/GraalvmNativePlugin.java b/src/main/java/com/formkiq/gradle/GraalvmNativePlugin.java index 4447cf6..8f53f75 100644 --- a/src/main/java/com/formkiq/gradle/GraalvmNativePlugin.java +++ b/src/main/java/com/formkiq/gradle/GraalvmNativePlugin.java @@ -34,48 +34,42 @@ public void apply(final Project project) { Provider 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 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 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))); } } diff --git a/src/main/java/com/formkiq/gradle/GraalvmNativeTask.java b/src/main/java/com/formkiq/gradle/GraalvmNativeTask.java index 5239c52..0f1b2c6 100644 --- a/src/main/java/com/formkiq/gradle/GraalvmNativeTask.java +++ b/src/main/java/com/formkiq/gradle/GraalvmNativeTask.java @@ -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; @@ -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) { @@ -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"); } @@ -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()); } diff --git a/src/main/java/com/formkiq/gradle/services/DefaultDockerService.java b/src/main/java/com/formkiq/gradle/services/DefaultDockerService.java index d74f380..6f262be 100644 --- a/src/main/java/com/formkiq/gradle/services/DefaultDockerService.java +++ b/src/main/java/com/formkiq/gradle/services/DefaultDockerService.java @@ -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; @@ -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() { @@ -68,19 +77,27 @@ 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 { @@ -88,20 +105,33 @@ public void runDockerImage(final Path buildDir, final String imageTag) 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"); } } diff --git a/src/main/java/com/formkiq/gradle/services/DockerService.java b/src/main/java/com/formkiq/gradle/services/DockerService.java index b20826a..c9d397a 100644 --- a/src/main/java/com/formkiq/gradle/services/DockerService.java +++ b/src/main/java/com/formkiq/gradle/services/DockerService.java @@ -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; /** diff --git a/src/main/java/com/formkiq/gradle/services/LoggingBuildImageResultCallback.java b/src/main/java/com/formkiq/gradle/services/LoggingBuildImageResultCallback.java new file mode 100644 index 0000000..a4f674b --- /dev/null +++ b/src/main/java/com/formkiq/gradle/services/LoggingBuildImageResultCallback.java @@ -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(); + } +} diff --git a/src/main/java/com/formkiq/gradle/services/ShellDockerService.java b/src/main/java/com/formkiq/gradle/services/ShellDockerService.java index 9a64dd8..ede019f 100644 --- a/src/main/java/com/formkiq/gradle/services/ShellDockerService.java +++ b/src/main/java/com/formkiq/gradle/services/ShellDockerService.java @@ -1,12 +1,10 @@ package com.formkiq.gradle.services; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Optional; /** Shell-based implementation using the Docker CLI. */ public class ShellDockerService implements DockerService { @@ -62,11 +60,11 @@ public void runDockerImage(final Path buildDir, final String imageTag) throws IO @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 = - Optional.ofNullable(dockerfile.getParent()).map(Path::toFile).orElse(new File(".")); + // File contextDir = + // Optional.ofNullable(dockerfile.getParent()).map(Path::toFile).orElse(new File(".")); ProcessBuilder pb = new ProcessBuilder("docker", "build", "-t", imageTag, "-f", dockerfile.toString(), contextDir.toString()); diff --git a/src/test/java/com/formkiq/gradle/GraalvmNativePluginTest.java b/src/test/java/com/formkiq/gradle/GraalvmNativePluginTest.java index 0d4c956..ffa8f95 100644 --- a/src/test/java/com/formkiq/gradle/GraalvmNativePluginTest.java +++ b/src/test/java/com/formkiq/gradle/GraalvmNativePluginTest.java @@ -14,11 +14,12 @@ */ package com.formkiq.gradle; -import static org.junit.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; import org.gradle.api.Project; +import org.gradle.api.internal.project.ProjectInternal; import org.gradle.testfixtures.ProjectBuilder; -import org.junit.Test; +import org.junit.jupiter.api.Test; /** Unit test for the 'com.formkiq.gradle.graalvm-native-plugin' plugin. */ public class GraalvmNativePluginTest { @@ -30,6 +31,23 @@ public void pluginRegistersATask() { Project project = ProjectBuilder.builder().build(); project.getPlugins().apply("java-gradle-plugin"); project.getPlugins().apply("com.formkiq.gradle.graalvm-native-plugin"); + project.getExtensions().getByType(GraalvmNativeExtension.class) + .setMainClassName("com.example.Main"); + ((ProjectInternal) project).evaluate(); + + // Verify the result + assertNotNull(project.getTasks().findByName("graalvmNativeImage")); + } + + /** Test Registering Task with dockerFile. */ + @Test + public void pluginRegistersATaskWithDockerfile() { + // Create a test project and apply the plugin + Project project = ProjectBuilder.builder().build(); + project.getPlugins().apply("java-gradle-plugin"); + project.getPlugins().apply("com.formkiq.gradle.graalvm-native-plugin"); + project.getExtensions().getByType(GraalvmNativeExtension.class).setDockerFile("Dockerfile"); + ((ProjectInternal) project).evaluate(); // Verify the result assertNotNull(project.getTasks().findByName("graalvmNativeImage")); diff --git a/src/test/java/com/formkiq/gradle/services/DockerServiceTests.java b/src/test/java/com/formkiq/gradle/services/DockerServiceTests.java index a90c2e2..7e6ea6a 100644 --- a/src/test/java/com/formkiq/gradle/services/DockerServiceTests.java +++ b/src/test/java/com/formkiq/gradle/services/DockerServiceTests.java @@ -26,15 +26,17 @@ class DockerServiceTests { void testDefaultServiceBuildDockerImage() throws IOException { // given Files.createDirectories(PATH); - DefaultDockerService service = new DefaultDockerService(); + DefaultDockerService service = new DefaultDockerService(null); List nativeArgs = List.of(); DockerfileGenerator gen = DockerfileGenerator.builder().baseImage(DOCKER_IMAGE_24) .addNativeImageArgs(nativeArgs).mainClass(null).build(); + Path contextDir = BUILD_DIR.resolve(GRAALVM_JAVA_MAIN); + // when - Path outputPath = - service.buildDockerImage(BUILD_DIR, TEST_IMAGE_NAME, gen.generateContents(BUILD_DIR)); + Path outputPath = service.buildDockerImage(BUILD_DIR, TEST_IMAGE_NAME, + gen.generateContents(BUILD_DIR), contextDir); // then assertTrue(Files.exists(outputPath), "Dockerfile should be created"); @@ -49,15 +51,17 @@ void testDefaultServiceBuildDockerImage() throws IOException { void testDefaultServiceRunDockerImage() throws IOException, InterruptedException { // given String dockerFileContent = getDockerfileContent("dockerfile/Dockerfile3"); + Path contextDir = BUILD_DIR.resolve(GRAALVM_JAVA_MAIN); - for (DockerService service : List.of(new DefaultDockerService(), new ShellDockerService())) { + for (DockerService service : List.of(new DefaultDockerService(null), + new ShellDockerService())) { FileUtils.deleteRecursively(PATH); FileUtils.deleteRecursively(Path.of("build", "graalvm")); // when service.removeDockerImage(TEST_IMAGE_NAME); - service.buildDockerImage(BUILD_DIR, TEST_IMAGE_NAME, dockerFileContent); + service.buildDockerImage(BUILD_DIR, TEST_IMAGE_NAME, dockerFileContent, contextDir); service.runDockerImage(BUILD_DIR, TEST_IMAGE_NAME); // then @@ -68,7 +72,7 @@ void testDefaultServiceRunDockerImage() throws IOException, InterruptedException @Test void testDefaultServiceIsDockerRunningDoesNotThrow() { - DefaultDockerService service = new DefaultDockerService(); + DefaultDockerService service = new DefaultDockerService(null); assertDoesNotThrow(() -> { boolean running = service.isDockerRunning(); assertTrue(running, "isDockerRunning should return a boolean and not throw"); @@ -79,6 +83,7 @@ void testDefaultServiceIsDockerRunningDoesNotThrow() { void testShellServiceBuildDockerImage() throws IOException { // given Files.createDirectories(PATH); + Path contextDir = BUILD_DIR.resolve(GRAALVM_JAVA_MAIN); ShellDockerService service = new ShellDockerService(); List nativeArgs = List.of(); @@ -86,8 +91,8 @@ void testShellServiceBuildDockerImage() throws IOException { .addNativeImageArgs(nativeArgs).mainClass(null).build(); // when - Path outputPath = - service.buildDockerImage(BUILD_DIR, TEST_IMAGE_NAME, gen.generateContents(BUILD_DIR)); + Path outputPath = service.buildDockerImage(BUILD_DIR, TEST_IMAGE_NAME, + gen.generateContents(BUILD_DIR), contextDir); // then assertTrue(Files.exists(outputPath), "Dockerfile should be created");