Pages

Sunday, October 27, 2013

Managing Dependencies for IntelliJ IDEA Plug-in Development

Probably the most painful part in developing plugins for IntelliJ IDEA is the dependency management of the libraries that your plugin might depend on. There is a multitude of problems that one may encounter:

  • IDEA binaries are not hosted publicly neither in Maven Central, JCenter, or any other repository.
  • Plugins cannot be built with the common tools like Maven or Gradle without setting up your hair on fire.

When your plugin depends on just a few external libraries, and the plugin build itself doesn't require customization, then everything is simple: put the dependencies into lib/ (or whatever) folder in the project and generate the Ant build script through Build -> Generate Ant Build... action. Simple. Well, not really simple: the generated build script will require a few JARs from IDEA by pointing to the IDEA installation directory. So the simplest thing to do in this case is to extract the full IntelliJ IDEA distribution into some directory at the machine where the continuous integration server runs.

Simple, ugly, but works. Why not to make things a bit more kosher?

In my world, any developer in the team should be able to clone the project, open it in the IDE(A), and ideally, launch the project without any additional tuning.

So what could we do in case of IntelliJ IDEA plugins projects? Store the *.iml files in VCS so that when the project is opened by another developer he would automatically have all the dependencies attached to the module? Not kosher.

For me, clearly, the dependencies should be managed by dependency management tool. Options: Maven, Ivy, Gradle. IDEA provides a very good Maven support. But not for its own plugin modules. In fact, for plugin modules I wouldn't even try to use Maven as once IDEA recognizes it as a Maven project, it will erase the information about its plugin origin.

Other options: Ivy and Gradle. Ivy is awesome. It works. There's also a nice plugin for IDEA that will automatically import the dependencies if it locates ivy.xml in the project directory. In that case you could still use the autogenerated Ant build script - just alter it a bit so that it would make a call to ivy task to get the dependencies and incorporate 'em into the build classpath.

Sounds a bit too hardcore to me. There's a high chance that the project import will not be as smooth as you would like it to be. Especially if the project structure isn't very trivial.

Gradle to the rescue!

Gradle is awesome when it comes to non-trivial project structures. Its Ant-like flexibility along with nice Groovy syntax and all the Maven-like goodies is just awesome!

First of all, managing dependencies is very easy. For instance:

  repositories {
    jcenter()
  }

  dependencies {
    compile('org.zeroturnaround:jr-sdk:5.4.1') {
        transitive = false
    }
    compile 'org.ow2.asm:asm-all:4.1'
    compile 'org.slf4j:slf4j-nop:1.6.3'
    compile 'org.apache.commons:commons-compress:1.2'
  }

The little cool part is that JetGradle (in IDEA 12) would also automatically resolve and add the dependencies into the project.

This is cool but it is not enough. First of all, JetGradle cannot import the project as an IDEA plugin module. Secondly, there's no JetGradle in IDEA 13 as Gradle integration is getting a major overhaul. OK, back to the drawing board.

There's an 'idea' plugin for Gradle, how cool is that?

Just run gradle idea and it will generate the project files, incorporating the references to the required dependencies. Good. But the generated module type is a Java module. And I need Plugin module. Have no fear, Gradle's here! We can easily customize the build script to adjust the XML project descriptor to our requirements.
  apply plugin: "idea"

  repositories { ... }
  dependencies { ... }

  idea.project.ipr {
    beforeMerged { project ->
      project.modulePaths.clear()
    }
  }

  idea.module.iml {
    withXml {
      it.node.@type = "PLUGIN_MODULE"
    }
  }

The withXml hook makes the magic here - instead of the auto-generated 'JAVA_MODULE' the final descriptor will contain 'PLUGIN_MODULE' which will make IDEA think that the module is an IntelliJ IDEA plugin.

Only little problem that bothered me: the generated project descriptor is an *.ipr file that is kind of deprecated. It would have been much better if the plugin generated a directory based project metadata. The feature request was filed long ago but still isn't resolved.

After experimenting with the project descriptors for a bit, I actually found a simple workaround for this minor annoyance. The .idea directory structure requires only modules.xml file that is identical to the *.ipr file that idea plugin generates. So I could just use this simple task to create directory based structure:

  task setup {
     dependsOn ideaModule, ideaProject
     doLast {
       copy {
          from '.'
          into '.idea/'
          include '*.ipr'
          rename { "modules.xml" }
       }
       project.delete "${project.name}.ipr"
     }
  }
[hate mode="on"]Just writing delete "*.ipr" would not work. That's annoying.[hate mode="off"]

So now I could just execute gradle setup and it would generate the directory based project structure, with the correct references to the required dependencies and I could import the project into IDEA without any hassle.

This is all good. However, this is not the end of the story! There's more to do and more issues to resolve:

  • Get the required IDEA internal artifacts to a local repository, so that these dependencies could also be downloaded via Gradle dependency manager.
  • Migrate auto-generated Ant build script into Gradle script.
  • Adopt the Gradle script to be able to manage the releases of the plugin.

However, [hate mode="on"]none of this hassle would be needed if IntelliJ IDEA provided a sane way of building the plugins and managing the dependencies.[hate mode="off"]

Disqus for Code Impossible