Dynamically switching Tiapp properties

Writing variables in Titanium’s tiapp.xml file

Andrea Jonus
Caffeina Developers

--

Artist’s rendition of a technologically enhanced tiapp.xml

Titanium sure has some strange ways of handling app settings.

Why do I have to put API IDs in a XML file? It’s not like they’re constants! I may want to use a different ID if I’m deploying an app for testing. Am I the only one that has a problem with managing multiple branches to maintain a development version of some settings? And what about LTS versions and enterprise apps? What’s the deal with eggplants? They’re not eggs!

The point is: I’m working with projects that need some properties in the tiapp.xml file to change with the type of build I’m deploying. The test versions may have a different Facebook API ID than the production versions. The app ID of a demo might be different from the one used in the other builds. I may be working on a development version that supports more screen orientations. And so on.

I’m tired of handling multiple branches just to keep different versions of a few strings.

There’s GOT to be!

Is there?

Turns out there’s not already a way to do what I need, or at least I don’t like the options I have. I am picky.

Let’s see what we got:

  • TiCh is pretty nice, but it doesn’t support multiple definitions of the same attribute. It allows dynamic substitution through a custom syntax, but I don’t really need that feature. Also, it handles any non top-level attribute using XPath, which would make me want to stab myself in the knee with a rusty nail;
  • dev.tiapp is a possibility, but I don’t like the fact that we’re working with custom tags. I’ll explain the reason later;
  • TiTh looks exactly like what I have in mind, but involves managing a different directory and tiapp.xml file for every version of the app I want, which would be annoying.

Not a large variety. Looks like we’ll have to write our own solution! Our custom script will work directly on the tiapp.xml file, will be as simple to configure as we can manage, and will be a Titanium plugin to integrate nicely with the rest of the build toolchain.

Onwards!

How and when the hell do we write this “tee-app” thing?

When the Titanium CLI executes a command, it obviously needs to read the Tiapp file at some point, at least to check if it is actually running in a Titanium project. We need to overwrite the configuration before this happens.

We’ll take a look at dev.tiapp to get some insights on the way we could accomplish this. From dev.tiapp.js:

