PhoneGap Build is Adobe's cloud build service that can build iOS, Android, Windows Phone etc. apps for you based on a zip file with your Cordova/PhoneGap files. This article describes everything that you need to know when developing an app for PhoneGap Build either as someone completely new to this type of development or as an experienced Cordova/PhoneGap developer who hasn't used PhoneGap Build before.

My experience of using PhoneGap Build has been one of confusion through a lack of documentation that makes sense, a lack of a comprehensive guide that pieces everything together (like this one) and a confusion about the (in some cases quite remarkable) differences between using Cordova/PhoneGap locally and using PhoneGap Build. My hope is that this article can provide some guidance to help demystify PhoneGap Build for other developers so they have a much smoother experience using it.

Cordova vs PhoneGap vs PhoneGap Build

The documentation around Cordova, PhoneGap and PhoneGap Build leave a lot to be desired and it can be very confusing trying to decipher the differences between them. It doesn't help that Google searches often link to old versions of the documentation. Pro tip: Make sure that you are viewing with the latest version number (or edge) in the URL for the Cordova and PhoneGap documentation sites and with no version number on the PhoneGap Build documentation site; especially when clicking a link from Google.

I'm not going to delve too deeply into the difference between PhoneGap and Cordova, but in short they are very similar except:

  • PhoneGap has a different creation template (see below for more details)
  • PhoneGap puts config.xml inside of the www folder (which is where PhoneGap Build expects it), whereas Cordova puts it at the root alongside the www directory
  • PhoneGap allows you to perform remote builds from your machine using PhoneGap Build (phonegap remote build)
  • PhoneGap's CLI commands are slightly different in format from Cordova despite them largely being a 1:1 wrapper over Cordova
  • PhoneGap might have other differences in the future as Adobe add their own bits on top of Cordova

In contrast, the difference between PhoneGap Build and Cordova/PhoneGap is remarkable. This isn't necessarily a problem (especially if you are new to this type of development), but can cause a lot of confusion if you are familiar with Cordova/PhoneGap.

How does PhoneGap Build work?

The initiation of a build with PhoneGap Build is when you upload a zip file containing the www directory of you application (including the config.xml file) - this can be via the API or manually via the website. If you are familiar with Cordova then you will know that this means you are missing out on the platforms, plugins and hooks directories.

So how can PhoneGap Build work then? Essentially, PhoneGap Build requires you to add a bunch of custom tags to your config.xml file that instruct it how to build the plugins and platforms directories for you and there is currently no support for running hooks at all. If you are new to Cordova/PhoneGap then this might not mean much to you, but that's OK because it means you can get up and running quicker without having to have a deep understanding of them.

There is documentation for the custom tags that you can add to the config.xml, but it's not easy or quick to find and navigate through and digest all of that information. I worked most of it out initially by seeing the difference between the config.xml that results from running cordova create helloworld and phonegap create helloworld (the latter includes a bunch of default PhoneGap Build compatible tags for you). I've published an explanation of the differences to a Gist.

The most important bit for this discussion is the config.xml file that PhoneGap generates:

