Thursday, March 13, 2014

Android: Take back your builds

So I started getting back into android development, reviving a little app I published a few years ago. The app itself was fairly crappy by android 1.6 standards (get off my lawn!) and now it's just looking terrible.

So I fire up the shiny new android studio but it looks like I'm going to have to start the project from scratch and port everything over. After getting frustrated trying to do the simplest things, like adding a library I went back to Eclipse. Several arcane build errors and several even more arcane eclipse ones later I was getting frustrated. Clearly this isn't going to work.

Aside from the terrible UI's of both IDE's there was something much more important. I wasn't getting any functionality out of them either, they were just getting in my way. I wanted to try the new atom editor but as yet don't have an invite, so I went back to trusty old vim with ant. This is when I discovered androids dirty little secret: the build system.

Monolithic Madness

A build system, as with other aspects of development, work best when you start with discreet parts and assemble them into a whole. I expected to find some ant tasks that wrap android tools and to be able to plug them in and get going.

Alas, google instead gives us some monolithic tools that define your build system for you. Attempting to go deeper give you a "here be dragons" warning. The tools that are at the very center of android development and yet there documentation consists of:

The other platform tools, such as aidl, aapt, dexdump, and dx, are typically called by the Android build tools or Android Development Tools (ADT), so you rarely need to invoke these tools directly. As a general rule, you should rely on the build tools or the ADT plugin to call them as needed.

 I firmly believe that this is at the center of why android IDE's are so lackluster, they have to conform to this monolithic build system. Aside from the "one build to rule them all" approach, building an android app is a very complicated procedure. This isn't "compile and run" like you find in other types of projects, these are the steps to build a basic, runable android application (illustrated here):

  1. Compile layouts, resources, etc into a resource file.
  2. Generate an source file, this is needed just to be able to compile your code.
  3. Compile your real code.
  4. Turn your .class files into .dex files.
  5. Combine your resource file with your .dex files into an .apk.
  6. Sign your .apk.
  7. Finally we get to run!

So to create a build system how you want it, you have to replicate all this with little to no documentation. And this is what I'm about to do.

Step 1 - House Keeping

The first thing we have to do when defining our build is to know where the android sdk, our libraries and everything else is, so in ant we have:

1:  <property name="android.sdk" location="D:\Program Files (x86)\Android\android-studio\sdk" />  
2:  <property name="android.aapt" location="${android.sdk}\build-tools\android-4.4.2\aapt.exe" />  
3:  <property name="android.jar" location="${android.sdk}\platforms\android-19\android.jar" />  
4:  <property name="android.dex" location="${android.sdk}\build-tools\android-4.4.2\dx.bat" />  
5:  <property name="android.adb" location="${android.sdk}\platform-tools\adb.exe" />  
6:  <property name="build.resource" location="build\aapt\resource.jar" />  
8:  <record name="buildlog.txt" action="start" append="false" />  
10:  <path id="libPackage">  
11:   <fileset dir="lib\">  
12:    <include name="android-binding-0.45-update.jar" />  
13:    <include name="guice-2.0-no_aop.jar" />  
14:    <include name="roboguice-1.1.2.jar" />  
15:   </fileset>  
16:  </path>  
17:  <path id="libApp">  
18:   <pathelement location="${android.jar}" />  
19:   <path refid="libPackage" />  
20:  </path>  
21:  <path id="libTest">  
22:   <fileset dir="lib\">  
23:   <include name="hamcrest-core-1.3.jar" />  
24:   <include name="junit-4.11.jar" />  
25:  </fileset>  
27:  <pathconvert property="info.libPackage" refid="libPackage" pathsep=";&#10;" />  
28:  <pathconvert property="info.libApp" refid="libApp" pathsep=";&#10;" />  
29:  <pathconvert property="info.libTest" refid="libTest" pathsep=";&#10;" />  
31:  <echo message="android.sdk: ${android.sdk}" />  
32:  <echo message="android.aapt: ${android.aapt}" />  
33:  <echo message="android.jar: ${android.jar}" />  
34:  <echo message="android.dex: ${android.dex}" />  
35:  <echo message="build.resource: ${build.resource}" />  
37:  <echo message="libPackage:" />  
38:  <echo message="${info.libPackage}" />  
39:  <echo message="libApp" />  
40:  <echo message="${info.libApp}" />  
41:  <echo message="libTest" />  
42:  <echo message="${info.libTest}" />  

