Guest post – inside Hojoki’s mobile build process

This guest blog post was written by Patrick Rudolph, Mobile Lead at Hojoki. They are a Germany-based startup that provide a platform for individuals and teams, integrating all your different cloud apps into one comprehensive app. As well as their web app, Hojoki offers mobile apps for iOS and Android (powered by Trigger.io).

 

By the time we decided to build our mobile apps with Trigger.io, we already had a web app in place for a year. There were existing processes that made testing and deploying easier, so we figured it might be best if the mobile development takes advantage of these practices. See Hojoki in action with this screencast by Amir of Trigger.io:

Here’s a run-down of the various tools, processes and scripts we use to handle our mobile builds.

When it comes to building our apps, we use Grunt, a task-based command line build tool for JS projects. Alongside some predefined tasks, such as validating & minifying your code or running QUnit tests, you can include 150+ plugins or write your own tasks. Luckily, next to their GUI Toolkit, Trigger.io also provides a couple of very easy commands (such as forge build and forge run), which can be easily integrated in a custom Grunt task.

Instead of using this type of build tool, one could also take advantage of Trigger’s hook system, which lets you easily perform tasks at certain points during the build process. However, we are not using hooks because we only use Trigger.io for our mobile apps, but want to keep the same build process for all of our projects.

Validation

Hojoki is doing a lot of processing on the client side. This means we have a huge JS code base. For many reasons, these JS files are organized in modules and get loaded on demand (we use a custom loading mechanism on top of LABjs). The resulting bulk of modules and dependencies adds a lot of complexity, which needs to be carefully validated. When calling grunt without any parameters, we’ll perform two tasks:

  1. dependency checks (ensure nothing is missing)
  2. jslint (ensure everything is valid)

Example of running grunt and getting a dependency issue and a JS error.

Deploy to the web

As soon as we want to build our app for the web, (this also includes the browser version of the mobile app) there are some more tasks to complete on top of the validation. Using many JS files that are loaded in demand is nice for development, but would result in lots of requests for the user if deployed directly. Another related topic is to prevent caching issues for users when updating an existing web app. Our command for this step is called grunt deploy and it performs the following tasks:

  1. copy all relevant files to dist/web/
  2. concat and minify CSS and JS
  3. include md5 hash in minified files
  4. replace placeholders in index.html with new file names
  5. gzip HTML, CSS and JS

Deploy the native apps

Lets get to the part where Trigger.io’s Forge tools come into play. This is basically an improved version of “grunt deploy” and includes the following tasks:

  1. All steps from grunt deploy
  2. copy all files to dist/native/
  3. forge build target
  4. delete unnecessary files to reduce app size
  5. forge run target
  6. forge package target
  7. copy a snapshot of the current code base to our revision control in order to have the legacy version available when making use of Trigger.io Reload

In order to trigger the tasks above, we registered the following commands:

  • Step 1 – 4: grunt build
  • Step 1 – 5: grunt run-target *
  • Step 1 – 6: grunt package-target **
  • Step 1 – 7: grunt release-target **

*  target can stand for ios or android
** target can stand for all, ios or android, step 5 is ignored for grunt package & release

Let me get into more detail on some of the steps mentioned above:

