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

Introduce Quiltflower decompiler support #25729

Merged
merged 1 commit into from
May 24, 2022
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 @@ -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.
Expand Down Expand Up @@ -217,6 +224,7 @@ public boolean isUberJar() {
}

@ConfigGroup
@Deprecated(forRemoval = true)
public static class FernflowerConfig {

/**
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -575,24 +575,30 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem,
}
Map<ArtifactKey, 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)) {
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<Path> jars = new ArrayList<>();
Expand All @@ -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
Expand All @@ -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)) {
Expand Down Expand Up @@ -837,58 +843,6 @@ private Set<ArtifactKey> 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<ArtifactKey> parentFirstArtifacts, OutputTargetBuildItem outputTargetBuildItem,
Map<ArtifactKey, List<Path>> runtimeArtifacts, Path libDir, Path baseLib, List<Path> jars,
boolean allowParentFirst, StringBuilder classPath, ResolvedDependency appDep,
Expand Down Expand Up @@ -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;
}
}
}

}
2 changes: 1 addition & 1 deletion docs/src/main/asciidoc/writing-extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down