You know The Joel Test, right? If you don’t, I won’t judge you, have a look, I’ll be here when you’re done: The Joel Test: 12 Steps to Better Code.

Today I’m honing in on Step 2: Can you make a build in one step? and putting a spin on it: Can you make a build in one step, when you have XCode, Flex Builder, ADT, Flex Builder and ADT again in the pipeline?

Those of you who write their own native extensions know what I mean. Changing something in the native code usually means 6 steps, in order to test it on your device:

  1. Rebuild the extension’s native code in XCode.
  2. Rebuild the extension’s AIR library in Flex Builder.
  3. Package the extension into an ANE (AIR Native Extension) file, using ADT, Adobe’s AIR Developer Tool (you can’t do this in Flex Builder).
  4. Rebuild the app in Flex Builder.
  5. Package the app into an IPA (iPhone application archive) file, again using ADT (this step you can do in Flex Builder).

And this is before you even upload the app to a device to run or start debugging it.

True to my ‘two days of clicking save two hours of reading the manual’ principle, I went in loops through these steps quite a few times, before I finally got fed up and decided that it was time to turn this into a single-click operation. If you are at this stage and haven’t done your homework yet, read on and implement. Copying and pasting is encouraged.

The components for the one-step build

… are easy to guess:

  1. A command line for building the XCode project
  2. A command line for building the AIR extension project and packaging it into an ANE file
  3. A command line for cleaning and building the AIR app and packaging it into an IPA file
  4. One script to rule them all, one script to bind them… Preferably parameterised, so we can choose whether we want to build Debug or Release, let it take in passwords, etc.

Note: Step 1 and parts of step 2 require Mac OS. However, if you have a number of Flex Builder projects to build, you can use the rest of the process pretty much unchanged on Windows.

Let’s get started.

1. Building the XCode project on the command line

To do a default (Debug) build of your XCode project, all you need to do is navigate on the command line to the folder where your .xcodeproj is and execute this:

[sourcecode language=”bash”]
xcodebuild
[/sourcecode]

Or, if you wanted to do a clean build:

[sourcecode language=”bash”]
xcodebuild clean
xcodebuild
[/sourcecode]

We want the ability to choose whether we want a Release or a Debug build, so it would be a good idea to parameterise that:

[sourcecode language=”bash”]
#Pass Debug or Release as a command line parameter:
build_type_iOS=$1

#And then use this parameter to perform a clean build:
xcodebuild clean -configuration "${build_type_iOS}"
xcodebuild -configuration "${build_type_iOS}"
[/sourcecode]

This will build the .a library of our extension and put either in the build/Release-iphoneos or the build/Debug-iphoneos folder of the XCode project.

A couple of words on structuring folders

Before we get on with the FlexBuilder side of things, I would like to give you the big picture of where everything is.  You are welcome to build and package the extension in any folder that suits your personality – I like putting everything in little drawers and labelling them. So I tend to keep folders for putting the final ANE and IPA files, which are outside the Flex Builder projects and, inside the projects, folders for keeping build scripts, in this case ant/.

My guinea pig for this example is a Native Camera Extension for iOS:

  • binaries/ is where the extension’s ANE file and the app’s IPA file will eventually end up;
  • FlexApp/ contains the app’s FlexBuilder project;
  • FlexCameraExtensionLib/ contains the extension’s AIR library project;
  • NativeCameraExtension/ contains the native XCode project.

Each of the two Flex Builder projects contains an ant/ folder with build scripts. You guessed it: we’ll be using Ant tasks for building the Flex projects.

2. Building and packaging the ANE on the command line

All we need for this step is located in the Flex Builder project’s ant/ folder of the extension:

  • build.xml – an ant script for building and packaging the ANE

