My app, auroras.live, has been out in the app stores for about two months now. There’s two versions available, a free and a paid version. Previously I was maintaining three GitHub branches — Master, Free and Paid. I’d make the changes in master, then make a PR to sync free and paid, then edit the config.xml in the respective repos so the app would detect and use the appropriate version.

After a while, this got tedious because I’d have to ensure all three branches were in sync, except for the config.xml file (which got reformatted each time a plugin was added), so I gave up on the idea. Gulp seemed like a great fit for all of this, so I whipped up a quick gulpfile that does a few things for me:

  • Sets the app name (e.g. Auroras.live Free or Auroras.live)
  • Sets the app ID (e.g. live.auroras.app.free or live.auroras.app)
  • Copies the correct icon file, then runs ionic resources to generate the proper icons
  • Builds the production version of the app
  • Signs the JAR, then runs zipalign.

All I need to do is call gulp build-android-free or gulp build-android-paid and it’s all done. No more manually editing config files, no more copying files around. It’s easy! Want this for your own app? The code is below:

var gulp = require('gulp');
var gutil = require('gulp-util');
var bower = require('bower');
var concat = require('gulp-concat');
var sass = require('gulp-sass');
var minifyCss = require('gulp-minify-css');
var rename = require('gulp-rename');
var sh = require('shelljs');
var cp = require("child_process")
var fs = require("fs")
var xml = require("xmldoc")
var defaultJarSignerLocation = "C:\\Program Files\\Java\\jdk1.8.0_73\\bin\\jarsigner"
var defaultZipAlignLocation = "C:\\Program Files (x86)\\Android\\android-sdk\\build-tools\\23.0.2\\zipalign.exe"
var defaultKeystoreLocation = "C:\\Location\\To\\Certificate\\Keystore\\com.example.app.keystore"
var defaultKeystoreAlias = "app_example_com"
var paths = {
sass: ['./scss/**/*.scss']
};
gulp.task('default', ['sass']);
gulp.task('sass', function(done) {
gulp.src('./scss/ionic.app.scss')
.pipe(sass())
.on('error', sass.logError)
.pipe(gulp.dest('./www/css/'))
.pipe(minifyCss({
keepSpecialComments: 0
}))
.pipe(rename({ extname: '.min.css' }))
.pipe(gulp.dest('./www/css/'))
.on('end', done);
});
gulp.task('watch', function() {
gulp.watch(paths.sass, ['sass']);
});
// =============================================================================
// Sets the package (widget) ID
gulp.task('set-package-id', function() {
if(typeof gutil.env.packageid === "undefined") {
gutil.log(gutil.colors.red("--packageid is not set. Aborting."))
process.exit(1)
}
configXML = new xml.XmlDocument(fs.readFileSync("config.xml"))
configXML.attr.id = gutil.env.packageid
fs.writeFileSync("config.xml", configXML.toString())
})
// Sets the package name
gulp.task('set-package-name', function() {
if(typeof gutil.env.packagename === "undefined") {
gutil.log(gutil.colors.red("--packagename is not set. Aborting."))
process.exit(1)
}
configXML = new xml.XmlDocument(fs.readFileSync("config.xml"))
configXML.childNamed("name").val = gutil.env.packagename
fs.writeFileSync("config.xml", configXML.toString())
})
// Sets the package (widget) version
gulp.task('set-package-version', function() {
if(typeof gutil.env.packageversion === "undefined") {
gutil.log(gutil.colors.red("--packageversion is not set. Aborting."))
process.exit(1)
}
configXML = new xml.XmlDocument(fs.readFileSync("config.xml"))
configXML.attr.version = gutil.env.packageversion
fs.writeFileSync("config.xml", configXML.toString())
})
// Copies "icon_free.png" to "icon.png". Running ionic resources will generate icons with that icon
gulp.task('set-free-icon', function() {
sh.rm("./resources/icon.png")
sh.cp("./resources/icon_free.png", "./resources/icon.png")
gutil.log(cp.execSync("ionic resources").toString('ascii'))
})
// Copies "icon_paid.png" to "icon.png". Running ionic resources will generate icons with that icon
gulp.task('set-paid-icon', function() {
sh.rm("./resources/icon.png")
sh.cp("./resources/icon_paid.png", "./resources/icon.png")
gutil.log(cp.execSync("ionic resources").toString('ascii'))
})
// Signs the APK
gulp.task("jarsign", function() {
if(typeof gutil.env.storepass === "undefined") {
gutil.log(gutil.colors.red("--storepass is not set. Aborting."))
process.exit(1)
}
if(typeof gutil.env.jarsigner === "undefined") {
gutil.env.jarsigner = defaultJarSignerLocation
}
if(typeof gutil.env.keystore === "undefined") {
gutil.env.keystore = defaultKeystoreLocation
}
if(typeof gutil.env.keystorealias === "undefined") {
gutil.env.keystorealias = defaultKeystoreAlias
}
gutil.log(gutil.colors.green("Signing JAR"))
gutil.log(cp.execSync("\"" + gutil.env.jarsigner + "\" -storepass " + gutil.env.storepass + " -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore \"" + gutil.env.keystore + "\" \".\\platforms\\android\\build\\outputs\\apk\\android-release-unsigned.apk\" " + gutil.env.keystorealias).toString('ascii'))
})
// Aligns the zip
gulp.task("zipalign", function() {
if(typeof gutil.env.packageid === "undefined") {
gutil.log(gutil.colors.red("--packageid is not set. Aborting."))
process.exit(1)
}
if(typeof gutil.env.zipalign === "undefined") {
gutil.env.zipalign = defaultZipAlignLocation
}
gutil.log(gutil.colors.green("Aligning zip"))
gutil.log(cp.execSync("\"" + gutil.env.zipalign + "\" -f -v 4 \".\\platforms\\android\\build\\outputs\\apk\\android-release-unsigned.apk\" \".\\platforms\\android\\build\\outputs\\apk\\" + gutil.env.packageid + ".apk\"").toString('ascii'))
})
gulp.task('set-free-vars', ['set-free-icon'], function() {
gutil.env.packageid = "com.example.app.free"
gutil.env.packagename = "Example.com App Free"
})
gulp.task('set-paid-vars', ['set-paid-icon'], function() {
gutil.env.packageid = "com.example.app"
gutil.env.packagename = "Example.com App"
})
gulp.task("build-android", function() {
gutil.log(cp.execSync("cordova build android --release").toString('ascii'))
})
gulp.task('build-android-free', ['set-free-vars', 'build-android', 'jarsign', 'zipalign'], function() {
})
gulp.task('build-android-paid', ['set-paid-vars', 'build-android', 'jarsign', 'zipalign'], function() {
})
// =============================================================================
gulp.task('install', ['git-check'], function() {
return bower.commands.install()
.on('log', function(data) {
gutil.log('bower', gutil.colors.cyan(data.id), data.message);
});
});
gulp.task('git-check', function(done) {
if (!sh.which('git')) {
console.log(
' ' + gutil.colors.red('Git is not installed.'),
'\n Git, the version control system, is required to download Ionic.',
'\n Download git here:', gutil.colors.cyan('http://git-scm.com/downloads') + '.',
'\n Once git is installed, run \'' + gutil.colors.cyan('gulp install') + '\' again.'
);
process.exit(1);
}
done();
});

All you need to do is:

  • Run npm install --save xmldoc in addition to the other dependencies for Ionic’s default gulpfile
  • Edit gulpfile.js and replace the defaults at the top of the file with your own.
  • Go into your resources folder and make two icons: icon_free.png and icon_paid.png.
  • Call either gulp build-android-free --storepass mykeystorepassword or gulp build-android-paid --storepass mykeystorepassword
  • You can also call this script with a few parameters:
    • --packageid – Sets the package ID
    • --packagename – Sets the package name
    • --jarsigner – Path to jarsigner
    • --zipalign – Path to zipalign
    • --keystore – Path to your keystore file
    • --keystorealias – The alias of your keystore