It might seem a bit verbose but having everything defined here and having the actual values in the output will make debugging a lot simpler. libPackage are the bare minimum of libraries that we need to deploy with our app. libApp is the superset of these that we need to compile, basically libPackage + android.jar. libTest is a superset of that which includes libraries like junit.

Our first two real targets are the clean and init ones:

1:  <target name="clean">   
2:   <delete dir="build" />   
3:  </target>   
6:  <target name="init">   
7:   <tstamp/>   
8:   <mkdir dir="${build}"/>   
9:   <mkdir dir="${build}\aapt\"/>   
10:   <mkdir dir="${build}\javac\app"/>   
11:   <mkdir dir="${build}\javac\test"/>   
12:   <mkdir dir="${build}\dex"/>   
13:  </target>  

Step 2: Compiling

Next in line is compiling our project:

1:  <target name="build" depends="init" >   
2:   <exec executable="${android.aapt}" failonerror="true">   
3:    <arg value="package" />   
4:    <arg value="-f" />   
5:    <arg value="-v" />   
6:    <arg value="-M" />   
7:    <arg path="src\app\AndroidManifest.xml" />   
8:    <arg value="-A" />   
9:    <arg path="src\app\assets" />   
10:    <arg value="-I" />   
11:    <arg path="${android.jar}" />   
12:    <arg value="-m" />   
13:    <arg value="-J" />   
14:    <arg path="build\aapt\" />   
15:    <arg value="-F" />   
16:    <arg path="${build.resource}" />   
17:    <arg value="-S" />   
18:    <arg path="src\app\res" />   
19:    <arg value="--rename-manifest-package" />   
20:    <arg value="" />   
21:   </exec>   
24:   <javac destdir="build\javac\app" includeantruntime="false" classpathref="libApp" >   
25:    <src path="src\app\" />   
26:    <src path="build\aapt\com" />   
27:   </javac>   
30:   <javac destdir="build\javac\test" includeantruntime="false" classpathref="libTest" >   
31:    <src path="src\test\" />   
32:    <classpath>   
33:     <pathelement location="build\javac\app"/>   
34:     <path refid="libTest"/>   
35:    </classpath>   
36:   </javac>   
38:  </target>  

The first and most unfamiliar one is where we invoke the aapt tool. I honestly don't know what half of these options are because it's so poorly documented but the important ones are:

  • -M The location of your android manifest file.
  • -I The location of the android.jar that your using.
  • -J The location of the file (needed to compile your real code).
  • -F The location of the generated resource.jar.
  • -S The location of your resource files.
The first javac compiles our actual application, referencing the libApp libraries. The source files used are src\app (Application code) and build\aapt\com ( The second compiles our unit tests, making sure to add our application .class files to the classpath.

Step 3: Testing

This is a very generic task that runs our unit tests. For java developers it should look fairly standard, just run junit with our compiled .class files and libTest libraries in the classpath:

1:  <target name="test" depends="init, build" >   
2:   <junit haltonerror="true" haltonfailure="false" enableTestListenerEvents="true">   
3:    <classpath >   
4:     <pathelement location="build\javac\app"/>   
5:     <pathelement location="build\javac\test"/>   
6:     <path refid="libTest"/>   
7:    </classpath>   
8:    <batchtest>   
9:     <fileset dir="build\javac\test" includes="**/*.class" />   
10:     <formatter type="plain" usefile="false"/>   
11:    </batchtest>   
12:   </junit>   
13:  </target>  

At this point we've create our standard "build". It is completely independent of the IDE. with VI for example, I simply type :mak<enter> and all my tests are run in approximately 4 seconds for fast feedback.

Unfortunately unit tests aren't enough and we will want to actually run our app. In the next article I'll cover packaging, signing and deploying. 

No comments:

Post a Comment