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
orgulp 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