One version number to rule them all

We have been developing a Qt mobile application on behalf of a customer for a few months now. The app is in both the Apple App Store and Google Play Store and we have so far been fairly pleased with getting back to our roots and developing Qt applications again. That's another blog post for another day though.

One of the things that annoyed us about our release process was every time we wanted to bump the version number of the application, we had to modify a handful of files and update the value. I personally dislike things like that because it is error prone. Marius said he had tried to find a workaround,  but it proved to be fairly challenging. I personally don't develop much Qt these days, but I kinda enjoy wrestling with qmake occasionally so I said CHALLENGE ACCEPTED!

The goal was to bump the version in a single location and have properly versioned packages for Android and iOS. First of all, let's define the one true version number in our .pro file like so:

VER_MAJ = 1
VER_MIN = 0
VER_PAT = 0
VERSION = $$sprintf("%1.%2.%3",$$VER_MAJ,$$VER_MIN,$$VER_PAT)

This should be pretty straightforward. We typically use the MAJOR.MINOR.PATCH pattern for our versions and the above variables are already supported by qmake. First let's take a look at how we can make use of this on iOS. The default mkspec for iOS uses XCode projects under the hood for building. It just so happens that when qmake generates the XCode project file, it writes the above qmake variables to the DYLIB_CURRENT_VERSION XCode variable. This version is for the actual binary so it doesn't directly affect the package. The metadata about the package is stored in the Info.plist file which mobile apps typically override in order to set the bundle identifier or change the version. One of the nice feature of the Info.plist file is that you can reference XCode project variables, so setting a version number for our package is as simple as modifying the Info.plist as follows:

<key>CFBundleShortVersionString</key>
<string>${DYLIB_CURRENT_VERSION}</string>
<key>CFBundleVersion</key>
<string>${DYLIB_CURRENT_VERSION}</string>

The documentation states that both values should be 3 integer values separated by a "." and the DYLIB_CURRENT_VERSION variable is exactly this ("1.0.0" in this example). Apple's intention seems to be that the above two values should be different because CFBundleVersion should represent an iteration (build number) as opposed to a release and this is necessary if you want to upload intermediate iterations to some distribution platform like HockeyApp or TestFlight. For us, we typically only upload to these services when we have bumped the version so if you follow this practice, then the two values can be the same otherwise you will need to do something fancier.

The Android version is more complicated and some will argue a nasty hack, but I'm ok with that. For this project we have switched to using the Gradle based build system that was released with Qt 5.4.0. Gradle is extremely powerful and let's us do some pretty expressive things (ie: hacks) in the build file. The processing of the build.gradle and therefore the compilation of Java code is not driven by the make build step. Instead, the make step compiles the C++ and generates a JSON file of build settings that is then picked up by the androiddeployqt tool. Unfortunately this approach makes it difficult to propagate any data from our .pro file into our Gradle build. There may well be a more elegant solution to this, but the approach that I used was to generate an additional metadata file from inside the .pro file and this can then be picked up by Gradle. I started writing a single variable to a file, but after discovering some of the newer replacement functions in qmake I set my sights a little higher. Behold the ugliness:

android {
    QT_varfile = ""
    for(var, $$list($$find($$list($$enumerate_vars()), ^(?!QMAKE_.*|QT\..*|QT_.*|\.QMAKE.*).*$))) {
        line = $$var "$$eval($$var)"
        QT_varfile += $$join(line, "=")
    }
    write_file($$absolute_path("VARIABLES.txt", $$OUT_PWD), QT_varfile)
}

If you're still reading, you can copy the above code block into your .pro file (probably at the bottom to be safe). The above snippet iterates through all variables inside qmake during processing of the given .pro file (enumerate_vars). That list of variables is very long and contains tonnes of internal qmake stuff so I used a negation regexp to filter out most it by ignoring these patterns:

  • QMAKE_*
  • QT.*
  • QT_*
  • .QMAKE*

I'm left with a reasonable list of variable names. I named my variable QT_varfile to avoid it appearing in the list. The next step is to iterate through these variables and print them along with their value to the file VARIABLES.txt. Of course there are still many uninteresting values in there, but the relevant ones for this example look like this:

VERSION=1.0.0
VER_MIN=0
VER_MAJ=1
VER_PAT=0

The final step is to read and parse this file in our build.gradle file. Here's a quick little snippet of Groovy code (no seriously that's what the language is called):

def readVarFile() {
    File varFile = file('../VARIABLES.txt')
    def qtVars = [:]
    varFile.eachLine { line ->
        if (line.trim()) {
          def eq = line.indexOf('=')
          if (eq > 0) {
              def key = line.substring(0, eq)
              def value = line.substring(eq+1)
              qtVars."$key" = value
          }
        }
    }
    return qtVars
}

def QtApp = readVarFile()

The above code reads the file and basically splits each line on "=" and stores the key and value in a map which is then accessible to other places as QtApp. The two variables we want to set for versioning are versionCode and versionName. The first is an integer value which should always increase when the version increases. The second is the string that gets displayed in HockeyApp and the Google Play Store. There are many solutions to generating the version code, but I typically use this one for my Android apps:

def versionMajor = QtApp.VER_MAJ.toInteger()
def versionMinor = QtApp.VER_MIN.toInteger()
def versionPatch = QtApp.VER_PAT.toInteger()

android {
    ...
    defaultConfig {
        ...
        versionCode versionMajor * 100000 + versionMinor * 1000 + versionPatch * 100
        versionName QtApp.VERSION
    }
}

The multiplier on versionPatch is just in case you wanted to add build numbers as well. Obviously all of these values can be tweaked to suit your needs. The versionCode and versionName variables are also declared in the AndroidManifest.xml, but as far as I can tell, Gradle overrides those values so I don't see the need to update the manifest file.

That's it! If anyone has suggestions to make this more elegant, I'll happily add an update.