and all other files the script will use:

  • build.properties, where folders, file names and other parameters are defined for use in the build
  • local.properties – a list of environment variables, which point to the SDKs and tools that will be used to build the extension (path to MXMLC – the Flex compiler, path to ADT (AIR Developer Tool, which will create the ANE package) path to the Flex SDK and the iOS SDK)
  • FlexCameraExtensionLib_flex.config – a dump of the project’s compile settings in XML format.
    This is generated by Flex Builder: to get a project’s settings dumped in a file, in the Project Properties dialog open Flex Compiler and add this to option under Additional compiler arguments:

[sourcecode language=”bash”]mxmlc -dump-config myapp-config.xml[/sourcecode]

You can find more information on configuration files in Adobe’s Help Center: About configuration files.

Here is the build.xml ANT script with its components explained:

[sourcecode language=”xml”]
<?xml version="1.0" encoding="UTF-8"?>
<project name="FlexCameraExtensionLib" default="package ane" basedir=".">

<!– All properties are defined in these two files: –>
<property file="local.properties" />
<property file="build.properties" />

<!– The clean target gets rid of the products of our previous build: –>
<target name="clean">
<delete dir="${lib.builddir}"/>
<mkdir dir="${lib.builddir}"/>
</target>

<!– Run the compc Flex compiler to build the .swc file of the AIR library: –>
<target name="build" depends="clean">
<exec executable="${COMPC}">
<arg line="
+configname=airmobile
-output ${lib.builddir}/${lib.libname}.swc
-source-path+=${lib.sourcedir}
-include-classes+=com.diadraw.extensions.camera.NativeCameraExtension
-load-config+=FlexCameraExtensionLib_flex.config
"/>
</exec>
</target>

<!– Prepare paths and gather the ingredients for the ANE package: –>
<target name="copy files for packaging" depends="build">
<!– Delete any old packages and create a folder to put the files in: –>
<delete dir="${lib.packagedir}"/>
<mkdir dir="${lib.packagedir}"/>
<!– Inside that folder, create a subfolder, where the ANE file will be put.
Note: when the ANE package is created, anything that is in the same folder as the ANE
will be put inside the package, thus it’s best to start with an empty folder. –>
<mkdir dir="${lib.packageoutput}"/>

<!– First ingredient: the library SWC file, which is really an archive of
two files: library.swf and catalog.xml.
We only need library.swf, so we unzip the SWC and delete catalog.xml: –>
<copy file="${lib.builddir}/${lib.swcfilename}" tofile="${lib.packagedir}/${lib.swcfilename}"/>
<unzip src="${lib.packagedir}/${lib.swcfilename}" dest="${lib.packagedir}"/>
<delete file="${lib.packagedir}/catalog.xml"/>

<!– The next two ingredients are the two .xml files the accompany the extension: –>
<copy file="${lib.sourcedir}/${package.extenxion_options}" tofile="${lib.packagedir}/${package.extenxion_options}"/>
<copy file="${lib.sourcedir}/${package.platform_options}" tofile="${lib.packagedir}/${package.platform_options}"/>

<!– And last, we need the native .a library: –>
<copy file="${lib.native_library_path}/${lib.native_library}" tofile="${lib.packagedir}/${lib.native_library}" />
</target>

<!– All the ingredients for the ANE package have been gathered, now create the package: –>
<target name="package ane" depends="copy files for packaging">
<exec executable="${ADT}">
<arg line="
-package
-target ane
${lib.packageoutput}/${package.ane}
${lib.packagedir}/${package.extenxion_options}
-swc ${lib.packagedir}/${lib.swcfilename}
-platform iPhone-ARM
-C ${lib.packagedir} .
-platformoptions ${lib.packagedir}/${package.platform_options}
"/>
</exec>

<!– The ANE is now inside ant/package/ane, copy it where we want it to end up for use from apps
and clean up the files we copied earlier, but no longer need: –>
<delete file="${package.destinationdir}/${package.ane}" />
<copy file="${lib.packageoutput}/${package.ane}" tofile="${package.destinationdir}/${package.ane}" />
<delete dir="${lib.packagedir}"/>
</target>

</project>
[/sourcecode]

