Pre-pre-build commands with qmake

With most non-trivial Qt projects that I create, I like to include a pre-pre-build command in the qmake project file. I'll explain why as we go, but first off, let's look at what I mean by "pre-pre-build" (it is not at all a standard term).

The typical build process (as performed by make) looks something like this:

  1. For the given target, check if any of the target's dependencies have been updated since it was last (re)built.
    1. If no dependencies have changed, do nothing - we're done ;)
    2. If one or more dependencies have changed, then:
      1. Build each dependent target.
      2. Build this target.
      3. If appropriate, link this target with its dependencies.

That is, of course, a gross oversimplification... but it will do the purpose of this post.

Now, like most Qt developers, I use qmake to generate the Makefiles that drive the build process. Qmake, being an excellent tool, allows you to customise the generated Makefiles in many, many ways. For example, you can add custom commands to be executed just before or after the link step (1.b.3 above) via the QMAKEPRELINK and QMAKEPOSTLINK variables.

However, qmake does not provide a QMAKEPREBUILD variable for adding pre-build commands, and certainly no QMAKEPREPRE_BUILD variable either. The former, it turns out, is pretty easy to achieve anyway, but the latter (the topic of this post) is a little bit trickier.

So, to explain what I see as a "pre-pre-build" step, let's repeat the steps show above, but with my imaginary pre-build and pre-pre-build steps included:

  1. Execute pre-pre-build commands.
  2. For the given target, check if any of the target's dependencies have been updated since it was last (re)built.
    1. If no dependencies have changed, do nothing - we're done ;)
    2. If one or more dependencies have changed, then:
      1. Build each dependent target.
      2. Execute pre-build commands.
      3. Build this target.
      4. If appropriate, link this target with its dependencies.

So you can see we now have two new steps: 1 and 1.b.2, which are pre-pre-build and pre-build respectively.

As mentioned above, it's pretty easy to create pre-build steps - you can, for example, create a new build target, and add it to the existing target's dependency list... but that's not the same as my pre-pre-build concept. The key difference being this: pre-pre-build commands will always be executed at the start of the make call, whereas pre-build commands will only be executed if the target needs to be rebuilt (ie if at least one of the target's dependencies has been updated).

So, why does that distinction matter? Well, to put it simply - it matters if/when the pre-pre-build commands might result in modification / updates to another target's dependencies! That being the case, clearly they need to be executed before target dependencies are evaluated.

Ok, so let's get to an example, which should make things a little bit clearer: Auto-updating Subversion build numbers. It's not uncommon (just search the Qt-interest mailing list) for people to want to automatically update part of a project's version number based on a revision control system's revision number. Some examples here, and here.

The idea is simple - if a project's version number automatically reflects the Subversion (in my case) revision number, then not only do I have an increasing and unique version number, but I can also easily look back a the repository's commit log to see exactly what changed between releases.

Now many people implement such auto-revision version numbers like this suggestion, which results in a pre-build step, rather than a pre-pre-build step. Which means this:

Of course, the "correct" behavior is mostly up to personal preference... on the one hand, if the target itself has not changed, then why update it's version number? but the on the other hand, if you have multiple qmake projects in a given application (eg libraries, etc) then you may (as I usually do) want them to all build to the same release version number.

And, of course, this is just one example... there are many other reasons that you may want a "pre-pre-build" command.

Ok, so that's it for the intro / justification... so how do we do it?! Well, since it's not a standard qmake feature, it is slightly "hackish", but it works by modifying the qmake project file as follows:

  1. Include a custom qmake target using QMAKEEXTRATARGETS.
  2. Hook that custom target into the very beginning of the build process (this is the tricky / hacky bit).

So, first off, we create a custom qmake target. I'll call the target "svnbuild", since it updates my project's build numbers to match the Subversion revision count, but obviously you can call it something more appropriate for your particular application.

# Create our custom svnbuild target.
win32:svnbuild.commands = ..\tools\build\updateBuildNumber.bat ..\svnbuild.h
else:svnbuild.commands = ../tools/build/updateBuildNumber.sh ../svnbuild.h
QMAKEEXTRATARGETS += svnbuild

Here you'll see I've got external updateBuildNumber.bat/sh scripts that do the Subversion checking etc - the .bat file for win32 builds, and the *.sh script for all other (non-win32) platforms.

Now, the (slightly) tricky bit - hooking our custom "svnbuild" target in as the first step in the generated Makefile. Here goes:

# Hook our svnbuild target in between qmake's Makefile update and the actual project target.
svnbuildhook.depends = svnbuild
CONFIG(debug,debug|release):svnbuildhook.target = Makefile.Debug
CONFIG(release,debug|release):svnbuildhook.target = Makefile.Release
QMAKEEXTRATARGETS += svnbuildhook

So how does that work? Well, to understand, (and here comes the hacky bit) let's look at the Makefiles that qmake generates... assuming that we have not added the above hook yet, the first build target for the generated Makefile.Debug and Makefile.Release files is "all" (no surprises there). Now here's the convenient part - the "all" target actually builds "Makefile.Debug" (or "Makefile.Release", depending on the build mode) before building the actual project target. This is done (presumably) so that the generated "Makefile." files are automatically updated if/when the primary "Makefile" file is updated. But since the "Makefile." targets are always evaluated first (thanks to the default generated "all" rule), we can simply add a custom build hook target (svnbuildhook) as the build rule for the "Makefile." targets... this works because we have not defined any commands for that hook target (ie we don't set "svnbuildhook.commands"), and so make sees an entry like this:

Makefile.Debug: svnbuild

Make interprets this line as simply declaring an additional dependency for "Makefile.Debug" (or "Makefile.Release"), since it does not define any build commands. That is to say, if we skipped the svnbuildhook intermediate target, and instead set our svnbuild target directly as the build rule for "Makefile.Debug", then qmake would generate (and thus make would see) something like the following:

Makefile.Debug:
    ..\tools\build\updateBuildNumber.bat ..\svnbuild.h

Which make would treat as the build rule for "Makefile.Debug" instead of simply being an additional dependency declaration.

Also, note that we did not set any dependencies for the svnbuild target - by leaving svnbuild's dependency list empty, make treats svnbuild as a phony target, which is highly desirably for a pre-pre-build target (it ensures that the associated commands will be executed every time, not just when some dependency has changed... see phony targets for more information).

Okay, well that's it. I know my explanation is a bit convoluted, but as long as you know what qmake is, and you know a bit about the Makefile format, then you should be able to copy and paste the code into your own qmake project files, inspect the resulting Makefiles, and then customise to suit your own desired pre-pre-build commands.

Just to recap, the code to add to qmake project files looks like:

# Create our custom svnbuild target.
win32:svnbuild.commands = ..\tools\build\updateBuildNumber.bat ..\svnbuild.hxx
else:svnbuild.commands = ../tools/build/updateBuildNumber.sh ../svnbuild.hx
QMAKEEXTRATARGETS += svnbuild

# Hook our svnbuild target in between qmake's Makefile update and the actual project target.
svnbuildhook.depends = svnbuild
CONFIG(debug,debug|release):svnbuildhook.target = Makefile.Debug
CONFIG(release,debug|release):svnbuildhook.target = Makefile.Release
QMAKEEXTRATARGETS += svnbuildhook

Good luck!! :)

comments powered by Disqus