Skip to content

Commit

Permalink
Integrate Fernflower decompiler into Quarkus build of fast-jar
Browse files Browse the repository at this point in the history
This makes it easy for Quarkus developers and extension authors
to see the bytecode that Quarkus produces

Fixes: quarkusio#612
  • Loading branch information
geoand committed Feb 18, 2021
1 parent 40b89fd commit 3832f56
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -23,7 +24,7 @@ public class PackageConfig {

/**
* The requested output type.
*
* <p>
* 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'.
*/
Expand All @@ -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}.
*
* <p>
* 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.
Expand Down Expand Up @@ -95,11 +96,11 @@ public class PackageConfig {

/**
* This is an advanced option that only takes effect for the mutable-jar format.
*
* <p>
* 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.
*
* <p>
* Note that before reaugmentation has been performed these jars will be ignored, and if they are updated the app
* should be reaugmented again.
*/
Expand All @@ -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) ||
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -481,6 +485,23 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem,
}
Map<AppArtifactKey, List<Path>> 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)) {
downloadFernflowerJar(packageConfig, fernflowerJar);
}
decompiledOutputDir = buildDir.getParent().resolve("decompiled");
FileUtil.deleteDirectory(decompiledOutputDir);
Files.createDirectory(decompiledOutputDir);
}

List<Path> jars = new ArrayList<>();
List<Path> bootJars = new ArrayList<>();
//we process in order of priority
Expand All @@ -500,6 +521,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);
Expand All @@ -522,6 +546,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");
Expand Down Expand Up @@ -667,6 +699,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<AppArtifactKey, List<Path>> runtimeArtifacts,
Path libDir, Path baseLib, List<Path> jars, boolean allowParentFirst, StringBuilder classPath, AppDependency appDep)
throws IOException {
Expand Down Expand Up @@ -1226,4 +1308,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");
}
7 changes: 7 additions & 0 deletions docs/src/main/asciidoc/writing-extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 3832f56

Please sign in to comment.