local.properties defines the following environment variables:

[sourcecode language=”css”]
FLEX_HOME=/Applications/Adobe Flash Builder 4.6/sdks/4.6.0
COMPC=${FLEX_HOME}/bin/compc
ADT=${FLEX_HOME}/bin/adt
[/sourcecode]

build.properties has all the project-specific information:

[sourcecode language=”css”]
lib.native_library_path="Define this on the command line"
lib.native_library=libNativeCameraExtensioniOS.a
lib.libname=FlexCameraExtensionLib
lib.swcfilename=${lib.libname}.swc
lib.builddir=../bin
lib.sourcedir=../src/
lib.packagedir=package
lib.packageoutput=${lib.packagedir}/ane

package.extenxion_options=${lib.libname}-extension.xml
package.platform_options=${lib.libname}-ios-platformoptions.xml
package.ane=${lib.libname}.ane
package.destinationdir=../../binaries/ane
[/sourcecode]

Here is how you use all of this to do a clean build and package the ANE: on the command line navigate to where your ant script is and call it with the “package ane” target – this will cause all of the targets to be executed.

[sourcecode language=”bash”]ant "package ane"[/sourcecode]

Note that any of the properties, defined in build.properties can be overridden on the command line. That’s actually a good thing, as there are things we would like to parameterise. You’ll also notice I’ve left this little gem in:

[sourcecode language=”css”]lib.native_library_path="Define this on the command line"[/sourcecode]

This is how we override it on the command line:

[sourcecode language=”bash”]
ant "package ane"-Dlib.native_library_path=../../NativeCameraExtension/iOS/NativeCameraExtensioniOS/build/"${build_type_iOS}"-iphoneos
[/sourcecode]

3. Building and packaging the IPA on the command line

We’ll use another ANT script and the same file structure as in the previous step:

build.xml:

[sourcecode language=”xml”]
<?xml version="1.0" encoding="UTF-8"?>
<project name="CameraTestApp" default="package ipa" basedir=".">

<!– All properties are defined in these two files: –>
<property file="local.properties" />
<property file="build.properties" />

<!– The clean target gets rid of the products of our previous build: –>
<target name="clean">
<delete dir="${app.builddir}"/>
<mkdir dir="${app.builddir}"/>
</target>

<!– We need to copy the assets/ folder, any icon files and the app descriptor file (-app.xml) –>
<target name="copy files for building" depends="clean">
<copy todir="${app.builddir}" preservelastmodified="true" verbose="true">
<fileset dir="${app.sourcedir}">
<patternset>
<include name="assets/**"/>
<include name="*.png"/>
<include name="*.xml"/>
</patternset>
</fileset>
</copy>
</target>

<!– Run the mcmlc Flex compiler to build the .swf file for the app: –>
<target name="build" depends="copy files for building">
<exec executable="${MXMLC}">
<arg line="
+configname=airmobile
-output ${app.builddir}/${app.swffile}
${app.source}
-source-path+=${app.sourcedir}
-load-config+=${app.name}_flex.config
"/>
</exec>
</target>

<!– Prepare paths and gather the ingredients for the IPA package: –>
<target name="copy files for packaging" depends="build">
<!– Delete any old packages and create a folder to put the files in: –>
<delete dir="${app.packagedir}"/>
<mkdir dir="${app.packagedir}"/>
<!– Inside that folder, create a subfolder, where the IPA file will be put.
Note: when the ANE package is created, anything that is in the same folder as the ANE
will be put inside the package, thus it’s best to start with an empty folder. –>
<mkdir dir="${app.packageoutput}"/>

<!– Copy the app SWF file, its descriptor and all assets and icons: –>
<copy todir="${app.packagedir}" preservelastmodified="true" verbose="true">
<fileset dir="${app.builddir}">
<patternset>
<include name="assets/**"/>
<include name="*.png"/>
<include name="*.xml"/>
<include name="*.swf"/>
</patternset>
</fileset>
</copy>