<?xml version='1.0' encoding='utf-8'?>
<widget id="com.phonegap.helloworld" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:gap="http://phonegap.com/ns/1.0">
    <name>HelloWorld</name>
    <description>
        Hello World sample application that responds to the deviceready event.
    </description>
    <author email="[email protected]" href="http://phonegap.com">
        PhoneGap Team
    </author>
    <preference name="permissions" value="none" />
    <preference name="phonegap-version" value="3.5.0" />
    <preference name="orientation" value="default" />
    <preference name="target-device" value="universal" />
    <preference name="fullscreen" value="true" />
    <preference name="webviewbounce" value="true" />
    <preference name="prerendered-icon" value="true" />
    <preference name="stay-in-webview" value="false" />
    <preference name="ios-statusbarstyle" value="black-opaque" />
    <preference name="detect-data-types" value="true" />
    <preference name="exit-on-suspend" value="false" />
    <preference name="show-splash-screen-spinner" value="true" />
    <preference name="auto-hide-splash-screen" value="true" />
    <preference name="disable-cursor" value="false" />
    <preference name="android-minSdkVersion" value="7" />
    <preference name="android-installLocation" value="auto" />
    <gap:plugin name="org.apache.cordova.battery-status" />
    <gap:plugin name="org.apache.cordova.camera" />
    <gap:plugin name="org.apache.cordova.media-capture" />
    <gap:plugin name="org.apache.cordova.console" />
    <gap:plugin name="org.apache.cordova.contacts" />
    <gap:plugin name="org.apache.cordova.device" />
    <gap:plugin name="org.apache.cordova.device-motion" />
    <gap:plugin name="org.apache.cordova.device-orientation" />
    <gap:plugin name="org.apache.cordova.dialogs" />
    <gap:plugin name="org.apache.cordova.file" />
    <gap:plugin name="org.apache.cordova.file-transfer" />
    <gap:plugin name="org.apache.cordova.geolocation" />
    <gap:plugin name="org.apache.cordova.globalization" />
    <gap:plugin name="org.apache.cordova.inappbrowser" />
    <gap:plugin name="org.apache.cordova.media" />
    <gap:plugin name="org.apache.cordova.network-information" />
    <gap:plugin name="org.apache.cordova.splashscreen" />
    <gap:plugin name="org.apache.cordova.vibration" />
    <icon src="icon.png" />
    <icon gap:platform="android" gap:qualifier="ldpi" src="res/icon/android/icon-36-ldpi.png" />
    <icon gap:platform="android" gap:qualifier="mdpi" src="res/icon/android/icon-48-mdpi.png" />
    <icon gap:platform="android" gap:qualifier="hdpi" src="res/icon/android/icon-72-hdpi.png" />
    <icon gap:platform="android" gap:qualifier="xhdpi" src="res/icon/android/icon-96-xhdpi.png" />
    <icon gap:platform="blackberry" src="res/icon/blackberry/icon-80.png" />
    <icon gap:platform="blackberry" gap:state="hover" src="res/icon/blackberry/icon-80.png" />
    <icon gap:platform="ios" height="57" src="res/icon/ios/icon-57.png" width="57" />
    <icon gap:platform="ios" height="72" src="res/icon/ios/icon-72.png" width="72" />
    <icon gap:platform="ios" height="114" src="res/icon/ios/icon-57-2x.png" width="114" />
    <icon gap:platform="ios" height="144" src="res/icon/ios/icon-72-2x.png" width="144" />
    <icon gap:platform="webos" src="res/icon/webos/icon-64.png" />
    <icon gap:platform="winphone" src="res/icon/windows-phone/icon-48.png" />
    <icon gap:platform="winphone" gap:role="background" src="res/icon/windows-phone/icon-173.png" />
    <gap:splash gap:platform="android" gap:qualifier="port-ldpi" src="res/screen/android/screen-ldpi-portrait.png" />
    <gap:splash gap:platform="android" gap:qualifier="port-mdpi" src="res/screen/android/screen-mdpi-portrait.png" />
    <gap:splash gap:platform="android" gap:qualifier="port-hdpi" src="res/screen/android/screen-hdpi-portrait.png" />
    <gap:splash gap:platform="android" gap:qualifier="port-xhdpi" src="res/screen/android/screen-xhdpi-portrait.png" />
    <gap:splash gap:platform="blackberry" src="res/screen/blackberry/screen-225.png" />
    <gap:splash gap:platform="ios" height="480" src="res/screen/ios/screen-iphone-portrait.png" width="320" />
    <gap:splash gap:platform="ios" height="960" src="res/screen/ios/screen-iphone-portrait-2x.png" width="640" />
    <gap:splash gap:platform="ios" height="1136" src="res/screen/ios/screen-iphone-portrait-568h-2x.png" width="640" />
    <gap:splash gap:platform="ios" height="1024" src="res/screen/ios/screen-ipad-portrait.png" width="768" />
    <gap:splash gap:platform="ios" height="768" src="res/screen/ios/screen-ipad-landscape.png" width="1024" />
    <gap:splash gap:platform="winphone" src="res/screen/windows-phone/screen-portrait.jpg" />
    <access origin="*" />
</widget>

All of the attributes and elements in the gap: namespace are PhoneGap specific and are used to instruct PhoneGap Build and provide a starting place to research all the different ways you can configure PhoneGap Build.

Configuration

PhoneGap Build has a number of custom preferences that you can add to your config.xml file; most of these preferences are covered in the PhoneGap and PhoneGap Build documentation:

