libGDX + ProGuard: How to obfuscate the Desktop JAR file

I have recently published my libGDX game Rainbow Diamonds for desktop computers. One problem I had was how to obfuscate the desktop JAR file for distribution, and this is how I solved it.

I’m using openjdk 11.0.13 distributed as part of Android Studio.

After a few searches on internet, I found this post on esotericsoftware.com. Please, check it out as that is the base solution for my custom one.

ProGuard rules

As the solution is based on ProGuard, we need a file to specify how we want to trafsform our code. I use a file named proguard-desktop.txt, placed in the desktop/ project folder with the following content:

-verbose
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-keepattributes Signature,InnerClasses,SourceFile,LineNumberTable

-dontwarn com.badlogic.**
-dontwarn org.lwjgl.**
-dontwarn org.objectweb.**

-keep class com.badlogic.**
-keep class org.lwjgl.**
-keep class org.objectweb.**

-keepclassmembers class com.badlogic.** { *; }
-keepclassmembers class org.lwjgl.** { *; }
-keepclassmembers class org.objectweb.** { *; }

-keepclasseswithmembernames class * {
  native <methods>;
}

-keep public class your.game.package.name.desktop.DesktopLauncher {
    public static void main(java.lang.String[]);
}

-keepclassmembers,allowoptimization enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

Note: The previous file can vary depending on your project needs.

ProGuard gradle task

Then, we will a create a ProGuard task to obfuscate the code. Let’s declare it in /desktop/build.gradle (note that is the gradle file of the Desktop project). After the dist task, we insert our new one:

task obfuscate(dependsOn: dist, type: proguard.gradle.ProGuardTask) {

    // Files
    def inputFile = dist.outputs.files.singleFile.path
    def outputJar = dist.destinationDirectory.file(<your_desired_jar_name.jar>)
    def outputJarFilePath = outputJar.get().asFile.path

    // Task configuration
    configuration files("proguard-desktop.txt")
    libraryjars(<jar_with_base_classes>)
    injars  files(inputFile)
    outjars files(outputJarFilePath)
}

The declared task has three main sections:

  • Task declaration
    We indicate the task type and dependency, as this task requires the build from the dist task.
  • Files
    We use the output from the dist task as the input for our (obfuscate) task. This way we will create a new obfuscated jar (place the desired name in <your_desired_jar_name.jar>) in the same folder as the dist did.
  • Task configuration
    We use the options configuration, libraryjars, injars and outjars. You can see all the available options on its reference page.

Task configuration

  • configuration
    We use the file proguard-desktop.txt we created in the first step of this guide.
  • injars
    The input of the task is the output JAR from the dist task.
  • outjars
    This is the obfuscated JAR the task will create.
  • libraryjars
    This param can be confused, so I will try to explain it in the next section.

 

libraryjars

From the official doc, I extract the following information about this param:

…The specified library jars should at least contain the class files that are extended by application class files. … This means that you explicitly have to specify the run-time jar that your code will use …

So we need to provide the classes our project classes are extending from. So, for an standard project, we need to provide at least the base Java classes (java.lang.Object, lava.lang.String, etc.). If you are using Java 8 or older versions, this classes are distributed in JAR files within the JRE. So you can use something like this:

libraryjars files("<path_to_your_jre>/lib/rt.jar", "<path_to_your_jre>/lib/jce.jar")

But if you are using a newer Java version (like me), you no longer have those JARs in your JRE, as from Java 9 these classes are distributed in a Java module file. This file is located at <JRE_LOCATION>/lib/modules. Well, My first try was trying to directly use that file path as libraryjars param, but at least for me it didn’t work. So I decided to create a JAR with the classes stored in /lib/modules file.

Creating the modules JAR

First, I recommend to make the following steps with a copy of the modules file. If you damage the original modules file probably you will get an unusable JRE.

Copying the modules file

mkdir MODULES_JAR
cp <JRE>/lib/modules MODULES_JAR
cd MODULES_JAR

Extracting the classes

jimage extract --dir=temp modules
cd temp

Preparing the JAR classes

After the classes extraction we have all the classes of the modules file in the temp/ folder. The problem now is that the folder structure we have is not directly packajable in a JAR file, as all of the classes are classified in a first folder with the module name they bellong.

For example, suppossing we are in the temp/ folder, the java.lang.Object class is located in java.base/java/lang/Object.class, but it should be just in java/lang/Object.class, as said its package declaration.

So now we need to put all the classes of the different modules in a common folder structure. Basically if we have something like this in the temp/ folder right now:

temp/
  |- java.base/
  |    |- java/
  |        |- lang/
  |           |- Object.class
  |           |- String.class
  |           |- ...
  |- java.logging/
  |    |- java/
  |        |- util/
  |            |- logging/
  |                |- ConsoleHandler.class
  |                |- ...
  |- ...

we need to remove the first level of folders, having a final file structure like this:

temp/
  |- java/
  |   |- lang/
  |      |- Object.class
  |      |- String.class
  |      |- ...
 
  |- java/
  |   |- util/
  |       |- logging/
  |           |- ConsoleHandler.class
  |           |- ...
  |- ...

If you like, I have created a script to make all this tedious work. You can find it here.

Packaging the classes

Inside the temp/ folder, we can execute the following command to package all the classes in a new JAR file.

jar --create --file=../modulesclasses.jar . 

Final ProGuard task

And now we have the JAR file with the required classes to be used to obfuscate our code.

task obfuscate(dependsOn: dist, type: proguard.gradle.ProGuardTask) {

    // Files
    def inputFile = dist.outputs.files.singleFile.path
    def outputJar = dist.destinationDirectory.file(<your_desired_jar_name.jar>)
    def outputJarFilePath = outputJar.get().asFile.path

    // Task configuration
    configuration files("proguard-desktop.txt")
    libraryjars(<your_path>/MODULES_JAR/modulesclasses.jar)
    injars  files(inputFile)
    outjars files(outputJarFilePath)
}

Final comments

This a very long tutorial about how I solved the original question (How to obfuscate my libGDX JAR for desktop distribution?). Probably this is not the only one option, and I don’t know if it is definetly the right way of solve this. But at least it’s a working solution and I hope it is a little bit easy for you if you have the same problem and don’t know exactly how to continue.

Thanks for reading this and good luck for your projects!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*