exports.init = function (logger, config, cli, appc) {[...]  // prefix replacement
cli.addHook('build.pre.construct', function (build, finished) {
var force_prefix = build.cli.argv.dev?build.cli.argv.dev.tiapp:undefined;
var prefixReg = new RegExp("^"+(force_prefix?force_prefix:'dev')+"\.");

if (build.tiapp && build.tiapp.properties) {
if(force_prefix || build.deployType !== "production"){

[...]

Ok, so now we know that:

  • Using event hooks might be the correct approach to this problem;
  • We have a few interesting objects we could check for useful data (config, cli, and build);
  • build.pre.construct already has the data from tiapp.xml, stored in the build object passed to the listener. We can already discard this event and all the others that come after it.

Now we need a list of all the events we can listen to. Reading from the official documentation:

Certain commands, such as the build, clean or create command, fire additional hook events. For builds, the events vary by platform, and whether you are building a production application or not.

Invoke a CLI command

cli:go (First hook that can be monitored by global plugins)

[command.config] if invoking the build, clean or create command

cli:command-loaded

cli:pre-validate

CLI displays its banner message.

cli:post-validate (First hook that can be monitored by local plugins)

cli:pre-execute

[help:header] if the help menu is invoked

cli:post-execute or other command hooks if invoking the build, clean or create command (see description below)

Android build hooks

The following hooks are fired after the cli:pre-execute hook when building a project for the Android platform:

build.pre.construct

build.pre.compile

[…]

At first glance, our upper limit would becli:pre-execute. However, we’ll start checking from cli:post-validate, since it’s the first hook that can be monitored by local plugins. We’ll write a small script that prints all the data available, and a test app that uses it as a local plugin:

const TAG = 'titanium-test-plugin';
console.log(`Running ${TAG}...`);
exports.cliVersion = '>=3.X';
exports.version = '1.0.0';
exports.init = function (logger, config, cli, appc) {
console.log('INIT CLI:', cli);
console.log('INIT CONFIG:', config);
cli.on("cli:post-validate", (build, finished) => {
console.log('BUILD:', build);
console.log('CLI:', cli);
console.log('CONFIG:', config);
finished();
});
};

When we run titanium build, we get this:

INIT CLI: CLI {
config:
[...] tiapp:
tiapp {
id: 'com.caffeinalab.tiapp.test',
name: 'Tiapp Test',
version: '1.0',
publisher: 'not specified',
url: 'caffeina.com',
description: '',
copyright: 'not specified',
icon: 'appicon.png',
fullscreen: false,
'navbar-hidden': false,
analytics: true,
guid: '36730cee-101d-41ca-be5c-9779b427766f',
properties:
{ 'ti.ui.defaultunit': [Object],
'run-on-main-thread': [Object] },
ios:
{ 'enable-launch-screen-storyboard': true,
'use-app-thinning': true,
plist: [Object] },
android: {},
mobileweb: { precache: {}, splash: [Object], theme: 'default' },
[...]CLI: CLI {
config:
[...] tiapp:
tiapp {
id: 'com.caffeinalab.tiapp.test',
name: 'Tiapp Test',
version: '1.0',
publisher: 'not specified',
url: 'caffeina.com',
description: '',
copyright: 'not specified',
icon: 'appicon.png',
fullscreen: false,
'navbar-hidden': false,
analytics: true,
guid: '36730cee-101d-41ca-be5c-9779b427766f',
properties:
{ 'ti.ui.defaultunit': [Object],
'run-on-main-thread': [Object] },
ios:
{ 'enable-launch-screen-storyboard': true,
'use-app-thinning': true,
plist: [Object] },
android: {},
mobileweb: { precache: {}, splash: [Object], theme: 'default' },
[...]

The first event listened by a local plugin already carries the parsed Tiapp data!

💩

We have to move into Global Plugins land. This is not a problem per se, it’s just that I’d have preferred to write something I could copy or clone in a project as a dependency. Git submodules are a pain to manage, but they have their uses.

To avoid wasting time, we’ll check the other events starting from cli:go and going down from there. I’ll skip all the boring output and go straight to the point: the last event we have at our disposal before the Tiapp is parsed is build.config.

Now we know what to do. Let’s go ahead and write the real stuff.

Injecting variables in the Tiapp

How do we put variables in a XML file? Here’s a list of ideas:

  • We do as dev.tiapp does: custom tags with different prefixes for each “environment” we want to support;
  • We use XSLT and transform the XML before Titanium parses it;
  • We just don’t.

Can you guess what is my favorite choice? That’s right!

We just don’t!

Using custom tags would allow a project to compile (probably) correctly even without our global plugin, but it would give the developer no insight on the fact that the plugin is a dependency.

I absolutely don’t want to use XSLT, or to parse/transform XML at all, for that matter. XPath is a pain to use, especially with structures like the one of our Tiapp, and RegExes are The Worst Idea™.

What we’re gonna do is: we’ll move all the content of tiapp.xml in another file, and we’ll add the variables we need in a syntax of our choice; we’ll write a JSON file with the definitions of those variables for the different environments we want to support; then, we’ll delete tiapp.xml and put it on gitignore.

The reasoning behind this unholy mitosis of the Tiapp file is that we want the build to fail immediately if the developer forgets to install our plugin. Plus, instead of parsing a tree and changing nested attributes or entries in arrays, we can simply replace strings inside a raw text and print the result in the new tiapp.xml file.

The core function

Enough explanations! Here’s some code:

function compose(env, tplfile, outfile) {
const fs = require('fs');
const { app } = env;

return new Promise((resolve, reject) => {
fs.readFile(tplfile, (err, tpl) => {
if (err) {
reject(err);
return;
}

const composed = eval('`' + tpl + '`');

fs.writeFile(outfile, composed, (err) => {
if (err) {
reject(err);
return;
}

resolve();
})
});
});
}

What’s happening here?

It’s a bit tricky to understand, but we’re using Template Literals to merge the content of an object (app) with a string we’re reading from a template file (tplfile). It’s a bit like melding mysterious alien technology and human flesh to get an unstoppable killing machine!

Sorry, i’ve been playing XCOM

I like template literals. They’re simple and they get the job done. We don’t need no custom parser, since we can just use a standard feature of ES6.

Let’s write the rest:

function compose(env, tplfile, outfile) {
const fs = require('fs');
const { app } = env;

return new Promise((resolve, reject) => {
fs.readFile(tplfile, (err, tpl) => {
if (err) {
reject(err);
return;
}

const composed = eval('`' + tpl + '`');

fs.writeFile(outfile, composed, (err) => {
if (err) {
reject(err);
return;
}

resolve();
})
});
});
}

function checkAndCompose(cli, logger, finished) {
let { tiappenv } = cli.globalContext.argv;

if (tiappenv == null) {
logger.warn(`${TAG}: --tiappenv flag not set, defaulting to "development"`);
tiappenv = "development";
}

let tiappCfg = null;

try {
tiappCfg = require(projectDir + '/tiapp-cfg');
} catch(err) {
logger.warn(`${TAG}: Couldn't find a tiapp-cfg.json file:`, err);
logger.warn(`${TAG}: Skipping tiapp.xml composing.`);
finished();
return;
}

if (tiappCfg[tiappenv] == null) {
logger.warn(`${TAG}: Couldn't find the environment "${tiappenv}" in the tiapp-cfg.json file.`);
logger.warn(`${TAG} Skipping tiapp.xml composing.`);
finished();
return;
}

compose(tiappCfg[tiappenv], TIAPP_TEMPLATE, OUTFILE)
.then(() => {
logger.info(`${TAG}: Successfully wrote tiapp.xml`);
})
.catch((err) => {
logger.warn(`${TAG}: Couldn't write the new tiapp.xml file:`, err);
logger.warn(`${TAG}: Skipping tiapp.xml composing.`);
})
.then(() => {
finished();
});
}

exports.init = function (logger, config, cli, appc) {
cli.on("build.config", (build, finished) => checkAndCompose(cli, logger, finished));

};

Did I already tell you I like template literals?

We’re listening to the build.config event, checking for the flag --tiappenv, that tells the plugin which “environment” to read from the configuration file tiapp-cfg.json. The plugin will default on the name “development” if it doesn’t find the flag, and skip the writing phase entirely if it doesn’t find a configuration file, or the environment required by the developer.

Here’s an example of a template file ( tiapp.tpl):

And here’s the corresponding configuration file (tiapp-cfg.json):

Since we’re just interpolating strings, you can replace entire sections of the Tiapp with variables. Hell, you could even put the whole tiapp.xml in the config as an attribute. I'm not judging you.

As you can see, it’s pretty easy to understand. The only quirk of this code is that every variable has to be wrapped inside the attribute named app. This gives us space to add more options for the plugin in the future.

Full code

You can see the full code of this plugin here:

I added a check to warn the developer he should put tiapp.xml in his gitignore, since it will be overwritten with every build he launches, thus dirtying the repo. I’m also listening to the clean.config event, because one of my deploy tools needs the Tiapp before launching the build, and I’m a self-centered prick that steals jokes from other people 🙂.

If you liked this little project, you can show your appreciation by clapping your hands really hard (like, 50 times at least), starring caffeinalab on GitHub and sending love letters.

Be sure to check out our work, and Trimethyl, the toolchain that makes Titanium development bearable.

Cover image by: Tim Gibson

--

--

github.com/jei | Italian software engineer. Kendoka, motorcycle enthusiast, metalhead. If a project looks silly and involves beer, I'll probably work on it.