Any preferences that are documented by your plugins should still be used in addition to the PhoneGap Build ones e.g. AndroidLaunchMode. There is also some overlap in some of the Cordova plugin preferences and the PhoneGap Build ones (e.g. AutoHideSplashScreen vs auto-hide-splash-screen) and in those situations I recommend you simply specify both settings. Unfortunately, the PhoneGap Build documentation for these settings leaves a lot to be desired so you might need to perform a bit of trial and error to make sure your app is configured correctly.

Something to keep in mind is that the plugins you specify have the ability to alter your configuration. This can be surprising because their configuration changes takes precedence over anything you upload in config.xml. The most common example of this is likely to be permissions.

It's also worth pointing out that there is a feature in the PhoneGapBuild documentation for adding custom modifications the iOS and Android configurations, which might come in handy for more advanced scenarios if you understand native Android and/or iOS development. This configuration ability is fairly limited at this point though.

Platforms

By default PhoneGap Build will build your app for all platforms. If you want to configure it to build specific platforms then you have to manually add gap:platform tags to your config.xml, e.g.:

<gap:platform name="ios" />
<gap:platform name="android" />

The full list of possible platforms is documented on the PhoneGap Build documentation.

For Cordova/PhoneGap developers: if you do a cordova platform add x or a phonegap build x that doesn't mean anything to PhoneGap Build - you have to manually add in these tags (unless you want it to simply build for all platforms).

Assets

For PhoneGap Build to access your assets (read: logos and splashscreens) then you need to include them in the www folder since that's what you include in the zip file you upload. The default structure that the PhoneGap CLI creates for you (illustrating just iOS and Android - there are others in a similar structure for the other platforms) is:

  • www/res/icon/android/icon-36-ldpi.png
  • www/res/icon/android/icon-48-mdpi.png
  • www/res/icon/android/icon-72-hdpi.png
  • www/res/icon/android/icon-96-xhdpi.png
  • www/res/icon/android/icon-96-xhdpi.png
  • www/res/icon/ios/icon-57-2x.png
  • www/res/icon/ios/icon-57.png
  • www/res/icon/ios/icon-72-2x.png
  • www/res/icon/ios/icon-72.png
  • www/res/screen/android/screen-hdpi-landscape.png
  • www/res/screen/android/screen-hdpi-portrait.png
  • www/res/screen/android/screen-ldpi-landscape.png
  • www/res/screen/android/screen-ldpi-portrait.png
  • www/res/screen/android/screen-mdpi-landscape.png
  • www/res/screen/android/screen-mdpi-portrait.png
  • www/res/screen/android/screen-xhdpi-landscape.png
  • www/res/screen/android/screen-xhdpi-portrait.png
  • www/res/screen/ios/screen-ipad-landscape-2x.png
  • www/res/screen/ios/screen-ipad-landscape.png
  • www/res/screen/ios/screen-ipad-portrait-2x.png
  • www/res/screen/ios/screen-ipad-portrait.png
  • www/res/screen/ios/screen-iphone-landscape-2x.png
  • www/res/screen/ios/screen-iphone-landscape.png
  • www/res/screen/ios/screen-iphone-portrait-2x.png
  • www/res/screen/ios/screen-iphone-portrait-568h-2x.png
  • www/res/screen/ios/screen-iphone-portrait.png

If you've done iOS or Android development then the filenames should be fairly self-explanatory even if they are slightly different from the standard filenames.

PhoneGap Build doesn't pick up these files based on convention so you don't have to use this folder structure, although for consistency and developer familiarity I recommend you do. The way to instruct PhoneGap Build where the splashscreens and icons are is via config.xml. If you refer to the config.xml snippet above, you will see entries like the following:

    <icon gap:platform="android" gap:qualifier="ldpi" src="res/icon/android/icon-36-ldpi.png" />
    <icon gap:platform="ios" height="57" src="res/icon/ios/icon-57.png" width="57" />
    <gap:splash gap:platform="android" gap:qualifier="port-ldpi" src="res/screen/android/screen-ldpi-portrait.png" />
    <gap:splash gap:platform="ios" height="480" src="res/screen/ios/screen-iphone-portrait.png" width="320" />

