diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java index 18807695b95290..5d6da17c24db4a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java @@ -3,9 +3,12 @@ import static io.quarkus.deployment.pkg.steps.LinuxIDUtil.getLinuxID; import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -27,7 +30,6 @@ import io.quarkus.deployment.util.ProcessUtil; public class UpxCompressionBuildStep { - private static final Logger log = Logger.getLogger(UpxCompressionBuildStep.class); /** @@ -40,6 +42,7 @@ public void compress(NativeConfig nativeConfig, NativeImageRunnerBuildItem nativ NativeImageBuildItem image, BuildProducer upxCompressedProducer, BuildProducer artifactResultProducer) { + if (nativeConfig.compression().level().isEmpty()) { log.debug("UPX compression disabled"); return; @@ -49,28 +52,64 @@ public void compress(NativeConfig nativeConfig, NativeImageRunnerBuildItem nativ Optional upxPathFromSystem = getUpxFromSystem(); if (upxPathFromSystem.isPresent()) { log.debug("Running UPX from system path"); - if (!runUpxFromHost(upxPathFromSystem.get(), image.getPath().toFile(), nativeConfig)) { - throw new IllegalStateException("Unable to compress the native executable"); - } + handleCompressionResult( + runUpxFromHost(upxPathFromSystem.get(), image.getPath().toFile(), nativeConfig), image); } else if (nativeConfig.remoteContainerBuild()) { - log.errorf("Compression of native executables is not yet implemented for remote container builds."); - throw new IllegalStateException( - "Unable to compress the native executable: Compression of native executables is not yet supported for remote container builds"); + handleCompressionResult(new CompressionResult(CompressionResultType.NOT_IMPLEMENTED)); } else if (nativeImageRunner.isContainerBuild()) { log.infof("Running UPX from a container using the builder image: " + effectiveBuilderImage); - if (!runUpxInContainer(image, nativeConfig, effectiveBuilderImage)) { - throw new IllegalStateException("Unable to compress the native executable"); - } + handleCompressionResult(runUpxInContainer(image, nativeConfig), image); } else { - log.errorf("Unable to compress the native executable. Either install `upx` from https://upx.github.io/" + - " on your machine, or enable in-container build using `-Dquarkus.native.container-build=true`."); - throw new IllegalStateException("Unable to compress the native executable: `upx` not available"); + handleCompressionResult(new CompressionResult(CompressionResultType.UPX_NOT_INSTALLED)); } - log.infof("Native executable compressed: %s", image.getPath().toFile().getAbsolutePath()); upxCompressedProducer.produce(new UpxCompressedBuildItem()); } - private boolean runUpxFromHost(File upx, File executable, NativeConfig nativeConfig) { + private void handleCompressionResult(CompressionResult result) { + handleCompressionResult(result, null); + } + + private void handleCompressionResult(CompressionResult result, NativeImageBuildItem image) { + var failureMessage = "Unable to compress the native executable"; + var upxNotAvailableMessage = "Unable to compress the native executable: `upx` not available"; + switch (result.type()) { + case SUCCESS -> log.infof( + "Native executable compressed: %s", + Objects.requireNonNull(image).getPath().toFile().getAbsolutePath()); + case ALREADY_COMPRESSED -> log.infof( + "Native executable already compressed: Skipping %s", + UpxCompressionBuildStep.class.getSimpleName()); + case FAILURE_WITH_EXIT_CODE -> { + log.errorf("Command: %s failed with exit code %d", + result.commandLine(), + result.exitCode()); + throw new IllegalStateException(failureMessage); + } + case FAILURE -> { + log.errorf(result.throwable(), + "Command: %s failed", + result.commandLine()); + throw new IllegalStateException(failureMessage); + } + case UPX_NOT_INSTALLED -> { + log.error("Unable to compress the native executable. Either install `upx` from https://upx.github.io/ " + + "on your machine, or enable in-container build using `-Dquarkus.native.container-build=true`."); + throw new IllegalStateException(upxNotAvailableMessage); + } + case UPX_NOT_PROVIDED -> { + log.errorf("Command: %s failed because the builder image does not provide the `upx` executable", + result.commandLine()); + throw new IllegalStateException(upxNotAvailableMessage); + } + case NOT_IMPLEMENTED -> { + var message = "Compression of native executables is not yet implemented for remote container builds."; + log.error(message); + throw new IllegalStateException("Unable to compress the native executable: " + message); + } + } + } + + private CompressionResult runUpxFromHost(File upx, File executable, NativeConfig nativeConfig) { List extraArgs = nativeConfig.compression().additionalArgs().orElse(Collections.emptyList()); List args = Stream.of( Stream.of(upx.getAbsolutePath()), @@ -78,35 +117,17 @@ private boolean runUpxFromHost(File upx, File executable, NativeConfig nativeCon extraArgs.stream(), Stream.of(executable.getAbsolutePath())) .flatMap(Function.identity()) - .collect(Collectors.toList()); + .toList(); log.infof("Executing %s", String.join(" ", args)); final ProcessBuilder processBuilder = new ProcessBuilder(args) .directory(executable.getAbsoluteFile().getParentFile()) .redirectOutput(ProcessBuilder.Redirect.PIPE) .redirectError(ProcessBuilder.Redirect.PIPE); - Process process = null; - try { - process = processBuilder.start(); - ProcessUtil.streamOutputToSysOut(process); - final int exitCode = process.waitFor(); - if (exitCode != 0) { - log.errorf("Command: " + String.join(" ", args) + " failed with exit code " + exitCode); - return false; - } - return true; - } catch (Exception e) { - log.errorf("Command: " + String.join(" ", args) + " failed", e); - return false; - } finally { - if (process != null) { - process.destroy(); - } - } + return executeCompression(processBuilder, args); } - private boolean runUpxInContainer(NativeImageBuildItem nativeImage, NativeConfig nativeConfig, - String effectiveBuilderImage) { + private CompressionResult runUpxInContainer(NativeImageBuildItem nativeImage, NativeConfig nativeConfig) { List extraArgs = nativeConfig.compression().additionalArgs().orElse(Collections.emptyList()); List commandLine = new ArrayList<>(); @@ -141,6 +162,7 @@ private boolean runUpxInContainer(NativeImageBuildItem nativeImage, NativeConfig Collections.addAll(commandLine, "-v", volumeOutputPath + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH + ":z"); + var effectiveBuilderImage = nativeConfig.builderImage().getEffectiveImage(); commandLine.add(effectiveBuilderImage); if (nativeConfig.compression().level().isPresent()) { commandLine.add(getCompressionLevel(nativeConfig.compression().level().getAsInt())); @@ -153,30 +175,47 @@ private boolean runUpxInContainer(NativeImageBuildItem nativeImage, NativeConfig final ProcessBuilder processBuilder = new ProcessBuilder(commandLine) .redirectOutput(ProcessBuilder.Redirect.PIPE) .redirectError(ProcessBuilder.Redirect.PIPE); + + return executeCompression(processBuilder, commandLine); + } + + private CompressionResult executeCompression(final ProcessBuilder processBuilder, final List commandLine) { Process process = null; try { process = processBuilder.start(); ProcessUtil.streamOutputToSysOut(process); final int exitCode = process.waitFor(); - if (exitCode != 0) { - if (exitCode == 127) { - log.errorf("Command: %s failed because the builder image does not provide the `upx` executable", - String.join(" ", commandLine)); - } else { - log.errorf("Command: %s failed with exit code %d", String.join(" ", commandLine), exitCode); - } - return false; - } - return true; - } catch (Exception e) { - log.errorf("Command: " + String.join(" ", commandLine) + " failed", e); - return false; + return switch (exitCode) { + case 0 -> new CompressionResult(CompressionResultType.SUCCESS); + case 2 -> checkExecutableAlreadyCompressed(process, commandLine); + case 127 -> new CompressionResult(CompressionResultType.UPX_NOT_PROVIDED, commandLine); + default -> new CompressionResult(CompressionResultType.FAILURE_WITH_EXIT_CODE, exitCode, commandLine); + }; + } catch (IOException e) { + return new CompressionResult(CompressionResultType.FAILURE, commandLine, e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return new CompressionResult(CompressionResultType.FAILURE, commandLine, e); } finally { if (process != null) { process.destroy(); } } + } + + private CompressionResult checkExecutableAlreadyCompressed(final Process process, List commandLine) { + try (var reader = process.errorReader(Charset.defaultCharset())) { + var errorMessage = reader.lines().collect(Collectors.joining(" ")); + boolean isAlreadyCompressed = errorMessage.contains("AlreadyPackedException"); + if (isAlreadyCompressed) { + log.debug(errorMessage); + return new CompressionResult(CompressionResultType.ALREADY_COMPRESSED); + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + return new CompressionResult(CompressionResultType.FAILURE_WITH_EXIT_CODE, process.exitValue(), commandLine); } private String getCompressionLevel(int level) { @@ -210,4 +249,50 @@ private Optional getUpxFromSystem() { private static String getUpxExecutableName() { return SystemUtils.IS_OS_WINDOWS ? "upx.exe" : "upx"; } + + private enum CompressionResultType { + SUCCESS, + ALREADY_COMPRESSED, + FAILURE_WITH_EXIT_CODE, + FAILURE, + UPX_NOT_INSTALLED, + UPX_NOT_PROVIDED, + NOT_IMPLEMENTED + } + + private record CompressionResult( + CompressionResultType type, + Optional optExitCode, + Optional> optCommandLine, + Optional optThrowable) { + + CompressionResult(CompressionResultType type) { + this(type, Optional.empty(), Optional.empty(), Optional.empty()); + } + + CompressionResult(CompressionResultType type, List commandLine) { + this(type, Optional.empty(), Optional.of(commandLine), Optional.empty()); + } + + CompressionResult(CompressionResultType type, int exitCode, List commandLine) { + this(type, Optional.of(exitCode), Optional.of(commandLine), Optional.empty()); + } + + CompressionResult(CompressionResultType type, List commandLine, Throwable throwable) { + this(type, Optional.empty(), Optional.of(commandLine), Optional.of(throwable)); + } + + public int exitCode() { + return optExitCode.orElse(Integer.MIN_VALUE); + } + + public String commandLine() { + var commandLine = optCommandLine.orElse(Collections.emptyList()); + return String.join(" ", commandLine); + } + + public Throwable throwable() { + return optThrowable.orElse(new IllegalStateException("Missing throwable")); + } + } }