From b04547e34787ff8b98a3be83de90c8487653f92d Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Mon, 23 May 2022 17:15:35 +0300 Subject: [PATCH] Introduce Quiltflower decompiler support Quiltflower is a new decompiler based on Fernflower that generates better decompiled code. This change also deprecates the Fernflower configuration in favor of the new Quiltflower configuration --- .../quarkus/deployment/pkg/PackageConfig.java | 30 +++ .../pkg/steps/JarResultBuildStep.java | 255 +++++++++++++----- .../src/main/asciidoc/writing-extensions.adoc | 2 +- 3 files changed, 217 insertions(+), 70 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 7ee2993cbd908..7dcd4c97a50ed 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 @@ -175,8 +175,15 @@ public class PackageConfig { * Fernflower Decompiler configuration */ @ConfigItem + @Deprecated(forRemoval = true) public FernflowerConfig fernflower; + /** + * Quiltflower Decompiler configuration + */ + @ConfigItem + public QuiltFlowerConfig quiltflower; + /** * If set to {@code true}, it will result in the Quarkus writing the transformed application bytecode * to the build tool's output directory. @@ -217,6 +224,7 @@ public boolean isUberJar() { } @ConfigGroup + @Deprecated(forRemoval = true) public static class FernflowerConfig { /** @@ -238,4 +246,26 @@ public static class FernflowerConfig { @ConfigItem(defaultValue = "${user.home}/.quarkus") public String jarDirectory; } + + @ConfigGroup + public static class QuiltFlowerConfig { + /** + * 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 version of Quiltflower to use + */ + @ConfigItem(defaultValue = "1.8.1") + public String version; + + /** + * 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 cb2d81c4b8f00..b260a92152711 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 @@ -575,24 +575,30 @@ 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 - } - } + Decompiler decompiler = null; + if (packageConfig.fernflower.enabled || packageConfig.quiltflower.enabled) { decompiledOutputDir = buildDir.getParent().resolve("decompiled"); FileUtil.deleteDirectory(decompiledOutputDir); Files.createDirectory(decompiledOutputDir); + if (packageConfig.fernflower.enabled) { + decompiler = new Decompiler.FernflowerDecompiler(); + Path jarDirectory = Paths.get(packageConfig.fernflower.jarDirectory); + if (!Files.exists(jarDirectory)) { + Files.createDirectory(jarDirectory); + } + decompiler.init(new Decompiler.Context(packageConfig.fernflower.hash, jarDirectory, decompiledOutputDir)); + decompiler.downloadIfNecessary(); + } else if (packageConfig.quiltflower.enabled) { + decompiler = new Decompiler.QuiltflowerDecompiler(); + Path jarDirectory = Paths.get(packageConfig.quiltflower.jarDirectory); + if (!Files.exists(jarDirectory)) { + Files.createDirectory(jarDirectory); + } + decompiler.init(new Decompiler.Context(packageConfig.quiltflower.version, jarDirectory, decompiledOutputDir)); + decompiler.downloadIfNecessary(); + } } List jars = new ArrayList<>(); @@ -616,8 +622,8 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, } } } - if (fernflowerJar != null) { - wasDecompiledSuccessfully &= decompile(fernflowerJar, decompiledOutputDir, transformedZip); + if (decompiler != null) { + wasDecompiledSuccessfully &= decompiler.decompile(transformedZip); } } //now generated classes and resources @@ -641,8 +647,8 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, Files.write(target, i.getClassData()); } } - if (fernflowerJar != null) { - wasDecompiledSuccessfully &= decompile(fernflowerJar, decompiledOutputDir, generatedZip); + if (decompiler != null) { + wasDecompiledSuccessfully &= decompiler.decompile(generatedZip); } if (wasDecompiledSuccessfully && (decompiledOutputDir != null)) { @@ -837,58 +843,6 @@ private Set getRemovedKeys(ClassLoadingConfig classLoadingConfig) { return removed; } - 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(ProcessBuilder.Redirect.DISCARD.file()) - .redirectOutput(ProcessBuilder.Redirect.DISCARD.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(Set parentFirstArtifacts, OutputTargetBuildItem outputTargetBuildItem, Map> runtimeArtifacts, Path libDir, Path baseLib, List jars, boolean allowParentFirst, StringBuilder classPath, ResolvedDependency appDep, @@ -1557,4 +1511,167 @@ public boolean test(Path path, BasicFileAttributes basicFileAttributes) { return basicFileAttributes.isRegularFile() && path.toString().endsWith(".json"); } } + + private interface Decompiler { + + void init(Context context); + + /** + * @return {@code true} if the decompiler was successfully download or already exists + */ + boolean downloadIfNecessary(); + + /** + * @return {@code true} if the decompilation process was successful + */ + boolean decompile(Path jarToDecompile); + + class Context { + final String versionStr; + final Path jarLocation; + final Path decompiledOutputDir; + + public Context(String versionStr, Path jarLocation, Path decompiledOutputDir) { + this.versionStr = versionStr; + this.jarLocation = jarLocation; + this.decompiledOutputDir = decompiledOutputDir; + } + + } + + class FernflowerDecompiler implements Decompiler { + + private Context context; + private Path decompilerJar; + + @Override + public void init(Context context) { + this.context = context; + this.decompilerJar = context.jarLocation.resolve(String.format("fernflower-%s.jar", context.versionStr)); + } + + @Override + public boolean downloadIfNecessary() { + if (Files.exists(decompilerJar)) { + return true; + } + String downloadURL = String.format("https://jitpack.io/com/github/fesh0r/fernflower/%s/fernflower-%s.jar", + context.versionStr, context.versionStr); + try (BufferedInputStream in = new BufferedInputStream(new URL(downloadURL).openStream()); + FileOutputStream fileOutputStream = new FileOutputStream(decompilerJar.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; + } + } + + @Override + public boolean decompile(Path jarToDecompile) { + int exitCode; + try { + ProcessBuilder processBuilder = new ProcessBuilder( + Arrays.asList("java", "-jar", decompilerJar.toAbsolutePath().toString(), + jarToDecompile.toAbsolutePath().toString(), + context.decompiledOutputDir.toAbsolutePath().toString())); + if (log.isDebugEnabled()) { + processBuilder.inheritIO(); + } else { + processBuilder.redirectError(ProcessBuilder.Redirect.DISCARD.file()) + .redirectOutput(ProcessBuilder.Redirect.DISCARD.file()); + } + exitCode = processBuilder.start().waitFor(); + } catch (Exception e) { + log.error("Failed to launch 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 = context.decompiledOutputDir.resolve(jarFileName); + try { + ZipUtils.unzip(decompiledJar, context.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; + } + } + + class QuiltflowerDecompiler implements Decompiler { + + private Context context; + private Path decompilerJar; + + @Override + public void init(Context context) { + this.context = context; + this.decompilerJar = context.jarLocation.resolve(String.format("quiltflower-%s.jar", context.versionStr)); + } + + @Override + public boolean downloadIfNecessary() { + if (Files.exists(decompilerJar)) { + return true; + } + String downloadURL = String.format( + "https://github.com/QuiltMC/quiltflower/releases/download/%s/quiltflower-%s.jar", + context.versionStr, context.versionStr); + try (BufferedInputStream in = new BufferedInputStream(new URL(downloadURL).openStream()); + FileOutputStream fileOutputStream = new FileOutputStream(decompilerJar.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 Quiltflower from " + downloadURL, e); + return false; + } + } + + @Override + public boolean decompile(Path jarToDecompile) { + int exitCode; + try { + int dotIndex = jarToDecompile.getFileName().toString().indexOf('.'); + String fileName = jarToDecompile.getFileName().toString().substring(0, dotIndex); + ProcessBuilder processBuilder = new ProcessBuilder( + Arrays.asList("java", "-jar", decompilerJar.toAbsolutePath().toString(), + jarToDecompile.toAbsolutePath().toString(), + context.decompiledOutputDir.resolve(fileName).toAbsolutePath().toString())); + if (log.isDebugEnabled()) { + processBuilder.inheritIO(); + } else { + processBuilder.redirectError(ProcessBuilder.Redirect.DISCARD.file()) + .redirectOutput(ProcessBuilder.Redirect.DISCARD.file()); + } + exitCode = processBuilder.start().waitFor(); + } catch (Exception e) { + log.error("Failed to launch decompiler.", e); + return false; + } + + if (exitCode != 0) { + log.errorf("Quiltflower decompiler exited with error code: %d.", exitCode); + return false; + } + + return true; + } + } + } + } diff --git a/docs/src/main/asciidoc/writing-extensions.adoc b/docs/src/main/asciidoc/writing-extensions.adoc index 6cedd9ceb5d75..e93cb2493e55e 100644 --- a/docs/src/main/asciidoc/writing-extensions.adoc +++ b/docs/src/main/asciidoc/writing-extensions.adoc @@ -2164,7 +2164,7 @@ The only particular aspect of writing Quarkus extensions in Eclipse is that APT Quarkus generates a lot of classes during the build phase and in many cases also transforms existing classes. It is often extremely useful to see the generated bytecode and transformed classes during the development of an extension. -If you set the `quarkus.package.fernflower.enabled` property to `true` then 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). +If you set the `quarkus.package.quiltflower.enabled` property to `true` then Quarkus will download and invoke the https://github.com/QuiltMC/quiltflower[Quiltflower decompiler] and dump the result in the `decompiled` directory of the build tool output (`target/decompiled` for Maven for example). NOTE: This property only works during a normal production build (i.e. not for dev mode/tests) and when `fast-jar` packaging type is used (the default behavior).