diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java index a4ea74168d02d..faff6f520f770 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; @@ -23,7 +24,7 @@ public class PackageConfig { /** * The requested output type. - * + *

* The default built in types are 'jar' (which will use 'fast-jar'), 'legacy-jar' for the pre-1.12 default jar * packaging, 'uber-jar' and 'native'. */ @@ -39,7 +40,7 @@ public class PackageConfig { /** * The entry point of the application. This can either be a a fully qualified name of a standard Java * class with a main method, or {@link io.quarkus.runtime.QuarkusApplication}. - * + *

* If your application has main classes annotated with {@link io.quarkus.runtime.annotations.QuarkusMain} * then this can also reference the name given in the annotation, to avoid the need to specify fully qualified * names in the config. @@ -95,11 +96,11 @@ public class PackageConfig { /** * This is an advanced option that only takes effect for the mutable-jar format. - * + *

* If this is specified a directory of this name will be created in the jar distribution. Users can place * jar files in this directory, and when re-augmentation is performed these will be processed and added to the * class-path. - * + *

* Note that before reaugmentation has been performed these jars will be ignored, and if they are updated the app * should be reaugmented again. */ @@ -115,6 +116,12 @@ public class PackageConfig { @ConfigItem(defaultValue = "true") public boolean includeDependencyList; + /** + * Fernflower Decompiler configuration + */ + @ConfigItem + public FernflowerConfig fernflower; + public boolean isAnyJarType() { return (type.equalsIgnoreCase(PackageConfig.JAR) || type.equalsIgnoreCase(PackageConfig.FAST_JAR) || @@ -134,4 +141,27 @@ public boolean isLegacyJar() { return (type.equalsIgnoreCase(PackageConfig.LEGACY_JAR) || type.equalsIgnoreCase(PackageConfig.LEGACY)); } + + @ConfigGroup + public static class FernflowerConfig { + + /** + * An advanced option that will decompile generated and transformed bytecode into the 'decompiled' directory. + * This is only taken into account when fast-jar is used. + */ + @ConfigItem(defaultValue = "false") + public boolean enabled; + + /** + * The git hash to use to download the fernflower tool from https://jitpack.io/com/github/fesh0r/fernflower/ + */ + @ConfigItem(defaultValue = "dbf407a655") + public String hash; + + /** + * The directory into which to save the fernflower tool if it doesn't exist + */ + @ConfigItem(defaultValue = "${user.home}/.quarkus") + public String jarDirectory; + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java index 747fac6bfd577..f9475b4f36def 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java @@ -5,7 +5,9 @@ import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.nio.file.StandardOpenOption.WRITE; +import java.io.BufferedInputStream; import java.io.BufferedWriter; +import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -19,6 +21,7 @@ import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; @@ -80,6 +83,7 @@ import io.quarkus.deployment.pkg.builditem.NativeImageSourceJarBuildItem; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.deployment.pkg.builditem.UberJarRequiredBuildItem; +import io.quarkus.deployment.util.FileUtil; /** * This build step builds both the thin jars and uber jars. @@ -481,6 +485,26 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, } Map> copiedArtifacts = new HashMap<>(); + Path fernflowerJar = null; + Path decompiledOutputDir = null; + boolean wasDecompiledSuccessfully = true; + if (packageConfig.fernflower.enabled) { + Path jarDirectory = Paths.get(packageConfig.fernflower.jarDirectory); + if (!Files.exists(jarDirectory)) { + Files.createDirectory(jarDirectory); + } + fernflowerJar = jarDirectory.resolve(String.format("fernflower-%s.jar", packageConfig.fernflower.hash)); + if (!Files.exists(fernflowerJar)) { + boolean downloadComplete = downloadFernflowerJar(packageConfig, fernflowerJar); + if (!downloadComplete) { + fernflowerJar = null; // will ensure that no decompilation takes place + } + } + decompiledOutputDir = buildDir.getParent().resolve("decompiled"); + FileUtil.deleteDirectory(decompiledOutputDir); + Files.createDirectory(decompiledOutputDir); + } + List jars = new ArrayList<>(); List bootJars = new ArrayList<>(); //we process in order of priority @@ -500,6 +524,9 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, } } } + if (fernflowerJar != null) { + wasDecompiledSuccessfully &= decompile(fernflowerJar, decompiledOutputDir, transformedZip); + } } //now generated classes and resources Path generatedZip = quarkus.resolve(GENERATED_BYTECODE_JAR); @@ -522,6 +549,14 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, Files.write(target, i.getClassData()); } } + if (fernflowerJar != null) { + wasDecompiledSuccessfully &= decompile(fernflowerJar, decompiledOutputDir, generatedZip); + } + + if (wasDecompiledSuccessfully && (decompiledOutputDir != null)) { + log.info("The decompiled output can be found at: " + decompiledOutputDir.toAbsolutePath().toString()); + } + //now the application classes Path runnerJar = appDir .resolve(outputTargetBuildItem.getBaseName() + ".jar"); @@ -667,6 +702,58 @@ public void accept(Path path) { return new JarBuildItem(initJar, null, libDir, packageConfig.type, null); } + private boolean downloadFernflowerJar(PackageConfig packageConfig, Path fernflowerJar) { + String downloadURL = String.format("https://jitpack.io/com/github/fesh0r/fernflower/%s/fernflower-%s.jar", + packageConfig.fernflower.hash, packageConfig.fernflower.hash); + try (BufferedInputStream in = new BufferedInputStream(new URL(downloadURL).openStream()); + FileOutputStream fileOutputStream = new FileOutputStream(fernflowerJar.toFile())) { + byte[] dataBuffer = new byte[1024]; + int bytesRead; + while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { + fileOutputStream.write(dataBuffer, 0, bytesRead); + } + return true; + } catch (IOException e) { + log.error("Unable to download Fernflower from " + downloadURL, e); + return false; + } + } + + private boolean decompile(Path fernflowerJar, Path decompiledOutputDir, Path jarToDecompile) { + int exitCode; + try { + ProcessBuilder processBuilder = new ProcessBuilder( + Arrays.asList("java", "-jar", fernflowerJar.toAbsolutePath().toString(), + jarToDecompile.toAbsolutePath().toString(), decompiledOutputDir.toAbsolutePath().toString())); + if (log.isDebugEnabled()) { + processBuilder.inheritIO(); + } else { + processBuilder.redirectError(NULL_FILE); + processBuilder.redirectOutput(NULL_FILE); + } + exitCode = processBuilder.start().waitFor(); + } catch (Exception e) { + log.error("Failed to launch Fernflower decompiler.", e); + return false; + } + + if (exitCode != 0) { + log.errorf("Fernflower decompiler exited with error code: %d.", exitCode); + return false; + } + + String jarFileName = jarToDecompile.getFileName().toString(); + Path decompiledJar = decompiledOutputDir.resolve(jarFileName); + try { + ZipUtils.unzip(decompiledJar, decompiledOutputDir.resolve(jarFileName.replace(".jar", ""))); + Files.deleteIfExists(decompiledJar); + } catch (IOException ignored) { + // it doesn't really matter if we can't unzip the jar as we do it merely for user convenience + } + + return true; + } + private void copyDependency(CurateOutcomeBuildItem curateOutcomeBuildItem, Map> runtimeArtifacts, Path libDir, Path baseLib, List jars, boolean allowParentFirst, StringBuilder classPath, AppDependency appDep) throws IOException { @@ -1226,4 +1313,9 @@ public boolean test(Path path, BasicFileAttributes basicFileAttributes) { return basicFileAttributes.isRegularFile() && path.toString().endsWith(".json"); } } + + // copied from Java 9 + // TODO remove when we move to Java 11 + + private static final File NULL_FILE = new File(SystemUtils.IS_OS_WINDOWS ? "NUL" : "/dev/null"); } diff --git a/docs/src/main/asciidoc/writing-extensions.adoc b/docs/src/main/asciidoc/writing-extensions.adoc index e4eab8d04ffe5..98cbf49b487a7 100644 --- a/docs/src/main/asciidoc/writing-extensions.adoc +++ b/docs/src/main/asciidoc/writing-extensions.adoc @@ -2034,6 +2034,13 @@ The only particular aspect of writing Quarkus extensions in Eclipse is that APT === Troubleshooting / Debugging Tips +==== Automatically decompile Quarkus bytecode + +As has been mentioned many times in this document, Quarkus always generates a host of classes as part of the build and in many cases also transforms existing classes. +When writing extensions it is often extremely useful to see a decompiled version (i.e. Java source code) of those classes, which is why Quarkus provides the `quarkus.package.fernflower.enabled` property (which is disabled by default). +When the flag is enabled, Quarkus will download and invoke the https://github.com/JetBrains/intellij-community/tree/master/plugins/java-decompiler/engine[Fernflower decompiler] and dump the result in the `decompiled` directory +of the build tool output (`target/decompiled` for Maven for example). + ==== Dump the Generated Classes to the File System During the augmentation phase Quarkus extensions generate new and modify existing classes for various purposes.