To understand all of these tags you can consult the PhoneGap Build documentation. Confusingly, the documentation uses the standard iOS/Android/etc. filenames rather than the ones that PhoneGap CLI outputs for you. For clarity, this is the config that I recently used for iOS and Android that had every size:

  <!-- Define app icon for each platform. -->
  <icon src="icon.png" />
  <icon src="res/icon/android/icon-36-ldpi.png"    width="36"  height="36"   gap:platform="android"  gap:density="ldpi"  />
  <icon src="res/icon/android/icon-48-mdpi.png"    width="48"  height="48"   gap:platform="android"  gap:density="mdpi"  />
  <icon src="res/icon/android/icon-72-hdpi.png"    width="72"  height="72"   gap:platform="android"  gap:density="hdpi"  />
  <icon src="res/icon/android/icon-96-xhdpi.png"   width="96"  height="96"   gap:platform="android"  gap:density="xhdpi" />
  <icon src="res/icon/android/icon-144-xxhdpi.png" width="144" height="144"  gap:platform="android"  gap:density="xxhdpi"/>
  <icon src="res/icon/ios/icon-29.png"             width="29"  height="29"   gap:platform="ios"                          />
  <icon src="res/icon/ios/icon-50.png"             width="50"  height="50"   gap:platform="ios"                          />
  <icon src="res/icon/ios/icon-40.png"             width="40"  height="40"   gap:platform="ios"                          />
  <icon src="res/icon/ios/icon-57.png"             width="57"  height="57"   gap:platform="ios"                          />
  <icon src="res/icon/ios/icon-29-2x.png"          width="58"  height="58"   gap:platform="ios"                          />
  <icon src="res/icon/ios/icon-60.png"             width="60"  height="60"   gap:platform="ios"                          />
  <icon src="res/icon/ios/icon-72.png"             width="72"  height="72"   gap:platform="ios"                          />
  <icon src="res/icon/ios/icon-76.png"             width="76"  height="76"   gap:platform="ios"                          />
  <icon src="res/icon/ios/icon-40-2x.png"          width="80"  height="80"   gap:platform="ios"                          />
  <icon src="res/icon/ios/icon-50-2x.png"          width="100" height="100"  gap:platform="ios"                          />
  <icon src="res/icon/ios/icon-57-2x.png"          width="114" height="114"  gap:platform="ios"                          />
  <icon src="res/icon/ios/icon-60-2x.png"          width="120" height="120"  gap:platform="ios"                          />
  <icon src="res/icon/ios/icon-72-2x.png"          width="144" height="144"  gap:platform="ios"                          />
  <icon src="res/icon/ios/icon-76-2x.png"          width="152" height="152"  gap:platform="ios"                          />
  <!-- Define app splash screen for each platform. -->
  <gap:splash src="res/screen/android/screen-ldpi-portrait.png"      gap:platform="android" gap:qualifier="port-ldpi"  />
  <gap:splash src="res/screen/android/screen-mdpi-portrait.png"      gap:platform="android" gap:qualifier="port-mdpi"  />
  <gap:splash src="res/screen/android/screen-hdpi-portrait.png"      gap:platform="android" gap:qualifier="port-hdpi"  />
  <gap:splash src="res/screen/android/screen-xhdpi-portrait.png"     gap:platform="android" gap:qualifier="port-xhdpi" />
  <gap:splash src="res/screen/android/screen-ldpi-landscape.png"     gap:platform="android" gap:qualifier="land-ldpi"  />
  <gap:splash src="res/screen/android/screen-mdpi-landscape.png"     gap:platform="android" gap:qualifier="land-mdpi"  />
  <gap:splash src="res/screen/android/screen-hdpi-landscape.png"     gap:platform="android" gap:qualifier="land-hdpi"  />
  <gap:splash src="res/screen/android/screen-xhdpi-landscape.png"    gap:platform="android" gap:qualifier="land-xhdpi" />
  <gap:splash src="res/screen/ios/screen-iphone-portrait.png"        gap:platform="ios"     width="320"  height="480"  />
  <gap:splash src="res/screen/ios/screen-iphone-portrait-2x.png"     gap:platform="ios"     width="640"  height="960"  />
  <gap:splash src="res/screen/ios/screen-iphone-portrait-568-2x.png" gap:platform="ios"     width="640"  height="1136" />
  <gap:splash src="res/screen/ios/screen-ipad-portrait.png"          gap:platform="ios"     width="768"  height="1024" />
  <gap:splash src="res/screen/ios/screen-ipad-landscape.png"         gap:platform="ios"     width="1024" height="768"  />
  <gap:splash src="res/screen/ios/screen-ipad-portrait-2x.png"       gap:platform="ios"     width="1536" height="2048" />
  <gap:splash src="res/screen/ios/screen-ipad-landscape-ios6-2x.png" gap:platform="ios"     width="2048" height="1536" />
  <gap:splash src="res/screen/ios/screen-ipad-portrait-ios6.png"     gap:platform="ios"     width="768"  height="1004" />
  <gap:splash src="res/screen/ios/screen-ipad-landscape-ios6.png"    gap:platform="ios"     width="1024" height="748"  />
  <gap:splash src="res/screen/ios/screen-ipad-portrait-ios6-2x.png"  gap:platform="ios"     width="1536" height="2008" />
  <gap:splash src="res/screen/ios/screen-ipad-landscape-ios6-2x.png" gap:platform="ios"     width="2048" height="1496" />

