From 0571fd77d18ae03d69fef6a997963d32de07a6aa Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 18 Feb 2021 11:41:42 +0200 Subject: [PATCH] Integrate Fernflower decompiler into Quarkus build of fast-jar This makes it easy for Quarkus developers and extension authors to see the bytecode that Quarkus produces Fixes: #612 --- .../quarkus/deployment/pkg/PackageConfig.java | 38 +++++++- .../pkg/steps/JarResultBuildStep.java | 88 +++++++++++++++++++ .../src/main/asciidoc/writing-extensions.adoc | 7 ++ 3 files changed, 129 insertions(+), 4 deletions(-) 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 a4ea74168d02da..faff6f520f7704 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 747fac6bfd5775..4b8fca90e21293 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. @@ -146,6 +150,7 @@ public class JarResultBuildStep { public static final String QUARKUS = "quarkus"; public static final String DEFAULT_FAST_JAR_DIRECTORY_NAME = "quarkus-app"; public static final String MP_CONFIG_FILE = "META-INF/microprofile-config.properties"; + private static final String FERNFLOWER_JAR = "fernflower.jar"; @BuildStep OutputTargetBuildItem outputTarget(BuildSystemTargetBuildItem bst, PackageConfig packageConfig) { @@ -481,6 +486,23 @@ 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(FERNFLOWER_JAR); + if (!Files.exists(fernflowerJar)) { + downloadFernflowerJar(packageConfig, fernflowerJar); + } + 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 +522,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 +547,14 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, Files.write(target, i.getClassData()); } } + if (fernflowerJar != null) { + wasDecompiledSuccessfully &= decompile(fernflowerJar, decompiledOutputDir, generatedZip); + } + + if (wasDecompiledSuccessfully) { + 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 +700,56 @@ public void accept(Path path) { return new JarBuildItem(initJar, null, libDir, packageConfig.type, null); } + private void 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); + } + } catch (IOException e) { + log.error("Unable to download Fernflower from " + downloadURL, e); + } + } + + 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 +1309,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 e4eab8d04ffe53..98cbf49b487a77 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.