<!– Copy any extension files –>
<delete dir="${package.extensiondir}"/>
<mkdir dir="${package.extensiondir}"/>

<copy todir="${package.extensiondir}" preservelastmodified="true" verbose="true">
<fileset dir="${ext.extensiondir}">
<patternset>
<include name="*.ane"/>
</patternset>
</fileset>
</copy>
</target>

<!– All the ingredients for the IPA package have been gathered, now create the package: –>
<target name="package ipa" depends="copy files for packaging">
<exec executable="${ADT}">
<arg line="-package
-target ${package.target_type}
-storetype ${package.storetype}
-keystore ${package.privatekey}
-storepass ${package.storepass}
-provisioning-profile ${package.mobileprovision}
${app.packageoutput}/${package.ipa}
${app.packagedir}/${app.descriptor}
-C ${app.packagedir} ${app.swffile}
-platformsdk ${IOS_SDK}
-extdir ${package.extensiondir}
assets
"/>
</exec>

<!– The IPA is now inside ant/package/ane, copy it where we want it to end up for use from apps
and clean up the files we copied earlier, but no longer need: –>
<delete file="${package.destinationdir}/${package.ipa}" />
<copy file="${app.packageoutput}/${package.ipa}" tofile="${package.destinationdir}/${package.ipa}" />
<delete dir="${app.packagedir}"/>
<delete dir="${package.extensiondir}"/>
</target>

</project>
[/sourcecode]

local.properties:

[sourcecode language=”css”]
IOS_SDK=/Developer/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.0.sdk
FLEX_HOME=/Applications/Adobe Flash Builder 4.6/sdks/4.6.0
MXMLC=${FLEX_HOME}/bin/mxmlc
ADT=${FLEX_HOME}/bin/adt
[/sourcecode]

build.properties:

[sourcecode language=”css”]
<!– /*** You can replace the value of any of these properties on the command line, using the -D option.
* For example, to pass a password and a build type, call ant like this:
ant ant "package ipa" -Dpackage.storepass="NewPassword"  -Dbuild.type="release"
*/ –>

build.type=debug
ext.extensiondir=../../binaries/ane
app.name=CameraTestApp
app.rootdir=../
app.builddir=${app.rootdir}/bin-${build.type}
app.packagedir=package
app.packageoutput=${app.packagedir}/ipa
app.sourcedir=${app.rootdir}/src
app.source=${app.sourcedir}/CameraTestApp.mxml
app.swffile=${app.name}.swf
app.descriptor=${app.name}-app.xml
app.includes=assets

package.target_type=ipa-debug-interpreter
package.extensiondir=ane
package.storetype=pkcs12
package.privatekey=your_ios_developer_private_key.p12
package.mobileprovision=your_iOS_Team_Provisioning_Profile_.mobileprovision
package.storepass=YourAppStoreDeveloperPassword
package.ipa=${app.name}.ipa
package.destinationdir=../../binaries/ipa
[/sourcecode]

Finally, the line we need to execute, in order to build and package the IPA:

[sourcecode language=”bash”]
ant "package ipa"
[/sourcecode]

And the same line, parameterised with data, which you might not want to store in a file, such as certificate names, password and type of build:

[sourcecode language=”bash”]
ant "package ipa" -Dpackage.privatekey="${private_key_file}" -Dpackage.mobileprovision="${mobile_provision_file}" -Dpackage.storepass="${app_store_password}" -Dbuild.type="${build_type_AS}"
[/sourcecode]

4. Putting the steps together: the one-click build script

This is the easy bit: calling all of the command lines from a single script. Each of these command lines will have to run in a specific location, so my script makes sure to set its working folder for each of them accordingly.

This is the meat of it – calling the three command lines we defined above:

[sourcecode language=”bash”]
# Do a clean build of the native iOS library:
pushd ../NativeCameraExtension/iOS/NativeCameraExtensioniOS
xcodebuild clean -configuration "${build_type_iOS}"
xcodebuild -configuration "${build_type_iOS}"
popd