I recommend you only use the ones you need to make sure your package sizes are as small as possible e.g. if you don't support iOS6 or you don't support iPad then don't include them.

Given that the www directory is placed inside of the final deployment package, having a folder containing all of the icons and screenshots for every platform adds quite a lot of filesize weight to your packages. Luckily, there is a feature in PhoneGap Build that you can use to exclude directories from being put into the final package. To take advantage of this just drop a file with the filename .pgbomit into the www/res directory. The default output of phonegap create does this for you.

Plugins

To get PhoneGap Build to install a plugin you need to manually add an element to your config.xml file with the package id of a plugin that has been uploaded to the PhoneGap Build plugins repository.

So, for instance, if you want to install the in-app browser plugin you need to add this line to config.xml:

    <gap:plugin name="org.apache.cordova.inappbrowser" />

You can also lock down the version of the plugin it uses; consult the documentation for details.

An important point about the PhoneGap Build Plugin site: it's not obvious at first, but to find the official Cordova plugins there is a separate tab on that page called "PhoneGap Plugins". It's especially confusing because people have submitted packages with the same name as some of the official packages (e.g. search for "inappbrowser").

Plugins are probably the biggest pain point with PhoneGap Build - there is an indeterminate and by all accounts highly variable amount of time from submitting a plugin to it getting approved. Furthermore, there are plugins submitted by random people (not the plugin author) that aren't being maintained. If the plugins you want are there then all is good, but if there is a missing plugin or a bug in the version of one of the plugins there then you are in for a world of pain (and a big delay).

For Cordova/PhoneGap developers: To reiterate - installing a package using the CLI via cordova plugin add https://url-to-git-repository/with-a-plugin.git or cordova plugin add namespace.of.plugin (from the Cordova package repository) or via PhoneGap's two wrapper commands (that provide the same functionality) phonegap plugin add {package} and phonegap local plugin add {package} doesn't add this <gap:plugin ... /> element for you - you have to manually add it.

Using Cordova/PhoneGap locally (advanced)

While developing the application you may need to debug the application with full native functionality on a real device. In that scenario you might want to use Cordova/PhoneGap to deploy the app to your device e.g. using cordova run {platform} or phonegap run {platform}.

If you are new to Cordova/PhoneGap then you might want to skip this section and read the next one.

If you decide to do this then there are a number of things to consider:

.gitignore

You should use your .gitignore file (or equivalent for your source control) to ignore the following directories:

  • platforms
  • plugins

That stops you from accidentally making a change that won't apply on PhoneGap Build since these files will always be generated on the fly on a fresh checkout or a git clean (or equivalent).

You could also ignore the hooks directory, but as you will see below it's useful to add some hooks to mimic PhoneGap Build.

Plugins

Since you are ignoring the plugins directory - when you install a plugin using Cordova that fact won't be stored in source control since this is recorded in plugins/{platform}.json. Thus, you need to create a hook that will take care of installing any required plugins for a fresh checkout.

You can do this by adding the following hook to hooks/after_prepare/001_install_plugins.js (the 001 can be replaced with whatever number you are up to; you should replace the pluginlist with your plugins):

#!/usr/bin/env node

// http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/

//this hook installs all your plugins

// add your plugins to this list--either
// the identifier, the filesystem location
// or the URL
var pluginlist = [
    "org.apache.cordova.device",
    "org.apache.cordova.geolocation",
    "org.apache.cordova.inappbrowser",
    "org.apache.cordova.splashscreen",
    "com.verso.cordova.clipboard",
    "https://github.com/driftyco/ionic-plugins-keyboard.git"
];

// no need to configure below

