diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java index ce4d24084b8f2..7717fd5d137e1 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java @@ -166,8 +166,8 @@ public class NativeConfig { public boolean dumpProxies; /** - * If this build should be done using a container runtime. If this is set docker will be used by default, - * unless container-runtime is also set. + * If this build should be done using a container runtime. Unless container-runtime is also set, docker will be + * used by default. If docker is not available or is an alias to podman, podman will be used instead as the default. */ @ConfigItem public boolean containerBuild; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java index 6aa30b51ea184..b20e35e8b0106 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java @@ -30,7 +30,7 @@ public NativeImageBuildContainerRunner(NativeConfig nativeConfig, Path outputDir containerRuntime = nativeConfig.containerRuntime.orElseGet(NativeImageBuildContainerRunner::detectContainerRuntime); log.infof("Using %s to run the native image builder", containerRuntime.getExecutableName()); - this.baseContainerRuntimeArgs = new String[] { "--env", "LANG=C" }; + this.baseContainerRuntimeArgs = new String[] { "--env", "LANG=C", "--rm" }; outputPath = outputDir == null ? null : outputDir.toAbsolutePath().toString(); @@ -70,6 +70,17 @@ protected String[] getBuildCommand(List args) { return buildCommand("run", getContainerRuntimeBuildArgs(), args); } + @Override + protected void objcopy(String... args) { + final List containerRuntimeBuildArgs = getContainerRuntimeBuildArgs(); + Collections.addAll(containerRuntimeBuildArgs, "--entrypoint", "/bin/bash"); + final ArrayList objcopyCommand = new ArrayList<>(2); + objcopyCommand.add("-c"); + objcopyCommand.add("objcopy " + String.join(" ", args)); + final String[] command = buildCommand("run", containerRuntimeBuildArgs, objcopyCommand); + runCommand(command, null, null); + } + protected List getContainerRuntimeBuildArgs() { List containerRuntimeArgs = new ArrayList<>(); nativeConfig.containerRuntimeOptions.ifPresent(containerRuntimeArgs::addAll); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java index ec8e6ee6cb4e2..f1e09c4c7a483 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java @@ -35,7 +35,7 @@ protected List getContainerRuntimeBuildArgs() { } } - Collections.addAll(containerRuntimeArgs, "--rm", "-v", + Collections.addAll(containerRuntimeArgs, "-v", volumeOutputPath + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH + ":z"); return containerRuntimeArgs; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalRunner.java index 4a2c8a123e30b..e693a90b34e55 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalRunner.java @@ -1,14 +1,17 @@ package io.quarkus.deployment.pkg.steps; +import java.io.File; import java.util.List; import java.util.stream.Stream; public class NativeImageBuildLocalRunner extends NativeImageBuildRunner { private final String nativeImageExecutable; + private final File workingDirectory; - public NativeImageBuildLocalRunner(String nativeImageExecutable) { + public NativeImageBuildLocalRunner(String nativeImageExecutable, File workingDirectory) { this.nativeImageExecutable = nativeImageExecutable; + this.workingDirectory = workingDirectory; } @Override @@ -21,6 +24,34 @@ protected String[] getBuildCommand(List args) { return buildCommand(args); } + @Override + protected void objcopy(String... args) { + final String[] command = new String[args.length + 1]; + command[0] = "objcopy"; + System.arraycopy(args, 0, command, 1, args.length); + runCommand(command, null, workingDirectory); + } + + @Override + protected boolean objcopyExists() { + // System path + String systemPath = System.getenv("PATH"); + if (systemPath != null) { + String[] pathDirs = systemPath.split(File.pathSeparator); + for (String pathDir : pathDirs) { + File dir = new File(pathDir); + if (dir.isDirectory()) { + File file = new File(dir, "objcopy"); + if (file.exists()) { + return true; + } + } + } + } + + return false; + } + private String[] buildCommand(List args) { return Stream.concat(Stream.of(nativeImageExecutable), args.stream()).toArray(String[]::new); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRemoteContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRemoteContainerRunner.java index 34c20b4b2a31f..f427ca4c5268b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRemoteContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRemoteContainerRunner.java @@ -4,6 +4,8 @@ import java.io.IOException; import java.io.InputStreamReader; import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.jboss.logging.Logger; @@ -13,52 +15,83 @@ public class NativeImageBuildRemoteContainerRunner extends NativeImageBuildContainerRunner { private static final Logger log = Logger.getLogger(NativeImageBuildRemoteContainerRunner.class); + // Use a predefined volume name and implicitly create the volume with `podman create`, instead of explicitly + // generating a unique volume with `podman volume create`, to work around Podman's 3.0.x + // issue https://github.com/containers/podman/issues/9608 + private static final String CONTAINER_BUILD_VOLUME_NAME = "quarkus-native-builder-image-project-volume"; + private final String nativeImageName; private final String resultingExecutableName; private String containerId; - public NativeImageBuildRemoteContainerRunner(NativeConfig nativeConfig, Path outputDir, String resultingExecutableName) { + public NativeImageBuildRemoteContainerRunner(NativeConfig nativeConfig, Path outputDir, + String nativeImageName, String resultingExecutableName) { super(nativeConfig, outputDir); + this.nativeImageName = nativeImageName; this.resultingExecutableName = resultingExecutableName; } @Override protected void preBuild(List buildArgs) throws InterruptedException, IOException { - List containerRuntimeArgs = getContainerRuntimeBuildArgs(); - String[] createContainerCommand = buildCommand("create", containerRuntimeArgs, buildArgs); - log.info(String.join(" ", createContainerCommand).replace("$", "\\$")); - Process createContainerProcess = new ProcessBuilder(createContainerCommand).start(); - if (createContainerProcess.waitFor() != 0) { - throw new RuntimeException("Failed to create builder container."); - } - try (BufferedReader reader = new BufferedReader(new InputStreamReader(createContainerProcess.getInputStream()))) { - containerId = reader.readLine(); - } + // docker volume rm + rmVolume(null); + // docker create -v :/project + final List containerRuntimeArgs = Arrays.asList("-v", + CONTAINER_BUILD_VOLUME_NAME + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH); + final String[] createTempContainerCommand = buildCommand("create", containerRuntimeArgs, Collections.emptyList()); + containerId = runCommandAndReadOutput(createTempContainerCommand, "Failed to create temp container."); + // docker cp :/project String[] copyCommand = new String[] { containerRuntime.getExecutableName(), "cp", outputPath + "/.", containerId + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH }; runCommand(copyCommand, "Failed to copy source-jar and libs from host to builder container", null); super.preBuild(buildArgs); } - @Override - protected String[] getBuildCommand(List args) { - return new String[] { containerRuntime.getExecutableName(), "start", "--attach", containerId }; + private String runCommandAndReadOutput(String[] command, String errorMsg) throws IOException, InterruptedException { + log.info(String.join(" ", command).replace("$", "\\$")); + Process process = new ProcessBuilder(command).start(); + if (process.waitFor() != 0) { + throw new RuntimeException(errorMsg); + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + return reader.readLine(); + } } @Override - protected void postBuild() throws InterruptedException, IOException { - copyFromBuilder(resultingExecutableName, "Failed to copy native executable from container back to the host."); + protected void postBuild() { + copyFromContainerVolume(resultingExecutableName, "Failed to copy native image from container volume back to the host."); if (nativeConfig.debug.enabled) { - copyFromBuilder("sources", "Failed to copy sources from container back to the host."); + copyFromContainerVolume("sources", "Failed to copy sources from container volume back to the host."); + String symbols = String.format("%s.debug", nativeImageName); + copyFromContainerVolume(symbols, "Failed to copy debug symbols from container volume back to the host."); } - String[] removeCommand = new String[] { containerRuntime.getExecutableName(), "container", "rm", "--volumes", + // docker container rm + final String[] rmTempContainerCommand = new String[] { containerRuntime.getExecutableName(), "container", "rm", containerId }; - runCommand(removeCommand, "Failed to remove container: " + containerId, null); + runCommand(rmTempContainerCommand, "Failed to remove container: " + containerId, null); + // docker volume rm + rmVolume("Failed to remove volume: " + CONTAINER_BUILD_VOLUME_NAME); } - private void copyFromBuilder(String path, String errorMsg) throws IOException, InterruptedException { + private void rmVolume(String errorMsg) { + final String[] rmVolumeCommand = new String[] { containerRuntime.getExecutableName(), "volume", "rm", + CONTAINER_BUILD_VOLUME_NAME }; + runCommand(rmVolumeCommand, errorMsg, null); + } + + private void copyFromContainerVolume(String path, String errorMsg) { + // docker cp :/project/ String[] copyCommand = new String[] { containerRuntime.getExecutableName(), "cp", containerId + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH + "/" + path, outputPath }; runCommand(copyCommand, errorMsg, null); } + + @Override + protected List getContainerRuntimeBuildArgs() { + List containerRuntimeArgs = super.getContainerRuntimeBuildArgs(); + Collections.addAll(containerRuntimeArgs, "-v", + CONTAINER_BUILD_VOLUME_NAME + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH); + return containerRuntimeArgs; + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRunner.java index b3b7014ce1b09..6c06430803489 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRunner.java @@ -42,7 +42,8 @@ public GraalVM.Version getGraalVMVersion() { public void setup(boolean processInheritIODisabled) { } - public int build(List args, Path outputDir, boolean processInheritIODisabled) + public int build(List args, String nativeImageName, String resultingExecutableName, Path outputDir, + boolean debugEnabled, boolean processInheritIODisabled) throws InterruptedException, IOException { preBuild(args); try { @@ -57,16 +58,44 @@ public int build(List args, Path outputDir, boolean processInheritIODisa errorReportLatch)); executor.shutdown(); errorReportLatch.await(); - return process.waitFor(); + int exitCode = process.waitFor(); + if (exitCode != 0) { + return exitCode; + } + + if (objcopyExists()) { + if (debugEnabled) { + splitDebugSymbols(nativeImageName, resultingExecutableName); + } else { + // Strip debug symbols regardless, because the underlying JDK might contain them + objcopy("--strip-debug", resultingExecutableName); + } + } else { + log.warn("objcopy executable not found in PATH. Debug symbols will not be separated from executable."); + log.warn("That will result in a larger native image with debug symbols embedded in it."); + } + return 0; } finally { postBuild(); } } + private void splitDebugSymbols(String nativeImageName, String resultingExecutableName) { + String symbols = String.format("%s.debug", nativeImageName); + objcopy("--only-keep-debug", resultingExecutableName, symbols); + objcopy(String.format("--add-gnu-debuglink=%s", symbols), resultingExecutableName); + } + protected abstract String[] getGraalVMVersionCommand(List args); protected abstract String[] getBuildCommand(List args); + protected boolean objcopyExists() { + return true; + } + + protected abstract void objcopy(String... args); + protected void preBuild(List buildArgs) throws IOException, InterruptedException { } @@ -81,7 +110,7 @@ protected void postBuild() throws InterruptedException, IOException { * If {@code null} the failure is ignored, but logged. * @param workingDirectory The directory in which to run the command */ - void runCommand(String[] command, String errorMsg, File workingDirectory) { + static void runCommand(String[] command, String errorMsg, File workingDirectory) { log.info(String.join(" ", command).replace("$", "\\$")); Process process = null; try { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index 88031ac921ac7..956e8ee204baa 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -10,7 +10,6 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -151,7 +150,8 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa String nativeImageName = getNativeImageName(outputTargetBuildItem, packageConfig); String resultingExecutableName = getResultingExecutableName(nativeImageName, isContainerBuild); - NativeImageBuildRunner buildRunner = getNativeImageBuildRunner(nativeConfig, outputDir, resultingExecutableName); + NativeImageBuildRunner buildRunner = getNativeImageBuildRunner(nativeConfig, outputDir, + nativeImageName, resultingExecutableName); buildRunner.setup(processInheritIODisabled.isPresent()); final GraalVM.Version graalVMVersion = buildRunner.getGraalVMVersion(); @@ -184,7 +184,8 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa List nativeImageArgs = commandAndExecutable.args; - int exitCode = buildRunner.build(nativeImageArgs, outputDir, processInheritIODisabled.isPresent()); + int exitCode = buildRunner.build(nativeImageArgs, nativeImageName, resultingExecutableName, outputDir, + nativeConfig.debug.enabled, processInheritIODisabled.isPresent()); if (exitCode != 0) { throw imageGenerationFailed(exitCode, nativeImageArgs); } @@ -193,6 +194,11 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa IoUtils.copy(generatedExecutablePath, finalExecutablePath); Files.delete(generatedExecutablePath); if (nativeConfig.debug.enabled) { + final String symbolsName = String.format("%s.debug", nativeImageName); + Path generatedSymbols = outputDir.resolve(symbolsName); + Path finalSymbolsPath = outputTargetBuildItem.getOutputDirectory().resolve(symbolsName); + IoUtils.copy(generatedSymbols, finalSymbolsPath); + Files.delete(generatedSymbols); final String sources = "sources"; final Path generatedSources = outputDir.resolve(sources); final Path finalSources = outputTargetBuildItem.getOutputDirectory().resolve(sources); @@ -201,17 +207,6 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa } System.setProperty("native.image.path", finalExecutablePath.toAbsolutePath().toString()); - if (objcopyExists()) { - if (nativeConfig.debug.enabled) { - splitDebugSymbols(finalExecutablePath); - } - // Strip debug symbols regardless, because the underlying JDK might contain them - objcopy("--strip-debug", finalExecutablePath.toString()); - } else { - log.warn("objcopy executable not found in PATH. Debug symbols will not be separated from executable."); - log.warn("That will result in a larger native image with debug symbols embedded in it."); - } - return new NativeImageBuildItem(finalExecutablePath, new NativeImageBuildItem.GraalVMVersion(graalVMVersion.fullVersion, graalVMVersion.major, graalVMVersion.minor, @@ -244,9 +239,9 @@ public static boolean isContainerBuild(NativeConfig nativeConfig) { } private static NativeImageBuildRunner getNativeImageBuildRunner(NativeConfig nativeConfig, Path outputDir, - String resultingExecutableName) { + String nativeImageName, String resultingExecutableName) { if (!isContainerBuild(nativeConfig)) { - NativeImageBuildLocalRunner localRunner = getNativeImageBuildLocalRunner(nativeConfig); + NativeImageBuildLocalRunner localRunner = getNativeImageBuildLocalRunner(nativeConfig, outputDir.toFile()); if (localRunner != null) { return localRunner; } @@ -259,7 +254,8 @@ private static NativeImageBuildRunner getNativeImageBuildRunner(NativeConfig nat log.warn(errorMessage + " Attempting to fall back to container build."); } if (nativeConfig.remoteContainerBuild) { - return new NativeImageBuildRemoteContainerRunner(nativeConfig, outputDir, resultingExecutableName); + return new NativeImageBuildRemoteContainerRunner(nativeConfig, outputDir, + nativeImageName, resultingExecutableName); } return new NativeImageBuildLocalContainerRunner(nativeConfig, outputDir); } @@ -372,12 +368,12 @@ private void checkGraalVMVersion(GraalVM.Version version) { } } - private static NativeImageBuildLocalRunner getNativeImageBuildLocalRunner(NativeConfig nativeConfig) { + private static NativeImageBuildLocalRunner getNativeImageBuildLocalRunner(NativeConfig nativeConfig, File outputDir) { String executableName = getNativeImageExecutableName(); if (nativeConfig.graalvmHome.isPresent()) { File file = Paths.get(nativeConfig.graalvmHome.get(), "bin", executableName).toFile(); if (file.exists()) { - return new NativeImageBuildLocalRunner(file.getAbsolutePath()); + return new NativeImageBuildLocalRunner(file.getAbsolutePath(), outputDir); } } @@ -399,7 +395,7 @@ private static NativeImageBuildLocalRunner getNativeImageBuildLocalRunner(Native if (javaHome != null) { File file = new File(javaHome, "bin/" + executableName); if (file.exists()) { - return new NativeImageBuildLocalRunner(file.getAbsolutePath()); + return new NativeImageBuildLocalRunner(file.getAbsolutePath(), outputDir); } } @@ -412,7 +408,7 @@ private static NativeImageBuildLocalRunner getNativeImageBuildLocalRunner(Native if (dir.isDirectory()) { File file = new File(dir, executableName); if (file.exists()) { - return new NativeImageBuildLocalRunner(file.getAbsolutePath()); + return new NativeImageBuildLocalRunner(file.getAbsolutePath(), outputDir); } } } @@ -446,51 +442,6 @@ private static String testGCCArgument(String argument) { return ""; } - private boolean objcopyExists() { - // System path - String systemPath = System.getenv(PATH); - if (systemPath != null) { - String[] pathDirs = systemPath.split(File.pathSeparator); - for (String pathDir : pathDirs) { - File dir = new File(pathDir); - if (dir.isDirectory()) { - File file = new File(dir, "objcopy"); - if (file.exists()) { - return true; - } - } - } - } - - return false; - } - - private void splitDebugSymbols(Path executable) { - Path symbols = Paths.get(String.format("%s.debug", executable.toString())); - objcopy("--only-keep-debug", executable.toString(), symbols.toString()); - objcopy(String.format("--add-gnu-debuglink=%s", symbols.toString()), executable.toString()); - } - - private static void objcopy(String... args) { - final List command = new ArrayList<>(args.length + 1); - command.add("objcopy"); - command.addAll(Arrays.asList(args)); - if (log.isDebugEnabled()) { - log.debugf("Execute %s", String.join(" ", command)); - } - Process process = null; - try { - process = new ProcessBuilder(command).start(); - process.waitFor(); - } catch (IOException | InterruptedException e) { - throw new RuntimeException("Unable to invoke objcopy", e); - } finally { - if (process != null) { - process.destroy(); - } - } - } - private static class NativeImageInvokerInfo { private final List args;