# Do a clean build of the extension SWC and package it into ANE:
pushd ../FlexCameraExtensionLib/ant
ant "package ane" -Dlib.native_library_path=../../NativeCameraExtension/iOS/NativeCameraExtensioniOS/build/"${build_type_iOS}"-iphoneos
popd

# Do a clean build of the Flex app and package it into IPA:
pushd ../FlexApp/ant
ant "package ipa" -Dpackage.privatekey="${private_key_file}" -Dpackage.mobileprovision="${mobile_provision_file}" -Dpackage.storepass="${app_store_password}" -Dbuild.type="${build_type_AS}"
popd
[/sourcecode]

And here is the final version of the script, error checks and all, which takes various parameters, in order to run. It runs checks on them and makes sure that they are passed in the format that each step requires (for example, the Flex side works with ‘debug’ and ‘release’, where XCode needs them capitalised: ‘Debug’ and ‘Release’).

[sourcecode language=”bash”]
#!/bin/bash

# Get the paths to the certificate and mobile provisioning files
# and the app store password from the command line:
private_key_file=$1
if [ -z "${private_key_file}" ]
then
echo "path to your .p12 file:"
read private_key_file
fi

mobile_provision_file=$2
if [ -z "${mobile_provision_file}" ]
then
echo "path to your .mobileprovision file:"
read mobile_provision_file
fi

app_store_password=$3
if [ -z "${app_store_password}" ]
then
echo "password:"

stty_orig=`stty -g` stty -echo
read app_store_password
stty $stty_orig
fi

# Optional command line argument: debug or release, defaults to debug, if nothing is passed on the command line
build_type=$4
if [ -z "${build_type}" ]
then
build_type="debug"
fi
# We want lowercase ‘debug’/’release’ for ActionScript
build_type_AS=$( echo "${build_type}" | tr "[:upper:]" "[:lower:]" )
# and uppercase first letter for iOS: ‘Debug’/’Release’
build_type_iOS=$( echo "${build_type:0:1}" | tr "[:lower:]" "[:upper:]" )${build_type:1}

# Do a clean build of the native iOS library:
pushd ../NativeCameraExtension/iOS/NativeCameraExtensioniOS
xcodebuild clean -configuration "${build_type_iOS}"
xcodebuild -configuration "${build_type_iOS}"

# Check whether the build succeeded and stop the script, if not:
if [ $? -ne 0 ]
then
exit $?
fi
popd

# Do a clean build of the extension SWC and package it into ANE:
pushd ../FlexCameraExtensionLib/ant
ant "package ane" -Dlib.native_library_path=../../NativeCameraExtension/iOS/NativeCameraExtensioniOS/build/"${build_type_iOS}"-iphoneos

# Check whether the build succeeded and stop the script, if not:
if [ $? -ne 0 ]
then
exit $?
fi
popd

# Do a clean build of the Flex app and package it into IPA:
pushd ../FlexApp/ant
ant "package ipa" -Dpackage.privatekey="${private_key_file}" -Dpackage.mobileprovision="${mobile_provision_file}" -Dpackage.storepass="${app_store_password}" -Dbuild.type="${build_type_AS}"

# Check whether the build succeeded and stop the script, if not:
if [ $? -ne 0 ]
then
exit $?
fi
popd
[/sourcecode]

Now we are free to either call this on the command line and pass parameters every time or set up several other scripts, which will call this one with predefined parameters: one for Debug, one for Release and so on, and have them scheduled to run nightly.

This pretty much covers the one-step build requirement for us. Hats off to Joel Spolsky.

About the Author

Radoslava is co-founder of DiaDraw. Prefers to communicate with images. Verbal communication always caused trouble with her parents. Started speaking Basic early on, followed by four years of Delphi, six years of C++, four years of ActionScript, lately converses in Objective-C. Her mum and dad hope she'll start speaking human at some point.