var fs = require('fs');
var path = require('path');
var sys = require('sys')
var exec = require('child_process').exec;

function puts(error, stdout, stderr) {
    sys.puts(stdout)
}

pluginlist.forEach(function(plug) {
    exec("cordova plugin add " + plug, puts); // You could replace cordova with phonegap if you like
});

Adding a new plugin to your application should then result in this workflow:

  1. Check the plugin is available on PhoneGap Build
  2. Add the plugin id to the config.xml for PhoneGap Build: <gap:plugin name="com.plugin.id" />
  3. Add the plugin repository or id to the pluginlist array in the hook file above
  4. Run cordova build or phonegap build {platform} to have the package installed locally

If you have a clean checkout then when adding a platform or doing a build you might get the following error:

Error: ENOENT, no such file or directory 'c:\...\android.json'
    at Object.fs.openSync (fs.js:427:18)
    at Object.fs.writeFileSync (fs.js:966:15)
    at Object.save_platform_json (...)
    ...

This simply means the plugins directory isn't present and should be created first. You can avoid this by either committing the plugins directory with a dummy file in it (before adding plugins to .gitignore) so it's always present or by simply executing mkdir plugins after a clean checkout.

Assets

If you want the icon and splashscreen files to be present in your local Cordova build then you will need to copy the files into the relevant parts of the platforms directory. If you look at the config.xml file above you might notice that, unlike <gap:splash ... />, <icon ... /> doesn't have the phonegap XML namespace gap: - it's actually a part of the config.xml schema. Cordova will look for your <icon ... /> elements and copy the files you specify into the correct place in the platforms directory for you.

Unfortunately, the base filepath that Cordova uses differs from PhoneGap Build:

  • Cordova expects filepaths from / e.g. <icon src="www/res/icon/android/icon-36-ldpi.png" width="36" height="36" gap:platform="android" gap:density="ldpi" />
  • PhoneGap Build expects filepaths from /www e.g. <icon src="res/icon/android/icon-36-ldpi.png" width="36" height="36" gap:platform="android" gap:density="ldpi" />

This leaves you with two options:

  1. Use a hook to copy the icons as well as the splashscreens
  2. Use www/... in the file paths and just before creating the zip file to upload to PhoneGap Build change the paths to remove the www/
    • For instance, I used the following PowerShell replacement in my deployment script: (Get-Content config.xml) -Replace """0.0.0.0""","""$version""" -Replace "src=""www/","src=""" | Set-Content config.xml (which also includes the replacement for the build version)

You can copy the splashscreens to the correct place by adding the following hook to hooks/after_prepare/002_copy_assets.js (the 002 can be replaced with whatever number you are up to):

#!/usr/bin/env node

var fs = require("fs");
var path = require("path");

var rootdir = process.argv[2];

function copy(source, destination) {
    var sourceFile = path.join(rootdir, source);
    var destinationFile = path.join(rootdir, destination);

    fs.writeFileSync(destinationFile, fs.readFileSync(sourceFile));
    console.log("Copied " + sourceFile + " to " + destinationFile);
}

if (fs.existsSync(path.join(rootdir, "platforms/android"))) {
    // Cordova copies the iOS icon over this
    copy("www/res/icon/android/icon-72-hdpi.png", "platforms/android/res/drawable-hdpi/icon.png")
    // Cordova doesn't currently support xxhdpi icons
    fs.mkdir(path.join(rootdir, "platforms/android/res/drawable-xxhdpi"), function(){});
    copy("www/res/icon/android/icon-144-xxhdpi.png", "platforms/android/res/drawable-xxhdpi/icon.png")
    // Cordova doesn't copy in splash screens automatically
    copy("www/res/screen/android/screen-ldpi-portrait.png", "platforms/android/res/drawable-port-ldpi/screen.png")
    copy("www/res/screen/android/screen-ldpi-landscape.png", "platforms/android/res/drawable-land-ldpi/screen.png")
    copy("www/res/screen/android/screen-mdpi-portrait.png", "platforms/android/res/drawable-port-mdpi/screen.png")
    copy("www/res/screen/android/screen-mdpi-landscape.png", "platforms/android/res/drawable-land-mdpi/screen.png")
    copy("www/res/screen/android/screen-hdpi-portrait.png", "platforms/android/res/drawable-port-hdpi/screen.png")
    copy("www/res/screen/android/screen-hdpi-landscape.png", "platforms/android/res/drawable-land-hdpi/screen.png")
    copy("www/res/screen/android/screen-xhdpi-portrait.png", "platforms/android/res/drawable-port-xhdpi/screen.png")
    copy("www/res/screen/android/screen-xhdpi-landscape.png", "platforms/android/res/drawable-land-xhdpi/screen.png")
}

