Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate Fernflower decompiler into Quarkus build of fast-jar #15162

Merged
merged 1 commit into from
Feb 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,26 @@ 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)) {
geoand marked this conversation as resolved.
Show resolved Hide resolved
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<Path> jars = new ArrayList<>();
List<Path> bootJars = new ArrayList<>();
//we process in order of priority
Expand All @@ -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);
Expand All @@ -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");
Expand Down Expand Up @@ -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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we download it rather than making it a build dependency?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My reasoning is that this is super unlikely to be used by regular users, so there should be no reason to make it an explicit dependency.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see it's not on Maven Central but maybe we could ask nicely?

Copy link
Contributor Author

@geoand geoand Feb 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was going to mention that as well.

For the time being it's only available on Jitpack because some fellow forked the Jetbrains repo and configured Jitpack.

I'll open an issue on the Jetbrains tracker to see if we can get it into Maven Central

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit more complicated than that. We had quite a lot of corporate users complaining it's hard to download things outside of Maven Central. So downloading from Maven Central is usually the way to go. Not sure downloading it always would be a big issue given how many artifacts people get to download to do a build. But I have no idea how big it is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also with Maven, you get signature checking, automatic updates with Dependabot and so on.

It's not on Maven atm so we can't really do anything about it but maybe we could ask?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I have no idea how big it is.

For the hash I have used, the jar is around 600kB

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to suggest making this default to true if the current user is stephane, but that jar thing looks like an attack vector :-/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hahahhah

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<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 +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");
}
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