Thursday, June 19, 2014

Using Ivy + Ant for Android in Eclipse (Dependency management and building made easy)

Recently I discovered Apache Ivy, a dependency management system supporting Maven repositories, that can be integrated with your Eclipse IDE and also with your Ant build scripts. Since I found very little documentation on how to integrate these things and struggled myself quiet a bit, I decided to make a guide to help others with the same problems.

The preparations

 

I assume you have Eclipse, the Android SDK and the Android plugin for Eclipse installed. The only thing you have to install now, is IveDE, the Eclipse plugin for Ivy. The easiest way is to copy the url of the update site

http://www.apache.org/dist/ant/ivyde/updatesite

into Eclipse's Update Manager. Make sure to install all four items. After the installation restart Eclipse and you're ready to go.

Configuring the Eclipse project


In the following step we are going to configure your Eclipse project to use Ivy.
  1. Open your project's properties page and swith to "Java Build Path".
  2. Click on "Libraries" and then "Add Library".
  3. Select "IvyDE Managed Dependencies".
  4. Click "Next", then "Finish".
  5. While the properties are still open, click on "Order and Export".
  6. Tick the checkbox next to "Ivy". This step is very important or else your app won't work when launching debug versions through Eclipse.
Configuring the Eclipse project to use Ivy


The reason you're probably getting an error now, is Ivy's configuration file is missing. Let's create it.
  1. Right-click on your project, select "New", "Others..." and then select "Ivy file".
  2. Leave everything as it is and click "Finish".
You should now have a new file called "ivy.xml" in the root folder of your project. Open it. You should see something like this.

<ivy-module version="2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:noNamespaceSchemaLocation="http://ant.apache.org/ivy/schemas/ivy.xsd">
    <info
        organisation=""
        module="ModuleName"
        status="integration">
    </info>
</ivy-module>

Adding and configuring dependencies


In this step we will add our first dependency that will be automatically resolved through Ivy. For this example I have chosen the excellent library "Butterknife" from Jake Wharton.

Like most Java libraries, Butterknife is available through Maven. Open the relevant page in your browser and you will find the section named "Ivy". This section contains the code snippet we are going to use in the ivy.xml file. In the end it should like like this:

<ivy-module version="2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:noNamespaceSchemaLocation="http://ant.apache.org/ivy/schemas/ivy.xsd">
    <info
        organisation=""
        module="BLSAndroidUiTest"
        status="integration">
    </info>
    
    <dependencies>
        <dependency org="com.jakewharton" name="butterknife" rev="5.1.0"/>
    </dependencies>
            
</ivy-module>


Save the file and you should see that Ivy automatically started resolving the new dependency. In case it didn't, right-click on your project and select "Ivy" -> "Resolve".

In the end you should see a new entry in your Project Explorer called "Ivy" containing all your resolved dependencies.

Ivy's classpath container with all the transitive dependencies
But wait, where do all these other libraries come from? It turns out "Butterknife" defines a number of dependencies itself. These dependencies are called transitive and are usually a good thing because the library you downloaded in the first place needs them for operating. Ivy resolving them for you means you don't have to.

In Butterknife's case, though, all the transitive dependencies are optional. It will work without them. What's more, your app won't work at all unless we remove them again. The entry named "android-4.1.1.4.jar" contains the whole Android API which will conflict with the actual Android API you're compiling against. That's why we are going to disable transitive resolving for Butterknife. This is easily done by adding the attribute "transitive" and setting it to false. Your xml should now look like this:

<ivy-module version="2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:noNamespaceSchemaLocation="http://ant.apache.org/ivy/schemas/ivy.xsd">
    <info
        organisation=""
        module="BLSAndroidUiTest"
        status="integration">
    </info>
    
    <dependencies>
        <dependency org="com.jakewharton" name="butterknife" rev="5.1.0" transitive="false"/>
    </dependencies>
            
</ivy-module>

After saving you should see all the other libraries disappear. If not, repeat the process from earlier: right-click on your project and select "Ivy" -> "Resolve".

Configuring Annotation Processors


Apart from being an awesome library, I selected Butterknife for a second reason. To use it, you have to configure it as an Annotation Processor in Eclipse. If your library doesn't need this (most don't) skip this section.

Butterknife's website explains how to setup Eclipse to use the custom Annotation Processor. The same steps apply here except for step 2. That's because the JAR we are setting up is not in the project's folder anymore. Here's what you do instead:
  1. In the Factory Path section, click on "Add Variable..."
  2. Select "IVY_HOME" and click on "Extend..."
  3. Find the relevant JAR and click OK. The image below shows where Butterknife's JAR can be found.
Setting an Annotation Processor downloaded by Ivy

 Setting up a build with Ant

At this point you should already be able to launch and debug your apps through Eclipse. Now we are going to make Ant work as well. If you're just interested in the end result, here it is:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:ivy="antlib:org.apache.ivy.ant" name="ProjectName" default="help">

    <property file="local.properties" />

    <property file="ant.properties" />

    <property environment="env" />
    <condition property="sdk.dir" value="${env.ANDROID_HOME}">
        <isset property="env.ANDROID_HOME" />
    </condition>

    <loadproperties srcFile="project.properties" />

    <fail message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable." unless="sdk.dir" />

    <import file="custom_rules.xml" optional="true" />

    <import file="${sdk.dir}/tools/ant/build.xml" />

    <taskdef resource="org/apache/ivy/ant/antlib.xml" uri="antlib:org.apache.ivy.ant" />

    <target name="-build-setup" depends="retrieve,android_rules.-build-setup" />

    <target name="-pre-compile">

        <pathconvert property="classpathPropBefore" refid="project.all.jars.path" />

        <path id="project.all.jars.path">
            <path path="${classpathPropBefore}" />
            <path refid="ivy.build.path" />
        </path>

    </target>

    <target name="retrieve" description="Retrieve Ivy dependencies">
        <ivy:resolve />
        <ivy:cachepath pathid="ivy.build.path" />
    </target>

</project>

In the target "retrieve" we're making Ivy resolve the dependencies and create a classpath for the relevant jars. Then, in "-pre-compile" we're adding them to the actual build's classpath. This post of mine explains how this is done.

And that's it. Have fun.