if (fs.existsSync(path.join(rootdir, "platforms/ios"))) {
    copy("www/res/screen/ios/screen-iphone-portrait.png", "platforms/ios/{PROJECT_NAME}/Resources/splash/Default~iphone.png");
    copy("www/res/screen/ios/screen-iphone-portrait-2x.png", "platforms/ios/{PROJECT_NAME}/Resources/splash/[email protected]~iphone.png");
    copy("www/res/screen/ios/screen-iphone-portrait-568-2x.png", "platforms/ios/{PROJECT_NAME}/Resources/splash/[email protected]~iphone.png");
    copy("www/res/screen/ios/screen-ipad-portrait.png", "platforms/ios/{PROJECT_NAME}/Resources/splash/Default-Portrait~ipad.png");
    copy("www/res/screen/ios/screen-ipad-portrait-2x.png", "platforms/ios/{PROJECT_NAME}/Resources/splash/[email protected]~ipad.png");
    copy("www/res/screen/ios/screen-ipad-landscape.png", "platforms/ios/{PROJECT_NAME}/Resources/splash/Default-Landscape~ipad.png");
    copy("www/res/screen/ios/screen-ipad-landscape-2x.png", "platforms/ios/{PROJECT_NAME}/Resources/splash/[email protected]~ipad.png");
}

Note: This only included Android and iOS and didn't include icons - it's left as an exercise for the reader to add support for those if needed.

Scripted deployments

It can be tedious to execute all of the correct commands to deploy the app given:

  • Sometimes it will be a clean checkout and require you to add the platform
  • You might need to, for instance, compile SASS or LESS stylesheets or do some other action
  • You might need to perform other actions like set an environment variable
    • For instance, we used environment variables to set whether the app being built should point to the local, staging or production API endpoint

To that end, it might help to provide scripts that developers can execute to quickly deploy the app to a connected device for testing/debugging.

The following is an example batch file (.bat) we used to deploy the app pointing to the staging API to an Android device on Windows:

SET TARGET=staging
setlocal EnableDelayedExpansion
FOR %%b IN ("%~dp0www\css\*.scss") DO (
  "%~dp0node\node" "%~dp0node\node_modules\node-sass\bin\node-sass" "%~dp0www\css\%%~nb.scss" "%~dp0www\css\%%~nb.css"
  if !errorlevel! neq 0 exit /b !errorlevel!
)
mkdir plugins
CALL cordova platform add android
CALL cordova run android

If using PhoneGap the last two lines could be replaced with CALL phonegap run android (PhoneGap automatically adds the platform for you).

The following is an example shell file (.sh) we used to build the iOS app pointing to the local API on Mac OSX and open XCode so the developer could hit the play button to deploy the app:

#!/bin/bash

# Make sure you: sudo npm install -g node-sass
cd $(dirname $0)
mkdir plugins
cd www/css
ls *.scss | xargs -n1 node-sass
cd $(dirname $0)
export TARGET=local
cordova platform add ios
cordova build ios
open platforms/ios/{PROJECT_NAME}.xcodeproj

Alternatives to using Cordova locally

It's actually possible to get away without using Cordova at all when using PhoneGap Build.

If you host the www folder on a web server locally (or use the cordova serve or phonegap serve command) and access it with your device then you can use the device's web browser to test out most of the features you are developing (apart from any native plugins).

