Continuous Integration using TeamCity for IntelliJ Plugin Development
by Matt CholickI built and now maintain an IntelliJ plugin that helps for writing Spock specifications. Since I released it, one thing I've struggled with is maintaining compatibility across different version of IntelliJ. On Friday someone submitted a ticket that the plugin was broken again in the IntelliJ preview build. I decided to wrestle with this issue again. The missing piece has always been a continuous integration setup. It's annoying (and with each release, becoming more infeasible) to flip the IntelliJ Plugin SDK in my IDE, build, and run the tests for each supported IntelliJ version.
The first approach I took was to try and tackle the problem by compiling and testing my plugin with Gradle. Unfortunately, IntelliJ doesn't release their compiled code to maven central or package things in a way that would support some other tool building against their SDK. The closest I found to a working solution was a project on Google code, intellij-maven. Using this started out promising, but I wasn't successful. The project didn't include pieces of IntelliJ that my plugin depended on. As I added each piece, I had to work out the additional third party libraries that it depended on. Even when things did compile, this project was structured around checking out IntelliJ source locally rather than publishing a complete set of jars so that someone could start off a build with minimal work (as in, on a CI server). My tolerance for fiddling with build tools is about a day and a half; after reaching that, I set this idea aside for a while.
When someone reported things were broken again in the 12.1 preview, I decided to tackle the problem again using TeamCity. I guessed (correctly) that it would natively understand IntelliJ project files. I was able to successfully get TeamCity building my plugin against multiple versions of the IntelliJ SDK. Here's how.
Add Variables to Project Files
I normally never check IDE project files into vcs. In this case, it's a requirement to leverage the TeamCity IntelliJ based builds. They do support checking in the project files. Simply exclude the files:
.idea/workspace.xml
.idea/tasks.xml
A path variable for the JDK name is the next piece of required configuration. Variables in project files are defined by surrounding the name with "$". They local value can be set in Path Variables, under settings. Also, after an edit to the project file, IntelliJ will prompt that it sees a new variable needing a value. The JDK name variable allows TeamCity to configure different builds for different SDK versions. This setting is located in ./idea/misc.xml. In my project, I used idea_sdk_name. This variable isn't strictly required (in TeamCity you can override the JDK location). Without it, though, I found the configurations confusing because the TeamCity project JDK is named after a particular version, but the value pointed to a different version. Also, as I changed the JDK's actual value (instead of the path variable) in my IDE, the change checked into vcs on .idea/misc.xml would break all the builds (because they would require reconfiguring the JDK settings block). The relevant section of .idea/misc.xml follows.
...
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" assert-keyword="true" jdk-15="true" project-jdk-name="$idea_sdk_name$" project-jdk-type="IDEA JDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
...
My plugin depends on two natively bundled IntelliJ plugins as well. These also need to be configured to vary with the SDK version. The change to solve this introduced a variable in the project's .iml file. I used idea_sdk as the path to the IntelliJ SDK.
...
<orderEntry type="module-library" scope="PROVIDED">
<library name="groovy_plugin">
<CLASSES>
<root url="file://$idea_sdk$/plugins/Groovy/lib" />
</CLASSES>
<JAVADOC />
<SOURCES />
<jarDirectory url="file://$idea_sdk$/plugins/Groovy/lib" recursive="false" />
</library>
</orderEntry>
...
Add a Run Configuration for Tests
In my case, I'm only interested in regression testing rather than deploying some artifact. TeamCity just has to compile and run the tests. I added a JUnit run configuration to execute the tests. The key here to select the "Share" checkbox on the configuration and check in the xml file this generates.
Configure TeamCity
I've always been a Jenkins user, but no one has written the IntelliJ integration I need in this case. The free version of TeamCity allows 20 build configurations, which is enough for my plugin.
I'd never used TeamCity before, but I did not run into any issues starting it up, connecting a build agent, and configuring the project. The build configurations are where things get interesting again. Step three of the build configuration, "Build Step: IntelliJ IDEA Project", is where the build actually happens. Here are the relevant non-default configuration options with an explanation:
Option | Value | Note |
---|---|---|
Runner type | IntelliJ IDEA Project | Leverage the IDEA project, the whole reason I chose TeamCity for this project |
idea_sdk | /Applications/IntelliJ IDEA 11.1 CE.app | Path to an IntelliJ install. This is in the path to the other IntelliJ plugins my source depends on. |
idea_sdk_name | IDEA IC-123.169 | |
$idea_sdk_name$ - IDEA Home | /Applications/IntelliJ IDEA 11.1 CE.app | Path to the IntelliJ install for building |
$idea_sdk_name$ - IDEA Jar Files Patterns | lib/*.jar | By default, this value will exclude idea.jar. For a plugin project (depending on what it does), idea.jar likely does include necessary classes. |
$idea_sdk_name$ - JDK Jar Files Patterns |
jre/lib/*.jar
lib/tools.jar |
I had to add tools.jar. I believe this is just an OSX thing. |
Run configurations to execute | test-app | The shared run configuration from earlier, setup to test the application |
I created a Gist of the current TeamCity project-config.xml.
Maintaing backward compatibility going forward is going to be much less painful. Also, thanks to the IntelliJ developers for an answer my question when I got stuck getting TeamCity to build my source.