- A little background info
- Putting together the ProGuard configuration
- Add ProGuard as dependency and register a task
- The input and output jars
- Dependencies and third parties
- Custom Extension Points as entry points
- Extensions
- Target
- Don’t shrink
- To optimize or not to optimize
- Repackage classes
- Attributes
- File and Code Templates
- Inspection descriptions
- Intention descriptions
- Inspection fields
- Exception stacktraces
- Other configurations
- Plugin Verifier
- CI/CD environment
- Debugging
- Testing
- The final configuration
- Other resources
The magical world of IDE plugin obfuscation with ProGuard…
First, let me thank a few people who directly or indirectly helped me dive into obfuscation:
to Jacky Liu for the example repo and ProGuard configuration to get some ideas from to start out, Yann Cébron for his answers on an older community post for the principles of obfuscating a JetBrains IDE plugin, and Karol Lewandowski for sharing his experience with ProGuard and providing a better approach at building the configuration.
A little background info
Recently, I have released my first paid plugin, WireMocha, which involves code obfuscation for more security and less reverse engineer-y.
The JetBrain Marketplace documentation has a dedicated article for obfuscation in which they recommend Zelix KlassMaster for various reasons, so that was my first choice. However, I quickly decided to go with something else (at least for the time being):
- It is mainly a licensed software, which although has an evaluation version, it is only for 30 days and with limited functionality,
- It would have cost more than USD260 which is not necessarily expensive in the long run, but since I’m starting a small business, I am absolutely happy with a free alternative for now,
- To quote their website (at least for trying it out) ““Free” e-mail addresses such as Hotmail, Yahoo and Gmail cannot be accepted. Please use your company e-mail address.”, which I don’t have at the moment.
Since ProGuard, a free, open-source alternative with Gradle integration and quite good documentation, is also mentioned in the Marketplace documentation, I decided to look into it instead.
Putting together the ProGuard configuration
TL;DR: If you are interested only in the complete configuration, and not the process of how I put it together, or any other aspects, please jump to the end of this article.
Add ProGuard as dependency and register a task
First, the ProGuard gradle plugin must be added to our build file (mine is based on the Kotlin DSL), so that we can use its configuration (you can find examples in their GitHub repository):
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("com.guardsquare:proguard-gradle:7.2.0")
}
}
Note that I didn’t include google()
as a repository (as some examples show) since we are not talking about an Android project.
Then we need to register a gradle task that will execute ProGuard with the configuration specified in it:
tasks.register<proguard.gradle.ProGuardTask>("proguard") {
}
The input and output jars
We need to specify the .jar file that is going to be obfuscated. For that we can use the injars
configuration.
First, for the sake of simplicity, I specified the path to the file that is created by the buildPlugin
gradle task.
injars("build/libs/PluginName-${properties("pluginVersion")}.jar")
Then later, since coupled with the outjars
config, the build resulted in the following error:
The output jar [<PATH REDACTED>/WireMocha-1.0.1.jar] must be specified after an input jar, or it will be empty.
(which I don’t know what it actually means), based on other examples, I updated it to use the result of the jar
gradle task:
injars(tasks.named("jar"))
Then, similarly to the input config, to tell ProGuard where to create our obfuscated archive, we can use outjars
, in which I used the same name as the input jar’s (when the input and output directories are the same, you can for instance add a postfix to the obfuscated file’s name, otherwise ProGuard lets you know that the input and output filenames are the same), and after some CI env. configuration, I decided to move it out to the build folder:
outjars("build/PluginName-${properties("pluginVersion")}.jar")
At this point, when I ran the new proguard
task (after running buildPlugin
), it notified me that there were some warnings:
Execution failed for task ':proguard'.
> java.io.IOException: Please correct the above warnings first.
however, it did not provide the actual warnings. For that I had to execute the proguard
task with the --info
switch, then I got bombarded with a myriad of warnings like these:
Warning: com.project.SomeBundle: can't find referenced class kotlin.Metadata
Warning: com.project.SomeClass: can't find referenced class com.intellij.psi.PsiExpression
Warning: com.project.SomeClass: can't find referenced class java.lang.Boolean
Since these are all references to 3rd-party classes, this is where dependency configuration comes in.
Dependencies and third parties
My first approach was to add dontwarn
options for class patterns that I wanted to ignore in the logs, for example:
dontwarn("com.intellij.**")
Although these removed some of the notifications, a few still remained, and it still didn’t tell me how to actually configure 3rd-parties. That’s when libraryjars
came into the picture, and this is when I found Jacky Liu’s example repository where I bumped into a feasible solution for both Java and 3rd-parties.
First, to configure Java itself, I used the the following snippet, which also removed the Java related warnings:
File("${System.getProperty("java.home")}/jmods/").listFiles()!!.forEach { libraryjars(it.absolutePath) }
Then, for 3rd-party libraries I used this one:
libraryjars(configurations.compileClasspath.get())
Finally, since this part of the configuration was complete, there were no more warnings reported, and I could remove the dontwarn
configs.
Custom Extension Points as entry points
According to ProGuard’s documentation:
In order to determine which code has to be preserved and which code can be discarded or obfuscated, you have to specify one or more entry points to your code. These entry points are typically classes with main methods, applets, midlets, activities, etc.
For IntelliJ-based IDEs, these entry points are custom Extension Points (<extensionPoints>
element). These are your plugin’s public API that has to be accessible from other plugins, so their names and their public members must be kept unobfuscated. This is where the various keep*
options come in.
The way you have to configure these options will depend on your plugin, what classes are added as custom Extension Points and what members they have, but there is probably a common class specification for them.
In general, most other parts of your plugin code can be obfuscated.
Extensions
Extensions and similar components are by which you can extend the functionality of the IDEs.
As a general rule we can say that all extension implementation classes in the <extension>
element, and similar components, e.g. actions, should be obfuscated, otherwise they would make a cracker’s job much easier to find out the meaning of those classes.
In addition to obfuscating the classes themselves, their names should also be adapted to the new ones. ProGuard has the adaptresourcefilecontents
option in which one can define the files or file patterns to adapt. This is primarily necessary in the plugin.xml and other plugin descriptor files, otherwise during build, these classes won’t be found by their original names.
If you have a single plugin.xml, you can use:
adaptresourcefilecontents("META-INF/plugin.xml")
or if you have multiple ones, you can either enumerate all of them:
adaptresourcefilecontents("META-INF/plugin.xml,META-INF/other-descriptor.xml,META-INF/another-one.xml")
or simply define a catch-all pattern:
adaptresourcefilecontents("**.xml")
Using this option will make changes like the one below, in your files:
//Original plugin.xml
<localInspection implementationClass="com.project.name.AnAwesomeInspection" ... />
//Obfuscated plugin.xml
<localInspection implementationClass="X" ... />
If you have other types of files (.properties, .form, etc.) that reference class names, it is recommended to include them too. But when it comes to plugin descriptor XML files, make sure you include all such files, otherwise your build will fail because it will try to reference the already obfuscated classes with their original names from the descriptor files, and you’ll get similar errors:
022-04-20 18:07:35,211 [ 8635] ERROR - nSystem.impl.ActionManagerImpl - com.project.SomeAction PluginClassLoader(plugin=PluginDescriptor(name=WireMocha, id=wiremocha, descriptorPath=plugin.xml, path=C:\<project path>\build\idea-sandbox\plugins\WireMocha, version=1.0.1, package=null, isBundled=false), packagePrefix=null, instanceId=77, state=active)
com.intellij.diagnostic.PluginException: com.project.SomeAction PluginClassLoader(plugin=PluginDescriptor(name=WireMocha, id=wiremocha, descriptorPath=plugin.xml, path=C:\<project path>\build\idea-sandbox\plugins\WireMocha, version=1.0.1, package=null, isBundled=false), packagePrefix=null, instanceId=77, state=active)
at com.intellij.serviceContainer.ComponentManagerImpl.instantiateClass(ComponentManagerImpl.kt:894)
at com.intellij.openapi.actionSystem.impl.ActionManagerImpl.instantiate(ActionManagerImpl.java:204)
at com.intellij.openapi.actionSystem.impl.ActionManagerImpl.convertStub(ActionManagerImpl.java:189)
at com.intellij.openapi.actionSystem.impl.ActionManagerImpl.getActionImpl(ActionManagerImpl.java:507)
at com.intellij.openapi.actionSystem.impl.ActionManagerImpl.getAction(ActionManagerImpl.java:496)
...
Caused by: java.lang.ClassNotFoundException: com.project.SomeAction PluginClassLoader(plugin=PluginDescriptor(name=WireMocha, id=wiremocha, descriptorPath=plugin.xml, path=C:\<project path>\build\idea-sandbox\plugins\WireMocha, version=1.0.1, package=null, isBundled=false), packagePrefix=null, instanceId=77, state=active)
at com.intellij.serviceContainer.ComponentManagerImplKt.doLoadClass(ComponentManagerImpl.kt:1443)
at com.intellij.serviceContainer.ComponentManagerImplKt.access$doLoadClass(ComponentManagerImpl.kt:1)
at com.intellij.serviceContainer.ComponentManagerImpl.instantiateClass(ComponentManagerImpl.kt:886)
... 17 more
Target
Since I’m building on Java 11, I specified the target
Java version as well. This is only applicable for versions <= 11. For versions > 11 this option is deprecated since ProGuard 7.2.1.
target("11")
Don’t shrink
Although I don’t know the underlying reason, when shrinking is enabled (the dontshrink
option is not specified) ProGuard makes such changes that the output jar would be empty, which ProGuard let’s you know about:
"The output jar is empty. Did you specify the proper '-keep' options?"
Thus, it is required to add the dontshrink
option to the configuration.
To optimize or not to optimize
ProGuard has an optimizer step as well which “optimizes bytecode and removes unused instructions”.
In my plugin it didn’t seem to matter if I had optimization enabled or not, so I decided to keep it enabled (didn’t add the dontoptimize
option). If you experience issues that might be related to optimization, you can add the following option to your config, and see if it makes any difference:
dontoptimize()
Repackage classes
To make it more confusing for curious eyes, there is an option to repackage classes that are renamed during the obfuscation process. For that you can use the repackageclasses
option which renames the packages to the specified one.
If you specify an empty string (repackageclasses("")
), packages will be removed completely. E.g. in the plugin.xml you will see:
//Original plugin.xml
<localInspection implementationClass="com.project.name.AnAwesomeInspection" ... />
//Obfuscated plugin.xml
<localInspection implementationClass="X" ... />
and the built plugin jar’s structure will be something like this:
[fileTemplates]
[messages]
[META-INF]
A.class
a.class
ac$a.class
...
If you specify an arbitrary package name (e.g. repackageclasses("my.plugin")
), it will change as follows:
//Original plugin.xml
<localInspection implementationClass="com.project.name.AnAwesomeInspection" ... />
//Obfuscated plugin.xml
<localInspection implementationClass="my.plugin.X" ... />
and in the plugin archive:
[fileTemplates]
[messages]
[META-INF]
[my]
[plugin]
A.class
a.class
ac$a.class
...
Attributes
There are certain aspects of the source code that may be useful or even necessary to preserve in order for your application to function properly. This can be achieved with the keepattributes
option. You can find examples of attributes and their meanings at Obfuscation Options, Attributes and Examples in the ProGuard documentation.
Although the list of attributes might be different for your plugin, I included the following ones in my configuration, and you can see the reasons for them in the next few sections:
keepattributes("InnerClasses,LineNumberTable,*Annotation*,SourceFile,Signature,EnclosingMethod")
SourceFile and LineNumberTable
This is in order to produce meaning obfuscated stacktraces. Having this option coupled with renamesourcefileattribute("SourceFile")
, a stacktrace will look something like this:
java.lang.RuntimeException: thrown 'cause why not
at ar.a(SourceFile:55)
at ao.b(SourceFile:30)
at aF.a(SourceFile:123)
at ao.invoke(SourceFile:30)
at com.intellij.codeInsight.intention.impl.config.IntentionActionWrapper.invoke(IntentionActionWrapper.java:65)
Annotation
The value *Annotation*
is a pattern that matches all attributes that contain the Annotation
keyword.
Without this option I encountered an exception during build, which although doesn’t make the build itself fail, it may break something during runtime:
2022-04-22 10:37:52,387 [ 8768] ERROR - rationStore.ComponentStoreImpl - Cannot init component state (componentName=, componentClass=x) [Plugin: wiremocha]
com.intellij.diagnostic.PluginException: Cannot init component state (componentName=, componentClass=x) [Plugin: wiremocha]
at com.intellij.configurationStore.ComponentStoreImpl.initComponent(ComponentStoreImpl.kt:142)
at com.intellij.configurationStore.ComponentStoreWithExtraComponents.initComponent(ComponentStoreWithExtraComponents.kt:48)
at com.intellij.serviceContainer.ComponentManagerImpl.initializeComponent$intellij_platform_serviceContainer(ComponentManagerImpl.kt:521)
at com.intellij.serviceContainer.ServiceComponentAdapter.createAndInitialize(ServiceComponentAdapter.kt:51)
at com.intellij.serviceContainer.ServiceComponentAdapter.doCreateInstance(ServiceComponentAdapter.kt:37)
at com.intellij.serviceContainer.BaseComponentAdapter.getInstanceUncached(BaseComponentAdapter.kt:113)
at com.intellij.serviceContainer.BaseComponentAdapter.getInstance(BaseComponentAdapter.kt:67)
at com.intellij.serviceContainer.BaseComponentAdapter.getInstance$default(BaseComponentAdapter.kt:60)
at com.intellij.serviceContainer.ComponentManagerImpl.doGetService(ComponentManagerImpl.kt:595)
at com.intellij.serviceContainer.ComponentManagerImpl.getService(ComponentManagerImpl.kt:569)
at com.intellij.openapi.client.ClientAwareComponentManager.getFromSelfOrCurrentSession(ClientAwareComponentManager.kt:37)
at com.intellij.openapi.client.ClientAwareComponentManager.getService(ClientAwareComponentManager.kt:22)
at w.a(SourceFile:17)
at v.<init>(SourceFile:17)
at com.intellij.serviceContainer.ComponentManagerImpl.instantiateClass(ComponentManagerImpl.kt:830)
at com.intellij.serviceContainer.ComponentManagerImpl.instantiateClass(ComponentManagerImpl.kt:886)
at com.intellij.openapi.options.ConfigurableEP$ClassProducer.createElement(ConfigurableEP.java:440)
at com.intellij.openapi.options.ConfigurableEP.createConfigurable(ConfigurableEP.java:346)
at com.intellij.openapi.options.ex.ConfigurableWrapper.createConfigurable(ConfigurableWrapper.java:43)
at com.intellij.openapi.options.ex.ConfigurableWrapper.wrapConfigurable(ConfigurableWrapper.java:37)
at com.intellij.openapi.options.ex.ConfigurableWrapper.createConfigurables(ConfigurableWrapper.java:61)
at com.intellij.application.options.editor.CodeFoldingConfigurable.createConfigurables(CodeFoldingConfigurable.java:96)
at com.intellij.openapi.options.CompositeConfigurable.getConfigurables(CompositeConfigurable.java:65)
at com.intellij.application.options.editor.CodeFoldingConfigurable.createComponent(CodeFoldingConfigurable.java:53)
at com.intellij.openapi.options.ex.ConfigurableWrapper.createComponent(ConfigurableWrapper.java:172)
...
Caused by: java.lang.UnsupportedOperationException: configurationSchemaKey must be specified for x
at com.intellij.configurationStore.ComponentStoreImpl.initComponent(ComponentStoreImpl.kt:367)
at com.intellij.configurationStore.ComponentStoreImpl.initComponent(ComponentStoreImpl.kt:110)
... 46 more
I have an application service registered in plugin.xml that implements com.intellij.openapi.components.PersistentStateComponent
. I found out that the configurationSchemaKey
property belongs to the ServiceDescriptor
class (that describes the <projectService>
and <applicationService>
elements), but I could not find out more about it.
I managed to single out the RuntimeVisibleAnnotations
attribute which if not specified, the exception above is thrown, so there must be a field/class/method annotation somewhere that is not preserved, which I haven’t been able to find. To make sure that I won’t run into annotation related issues, I kept all annotation types, hence the *Annotation*
.
InnerClasses, Signature, EnclosingMethod
Although I didn’t have any problem when they were not specified, I decided to keep them, since their documentation states that
Code may access this information by reflection.
I encourage you to play around with these attributes whether you find them necessary or not.
File and Code Templates
I have a feature that builds on File and Code templates, and I use FileTemplateManager
to load them from the fileTemplates and fileTemplates/code resource folders. Unfortunately, the underlying ClassLoader
in the platform code base didn’t find the main/resources/fileTemplates
directory in my plugin. See the following line in com.intellij.ide.fileTemplates.impl.FileTemplatesLoader#loadDefaultTemplates
in pre-IJ2022.1 platform code (since I build with version 2021.3).
Enumeration<URL> systemResources = loader.getResources(DEFAULT_TEMPLATES_ROOT); //Where DEFAULT_TEMPLATES_ROOT is by default "fileTemplates"
These were the resource URLs returned:
//obfuscated:
jar:file:/D:/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/ideaIC/2021.3/75777e10a0e2880bc02945066dda2480a696c3d9/ideaIC-2021.3/plugins/java/lib/java-impl.jar!/fileTemplates
jar:file:/D:/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/ideaIC/2021.3/75777e10a0e2880bc02945066dda2480a696c3d9/ideaIC-2021.3/plugins/junit/lib/idea-junit.jar!/fileTemplates
jar:file:/D:/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/ideaIC/2021.3/75777e10a0e2880bc02945066dda2480a696c3d9/ideaIC-2021.3/lib/platform-impl.jar!/fileTemplates
//unobfuscated:
jar:file:/D:/<project path>/build/idea-sandbox/plugins/WireMocha/lib/WireMocha-1.0.1.jar!/fileTemplates
jar:file:/D:/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/ideaIC/2021.3/75777e10a0e2880bc02945066dda2480a696c3d9/ideaIC-2021.3/plugins/java/lib/java-impl.jar!/fileTemplates
jar:file:/D:/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/ideaIC/2021.3/75777e10a0e2880bc02945066dda2480a696c3d9/ideaIC-2021.3/plugins/junit/lib/idea-junit.jar!/fileTemplates
jar:file:/D:/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/ideaIC/2021.3/75777e10a0e2880bc02945066dda2480a696c3d9/ideaIC-2021.3/lib/platform-impl.jar!/fileTemplates
The documentation of the keepdirectories
option states, “By default, directory entries are removed.”, although the resource directories are physically in the built .jar file. By adding this option to the configuration, the fileTemplates directory in my plugin is found and loaded properly. Unfortunately, none of the directory filters I came up with for fileTemplates/code worked, so I decided not to apply a directory filter:
keepdirectories()
Inspection descriptions
By default, inspection description files have the short names generated from their implementation classes. If they are left unobfuscated, they would reveal the class names they are bound to, and the platform would still try to access them using their obfuscated names.
Inspection descriptions can be specified as static descriptions using the getStaticDescription()
method in their implementation classes, and the description HTML files removed. And, to enable static descriptions you have to specify the following in your plugin.xml:
<localInspection hasStaticDescription="true" ... />
This eliminates the need to reference them by class names.
At this point it is up to you to decide on whether you specify the whole description within that method, or retrieve it from a bundle, or from elsewhere.
An other alternative might be overriding getDescriptionFileName()
, but it can be seen in com.intellij.codeInspection.ex.InspectionToolWrapper#loadDescription()
that com.intellij.codeInspection.InspectionProfileEntry#loadDescription()
is called in exceptional cases, so I personally would not go with this solution.
Intention descriptions
Intention description files are located in directories called the same as their implementation classes. The implications are the same as for inspections.
Directories of Intention descriptions can be renamed, and their names specified in the plugin.xml within the <intentionAction>
element. I recommend giving them meaningful names, so that they don’t confuse you what they actually refer to, but different enough so that their class names are more difficult to find out:
<intentionAction>
<categoryKey>some.category</categoryKey>
<className>com.project.ConvertSomethingToSomethingElseIntention</className>
<descriptionDirectoryName>SomethingToElse</descriptionDirectoryName>
</intentionAction>
Inspection fields
Inspection options panel related configuration are stored as fields in inspections’ implementation classes, and are accessed via reflection, thus these fields have to be kept unobfuscated. Otherwise you can get errors like this:
2022-04-20 10:05:49,464 [ 108325] ERROR - llij.ide.plugins.PluginManager - field 'someFieldName'not found
java.lang.AssertionError: field 'someFieldName'not found
at com.intellij.codeInspection.ui.OptionAccessor$Default.getOption(OptionAccessor.java:27)
at com.intellij.codeInspection.ui.InspectionOptionsPanel.addCheckboxEx(InspectionOptionsPanel.java:76)
at com.intellij.codeInspection.ui.InspectionOptionsPanel.addCheckbox(InspectionOptionsPanel.java:69)
at com.intellij.codeInspection.ui.SingleCheckboxOptionsPanel.<init>(SingleCheckboxOptionsPanel.java:29)
at aQ.createOptionsPanel(SourceFile:23)
at com.intellij.codeInspection.ex.ScopeToolState.getAdditionalConfigPanel(ScopeToolState.java:97)
at com.intellij.profile.codeInspection.ui.SingleInspectionProfilePanel.setConfigPanel(SingleInspectionProfilePanel.java:240)
at com.intellij.profile.codeInspection.ui.SingleInspectionProfilePanel.updateOptionsAndDescriptionPanel(SingleInspectionProfilePanel.java:858)
at com.intellij.profile.codeInspection.ui.SingleInspectionProfilePanel.lambda$initTreeScrollPane$8(SingleInspectionProfilePanel.java:570)
at java.desktop/javax.swing.JTree.fireValueChanged(JTree.java:2967)
...
To keep members of classes, you can use the keepclassmembers
option, in which you can specify a class specification that describes what members in which classes to keep. I specified that every field in classes extending com.intellij.codeInspection.LocalInspectionTool
should be kept:
keepclassmembers("""class ** extends com.intellij.codeInspection.LocalInspectionTool {
<fields>;
}""")
I tried to restrict the fields to public ones only, as public <fields>;
, but the Plugin Verifier, for some reason, fails at classes unrelated to (in this case) inspections. At the moment I don’t know what changes happen to the code that cause this.
Exception stacktraces
When applying various keep*
and other options, it is worth checking what exception stacktraces look like, so that you can further (or maybe lesser) obfuscate class names, package names, etc. if you feel it necessary to do so. It also won’t be your first time you meet with an obfuscated stacktrace only when one of your users report an issue.
Along with stacktraces, I encourage everyone to apply the printmapping
configuration, so that when you encounter an issue, you will have the keyword mappings at hand to trace the code back, either manually or by using ProGuard’s Retrace tool, e.g.:
printmapping("build/mapping.txt")
The mapping file will contain entries like this:
com.project.plugin.NameValidator -> d:
java.util.regex.Pattern NAME_PATTERN -> a
java.lang.String postfix -> c
33:35:void <init>() -> <init>
void <init>(com.intellij.openapi.vfs.VirtualFile) -> <init>
1000:1000:void $$$reportNull$$$0(int):0:0 -> <init>
45:46:com.project.plugin.NameValidator ensurePostfix(java.lang.String) -> b
51:59:java.lang.String getErrorText(java.lang.String) -> getErrorText
NOTE: the last name, getErrorText
, is not replaced since it is an overridden method that comes from a class in the IntelliJ platform code base.
Other configurations
There are many other options to customize ProGuard with. I decided to include one more, overloadaggressively
, to further reduce the size of the built plugin jar:
overloadaggressively()
Plugin Verifier
When it comes to the Plugin Verifier, although it works on obfuscated archives as well, you have to be careful how you configure ProGuard because certain configurations can cause Plugin Verifier to report compatibility issues, even if those issues would not affect how the plugin functions. For example:
#Abstract method com.plugin.folding.c.a(PsiMethodCallExpression arg0) : Optional is not implemented
Concrete class m inherits from com.plugin.folding.c but doesn't implement the abstract method a(PsiMethodCallExpression arg0) : Optional. This can lead to **AbstractMethodError** exception at runtime.
At first glance it might seem that it is caused by the fact that identifiers are replaced in the code, and the Plugin Verifier cannot identify them properly. However, in my case they were caused by having the mergeinterfacesaggressively
option added. I went a little overboard with my efforts to optimize and shrink my plugin, and I noticed it only later that it makes such changes to the source code that it has this effect.
So, I’d say by default don’t add this option to your configuration, but later if you are feeling adventurous, try it out if it causes verifier issues in your plugin or not.
As for CI/CD execution (if you are using the intellij-platform-plugin-template), if you are having trouble solving the verifier issues, as an alternate/temporary solution, you can disable those phases, replace them with a simple buildPlugin
execution, and run the Plugin Verifier in your local environment againts the not obfuscated plugin archive. So, essentially the Setup Plugin Verifier IDEs Cache, Run Plugin Verification and Collect Plugin Verifier Result actions would be commented out/removed, and instead the following one would be added right after the Collect Tests Result action:
- name: Build Plugin
run: ./gradlew buildPlugin
CI/CD environment
Since my plugin in based on the intellij-platform-plugin-template, the CI/CD workflow operates in GitHub Actions.
Hook ProGuard into the build workflow
The following is my configuration to execute the proguard
task automatically during build. I added it to the perpareSandbox
task, which may not be optimal for you, but it works perfectly for me for now:
tasks {
...
prepareSandbox {
dependsOn("proguard")
}
}
Retrieve the obfuscation mapping file
In order to be able to retrace stacktraces later, you want to have the mapping file available among the CI job artifacts. In GitHub Actions you can use the upload-artifact action for that. I added this step right after the one called Upload artifact:
- name: Upload Obfuscation Mapping
uses: actions/upload-artifact@v2.2.4
with:
name: Obfuscation Mapping
path: ./build/mapping.txt
Zip the obfuscated archive
To make sure that the obfuscated (and not the original) jar is zipped into the downloadable final archive, I found that the unobfuscated one has to be deleted. I extended the prepareSandbox
logic with the following:
val inputPath = "build/libs/<PluginName>-${properties("pluginVersion")}.jar" //Where <PluginName> is the actual name of your plugin
val outputPath = "build/<PluginName>-${properties("pluginVersion")}.jar"
tasks {
...
prepareSandbox {
dependsOn("proguard")
doFirst {
with(File(inputPath)) {
if (exists()) {
val proguarded = File(outputPath)
if (proguarded.exists()) {
delete() //delete original jar
proguarded.renameTo(this)
println("Plugin archive successfully obfuscated and optimized.")
} else println("ProGuarded file doesn't exist.")
} else println("Original file doesn't exist.")
}
}
}
}
Qodana
Once GitHub Actions gets over the Plugin Verifier phase, the Qodana validation phase is coming up. However, I quickly ran into an issue here. For some reason it can’t create the proguard
task that is configured in the gradle build file, due to an underlying NullPointerException.
Unfortunately, I don’t have the proper docker and gradle knowledge for this to investigate, so being a good test automation engineer, I disabled the Qodana phase, because that way my CI/CD runs green. 🙂
I post a portion of the stacktrace here, so if anyone is interested can take a look:
Stacktrace during Qodana phase…
java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Gradle project PluginName:15375f63 resolve failed.
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:395)
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1999)
at org.jetbrains.plugins.gradle.GradleCommandLineProjectConfigurator$StateNotificationListener.waitForImportEnd(GradleCommandLineProjectConfigurator.kt:196)
at org.jetbrains.plugins.gradle.GradleCommandLineProjectConfigurator.configureProject(GradleCommandLineProjectConfigurator.kt:76)
at com.intellij.codeInspection.InspectionApplicationBase.configureProject(InspectionApplicationBase.java:462)
at org.jetbrains.plugins.staticAnalysis.inspections.scripts.QodanaScript$launchInspections$1.invoke(QodanaScript.kt:77)
at org.jetbrains.plugins.staticAnalysis.inspections.scripts.QodanaScript$launchInspections$1.invoke(QodanaScript.kt:21)
at org.jetbrains.plugins.staticAnalysis.inspections.runner.Utils_time_loggerKt$runTaskAndLogTime$1.invokeSuspend(utils-time-logger.kt:63)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
...
Caused by: java.lang.IllegalStateException: Gradle project PluginName:15375f63 resolve failed.
at org.jetbrains.plugins.gradle.GradleCommandLineProjectConfigurator$StateNotificationListener.onFailure(GradleCommandLineProjectConfigurator.kt:179)
at com.intellij.openapi.externalSystem.service.remote.ExternalSystemProgressNotificationManagerImpl$TaskListenerWrapper.onFailure(ExternalSystemProgressNotificationManagerImpl.kt:106)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at com.intellij.util.EventDispatcher.dispatchVoidMethod(EventDispatcher.java:120)
at com.intellij.util.EventDispatcher.lambda$createMulticaster$1(EventDispatcher.java:85)
at com.sun.proxy.$Proxy125.onFailure(Unknown Source)
at com.intellij.openapi.externalSystem.service.remote.ExternalSystemProgressNotificationManagerImpl$onFailure$1.invoke(ExternalSystemProgressNotificationManagerImpl.kt:65)
...
Caused by: com.intellij.openapi.externalSystem.model.LocationAwareExternalSystemException: Could not create task ':proguard'.
at org.jetbrains.plugins.gradle.service.execution.GradleExecutionErrorHandler.createUserFriendlyError(GradleExecutionErrorHandler.java:150)
at org.jetbrains.plugins.gradle.service.project.AbstractProjectImportErrorHandler.createUserFriendlyError(AbstractProjectImportErrorHandler.java:46)
...
Caused by: org.gradle.api.internal.tasks.DefaultTaskContainer$TaskCreationException: Could not create task ':proguard'.
at org.gradle.api.internal.tasks.DefaultTaskContainer.taskCreationException(DefaultTaskContainer.java:715)
at org.gradle.api.internal.tasks.DefaultTaskContainer.access$600(DefaultTaskContainer.java:76)
at org.gradle.api.internal.tasks.DefaultTaskContainer$TaskCreatingProvider.domainObjectCreationException(DefaultTaskContainer.java:707)
at org.gradle.api.internal.DefaultNamedDomainObjectCollection$AbstractDomainObjectCreatingProvider.tryCreate(DefaultNamedDomainObjectCollection.java:948)
at org.gradle.api.internal.tasks.DefaultTaskContainer$TaskCreatingProvider.access$1401(DefaultTaskContainer.java:654)
at org.gradle.api.internal.tasks.DefaultTaskContainer$TaskCreatingProvider$1.run(DefaultTaskContainer.java:680)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:68)
at org.gradle.api.internal.tasks.DefaultTaskContainer$TaskCreatingProvider.tryCreate(DefaultTaskContainer.java:676)
at org.gradle.api.internal.DefaultNamedDomainObjectCollection$AbstractDomainObjectCreatingProvider.calculateOwnValue(DefaultNamedDomainObjectCollection.java:929)
at org.gradle.api.internal.provider.AbstractMinimalProvider.get(AbstractMinimalProvider.java:84)
at org.gradle.api.internal.DefaultNamedDomainObjectCollection$AbstractDomainObjectCreatingProvider.get(DefaultNamedDomainObjectCollection.java:915)
at org.gradle.api.internal.DefaultDomainObjectCollection.addLater(DefaultDomainObjectCollection.java:286)
at org.gradle.api.internal.DefaultNamedDomainObjectCollection.addLater(DefaultNamedDomainObjectCollection.java:146)
at org.gradle.api.internal.tasks.DefaultTaskContainer.addLaterInternal(DefaultTaskContainer.java:761)
at org.gradle.api.internal.tasks.DefaultTaskContainer.access$900(DefaultTaskContainer.java:76)
at org.gradle.api.internal.tasks.DefaultTaskContainer$3.call(DefaultTaskContainer.java:416)
at org.gradle.api.internal.tasks.DefaultTaskContainer$3.call(DefaultTaskContainer.java:403)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:199)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
at org.gradle.api.internal.tasks.DefaultTaskContainer.registerTask(DefaultTaskContainer.java:403)
at org.gradle.api.internal.tasks.DefaultTaskContainer.register(DefaultTaskContainer.java:375)
at Build_gradle.<init>(build.gradle.kts:251)
...
Caused by: java.lang.NullPointerException
at Build_gradle$7.invoke(build.gradle.kts:174)
at Build_gradle$7.invoke(build.gradle.kts:2)
at Build_gradle$inlined$sam$i$org_gradle_api_Action$0.execute(TaskContainerExtensions.kt)
at org.gradle.api.internal.DefaultMutationGuard$2.execute(DefaultMutationGuard.java:44)
at org.gradle.api.internal.DefaultMutationGuard$2.execute(DefaultMutationGuard.java:44)
at org.gradle.configuration.internal.DefaultUserCodeApplicationContext$CurrentApplication$1.execute(DefaultUserCodeApplicationContext.java:123)
at org.gradle.api.internal.DefaultCollectionCallbackActionDecorator$BuildOperationEmittingAction$1.run(DefaultCollectionCallbackActionDecorator.java:110)
... 208 more
Debugging
In my experience debugging obfuscated code is either extermely difficult or simply impossible. I had a few occasions when I had to investigate the underlying status of my plugin and/or the platform. In those cases I either debug my plugin as unobfuscated, or logged information to the console, as a simple alternative.
However, even if you load the obfuscated version of your plugin into the IDE, it is still possible to debug classes from the IntelliJ platform code base.
Testing
If you are doing the obfuscation for the first time for your plugin, it can be beneficial to do an “exhaustive” manual testing and to go through all plugin features (including Settings, UI, etc.) to make sure that everything works properly, and no class, attribute, resource file or some other functionality or configuration is missing or misconfigured.
It is a somewhat tedious task but I personally uncovered issues not just related to obfuscation but issues unrelated too, and could fix a couple of bugs, increase test coverage and simplify the code base. So, it was definitely worth the effort.
After talking to community members, I discovered a couple of other areas that are also worth paying attention to: GSON serialization names and settings serialization (e.g. field names in XML output), code style settings and custom extension points.
The final configuration
Note that this config may not be optimal, and could be even stricter when it comes to the obfuscation part, so make sure you check out the ProGuard documentation for additional configuration and customize your gradle config further, if you need to.
import proguard.gradle.ProGuardTask
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("com.guardsquare:proguard-gradle:7.2.0")
}
}
tasks.register<ProGuardTask>("proguard") {
//Input and output jars
injars(tasks.named("jar"))
outjars("build/<PluginName>-${properties("pluginVersion")}.jar") //Where <PluginName> is the actual name of your plugin
//Dependencies
File("${System.getProperty("java.home")}/jmods/").listFiles()!!.forEach { libraryjars(it.absolutePath) }
libraryjars(configurations.compileClasspath.get())
//Processing-configuration
target("11")
printmapping("build/obfuscation-mapping.txt")
dontshrink()
overloadaggressively()
repackageclasses("")
renamesourcefileattribute("SourceFile")
adaptresourcefilecontents("**.xml")
keepdirectories()
keepattributes("InnerClasses,Signature,EnclosingMethod,SourceFile,LineNumberTable,*Annotation*")
keepclassmembers("""class ** extends com.intellij.codeInspection.LocalInspectionTool {
<fields>;
}""")
}
Other resources
- StackOverflow: Android Build with Gradle and ProGuard : “The output jar must be specified after an input jar, or it will be empty”
- StackOverflow: how to fix proguard warning ‘can’t find referenced method’ for existing methods ‘clone’ and ‘finalize’ of class java.lang.Object
- StackOverflow: How to obfuscate package name in android studio