If you need to view the app on a device to test native functionality then there are still options:

  • Use the remote build service (phonegap remote build) from the PhoneGap CLI (it's pretty slow though)
  • Use the Hydration feature and a quick, automated deployment process (will be too slow for tedious debugging though)
  • Use the PhoneGap developer app (this doesn't work with custom plugins though - only the in-built ones)

It's possible that the options above are perfectly suitable for your situation, but if you are doing a lot of native integration and need to do tedious debugging then I recommend using Cordova/PhoneGap for it's speed advantages (albeit requiring you to learn Cordova/PhoneGap if you haven't already).

Deployments

Manual builds

There are two ways to manually build an application using PhoneGap Build:

  • Use the phonegap remote build command
  • Manually zip your www directory (including config.xml) and uploading it on the PhoneGap Build website.
    • Upload using PhoneGap Build
    • Note: If you click on your application and then click on "Settings" there is a dialog under "Basic" to upload "Source code" - as far as I can tell this does nothing (but it looks like it does because it triggers a rebuild) so don't get confused by it; always use "Update code" to update your application

You can configure your application in the "Settings" tab of your application on the PhoneGap Build website. This currently allows you to:

Automated builds

You can use the developer API to easily trigger deployments from an automated script. I have published an example of a PhoneGapBuild deployment process along with some code to utilise the PhoneGap Build API to my blog.

Beta testing

Beta testing is really easy for Android apps. Not only can you use the same package as for production, but you can easily manage your Beta testers (and your Alpha testers) via a Google group. This means you have a channel of communication directly tied to the ability for people to access your app.

Things get a bit trickier when looking at Beta testing Windows Phone and iOS apps.

Windows Phone allows you to manage the Beta and access to it via their Dev Center, which is nice, however you need to use a different package id. With PhoneGap Build this requires you to have a separate application (moving you to the paid tier if you only had one app otherwise) and for you to tweak the config.xml file just before uploading to change the id.

iOS has an appallingly complicated system for both managing the Beta testers (you have to get them to give you the UUID of their device - it's not tied to their Apple ID) and the generation of a package you can distribute outside of the app store (you need a separate provisioning profile with all the UUIDs registered on it). The example of a PhoneGapBuild deployment process that I published on my blog contains a description and some code to illustrate how to accomplish this.

Secret Management

In order to sign your apps you need to upload your private keys to PhoneGap Build. Confusingly, these aren't tied to your app, but rather to your account (hence it's not immediately obvious how to do it).

In order for a key to be used to perform a build you need to do two things:

  1. Select that key as the one that should be used for a particular platform (you can have multiple keys for a platform)
  2. Ensure you have unlocked that key within the last hour by providing the password

There are instructions on the PhoneGap Build documentation for how to get the Android and iOS keys.

It's important to ensure the secrecy of the password that you use to unlock your private keys. If your private keys and their password is exposed then someone could create an application package that looks like it's from you. If that does happen then it's possible to regenerate a different key for iOS, but for Android you would have to start again.

Since you need to expose your password every time a build happens on PhoneGap Build (unless the build happens within an hour of the last build) I recommend that you implement an automated process using the Developer API that protects the passwords. The example of a PhoneGapBuild deployment process that I published on my blog explains one way of doing that.

Conclusion

PhoneGap Build provides you with a lot of out-of-the-box power hidden behind a web of somewhat confusing documentation. It essentially requires you to abandon using Cordova almost completely in favour of a simpler experience that you can get up and running with much faster. In doing so though, you trade away a lot of the power and flexibility that Cordova affords you.

I see the advantages of PhoneGap Build being:

  • Easier to ramp up if you are completely new to mobile app development (assuming you can navigate the documentation or have a handy reference like this article)
  • You don't have to set up a complex array of build servers with tedious configuration for all the different SDKs and operating systems needed
  • You can develop using just www and config.xml without needing to worry about the complex hooks, platforms or plugins directories
  • This also means you don't have to go through the complex and time-consuming set of your local development environment apart from installing node.js and PhoneGap
  • You can easily automate the deployment of your app by connecting your Git repository or using the PhoneGap Build API from your CI server
  • You can make use of powerful development features such as remote debugging and automatic updates with the effort of ticking a checkbox
  • Free for 1 project, $10/month for 25 - pretty reasonable

I also see the following disadvantages of PhoneGap Build:

  • Confusing to ramp up if you are a Cordova developer given how different it is (the documentation really doesn't help make that clear)
  • The custom plugin situation is scary - there is a likelihood that you could be left in the lurch needing a bug fix or a plugin that hasn't been uploaded before and need to wait weeks without an avenue of escalation (that I could see)
  • Lose the full flexibility and power of Cordova (e.g. no hooks and very limited ability to change native configuration)
  • There is a lag for new Cordova versions to be adopted by PhoneGap Build if you need features in the latest version

In closing, I recommend you decide how much you're willing to trade off ease of development and deployment vs control and flexibility and use that to choose the right tool for the job.

Contact me

You can contact me via Twitter or my blog.