Until recently, we have updated the manifest file (Android) and the Info.plist (iOS) after step 3, to enable custom URL schemes (e.g. hojoki://) for our app. However, custom urls been implemented as a native feature by Trigger.io as of API version 1.4.15, making it very easy for everybody to use this.

Step 4 can come very handy if your app’s resources (especially images) vary heavily between iOS and Android. Once the app has been built, we delete all Android images in the iOS build and vice versa. Here is a snippet right from our grunt code:

log.writeln('\nreducing app size'.yellow);
[ 'dist/native/development/android/assets/src/images/ios',
  'dist/native/development/ios/device-ios.app/assets/src/images/android'
].map(function(folder2delete) {
  log.writeln('  deleting ' + folder2delete.blue);
  rimraf.sync(folder2delete);
});
log.writeln('[   DONE]'.green);

As of API version 1.4.17, we are also using wireless distribution to accelerate iOS testing on multiple devices even more. A possible integration of this new feature into the build tools involves replacing the placeholder URLs and renaming the wireless distribution plist file. Otherwise we’d always need to change the link to the plist file on the distribution page. Of course there are other ways, like detecting it automatically with a server-side language like PHP. Here is our implementation, which runs right after grunt package is finished:

log.writeln('\npatching ios wireless distribution plist'.yellow);
grunt.file.expandFiles([
  'dist/native/release/ios/*-WirelessDistribution.plist'
]).map(function(plistFile) {
  var pflistContent = file.read(plistFile);
  var versionId = plistFile.split('-')[1];
  pflistContent = pflistContent.replace(
    'http://www.example.com/app.ipa',
    'http://www.yourpage.com/custompath/appname-' + versionId + '.ipa'
  );
  pflistContent = pflistContent.replace(
    'http://www.example.com/image.57x57.png',
    'http://www.yourpage.com/custompath/filename-57.png'
  );
  pflistContent = pflistContent.replace(
    'http://www.example.com/image.512x512.jpg',
    'http://www.yourpage.com/custompath/filename-512.png'
  );
  file.write(plistFile, pflistContent);
  fs.renameSync(plistFile, 'dist/native/release/ios/appname.plist');
});
log.writeln('[   DONE]'.green);

Another interesting part of our build process might be step 7. Here we commit a snapshot of the current code base in an extra branch of our revision control, in order to use Trigger.io Reload more easily later on. Imagine you want to fix something in one of your older app store versions, which has many parts of its code already changed by now. How would you be able to change a small portion of the code and push a Reload without having the source code of this exact version? Here is the code:

if (release) {
  log.writeln('\nadding snapshot to svn'.yellow);

  [ 'dist/native/development',
    'dist/native/.template',
  'dist/native/.lib' ].map(function(folder2delete) {
    log.writeln('  deleting ' + folder2delete.blue);
    rimraf.sync(folder2delete);
  }); // we don't want to commit those

  var url = 'https://your-version-control-url/snapshots/' +
    moment().format('YYYY-MM-DD');
  log.writeln('  deleting old');
  grunt.helper(
    'hojoki-exec', // similiar to grunt-exec
    'svn delete ' + url + ' -q -m "deleting old"',
    true, true)
  .always(function() {

  var config = JSON.parse(file.read('dist/native/src/config.json'));
  var msg = '#snapshot of version ' + config.version +
    ' (platform ' + config.platform_version + ')' +
    ((target_android && ' #android') || '') +
    ((target_ios && ' #ios')   || '');

  log.writeln('  committing "' + msg + '"');
  grunt.helper(
    'hojoki-exec',
    'svn import dist/native/ ' + url + ' -q -m "creating ' + msg + '"',
    true, true)
  .done(function() {
    log.writeln('[   DONE]'.green);
  });
});
}

Example of an automated commit (as seen in Beanstalk) done by step 7 when running grunt release-all.

In case you are wondering, hojoki-exec is basically the same as grunt-exec, but with deferreds (done(), fail(), always()). The code snipped above triggered the following output when I released our app version 1.4.2 for both Android and iOS:

What else?

Even though our mobile apps share lots of their code base with our web app, the latter has a much longer history when it comes to a customized build process. Other tools that we have in use, include image conversation and optimization (ImageMagick, pngquant, ImageOptim), image spriting (Glue) and running QUnit tests (predefined grunt task).

Sure enough, there is always more you can do for automation. One thing we’d like to do soon is switching to LESS or Sass and include CSS validation in the build process. Adding support for source maps and playing around with lazy JS instantiation are also interesting topics related to this.

This blog hopefully provided you with some insights to our build process and will help to make building and deploying apps with Trigger.io even easier.

 

Sign-up for Trigger.io now to get started with your mobile app. Questions? Email support@trigger.io anytime.