To make, or not to make - QMake and beyond

QMake is one of those crucial tools which just has to work, or it totally ruins your day; that is, if your project is using qmake, of course.

Before QMake came around, there was TMake. TMake was a perl script which did a good job for what it was designed for at the time. The inner workings of QMake was based upon TMake, and a gazillion features and hacks later, QMake has ended up as a very-hard-to-maintain-without-breaking-anything-esoteric beast. The question is, what are we going to do about it?

There have been plenty of internal discussions going on for years, if we should scrap QMake and make a new, go for one of the existing build systems, or to spend extra resources trying to fix up qmake (and not break compatibility). A few projects began, but never finished. One project (QBuild) was even released with Qtopia, but not completed and further developed.
Given the non-uniform set of developers at Qt, the discussions have be fierce, and opinions as plentiful as the number of developers. There's really no tool out there which satisfies our complete wish list, and we have looked into many of them. Either the language is broken, it's lacking features, it's too hard to mold to do trivial things, the language is too verbose for trivial projects (XML for example), the language is not limiting enough (Python with all its libs, for exampe), too slow, doesn't parallelize well, doesn't see all dependencies or takes forever to process them, it's trigger (fork) happy, and the list goes on and on.

So, in order to make the decision even harder, we seek your opinion on the matter. :-)

I'll present some opinions for a potential new build system, and how to potentially tackle them.

1) "Proprietary" language
As we have already experienced with QMake, maintaining an own language is sub-optimal. You actively have to work on both developing the language to support new features, and on the internals/usage of the language. You might end up 'designing yourself' into bad constructs which hinders new development, and doesn't make as much sense as perhaps other possible language constructs. It can make the language hard to learn for new users. One example of this in QMake can be seen in the file

<code>    QTDIR/mkspecs/features/exclusive_builds.prf</code>

Another problem with a proprietary language is that it creates an additional hurdle for new users to overcome before being able to use the system, as they cannot relate any previous language skills.

It would therefor be great if the language of this new build system was a language which most people can relate to, for example JavaScript, which is getting more and more widespread (http://www.langpop.com/). By using a language which is already defined, we could focus entirely on how to use the language most efficiently and cleanly, rather than also creating a language.
We should probably also steer away from 3rd party scripting languages, as they usually come with a plethora of utility libraries, making it very hard to enforce a structure of the build system, allowing for too much customization. People going that route should probably rather use a custom script for their builds (and any build system could run that, if need be).

2) IDE integration
The build system needs to integrate well into IDEs.
We have created our own IDE, and that IDE needs to interact properly with the build process of Qt, and Qt-based projects. This means that the IDE must be able to show all the files for all different platforms at the same time, and also know which ones are built on the host and target (mixed cross-platform build) platforms, to ensure that Intellisense/code completion works as expected. It also must be able to alter the build process, like adding/modifying/removing compiler/linker options and files, which is then reflected in the build system project files.

This is not as easy as simply creating a separate file which specifies the sources needed for a build, since we need to list platform specific files, and also change settings depending on platform.

The problem then is of course conditions in the language, as these are evaluated on parse time, and thus normally not known by the IDE which needs to parse the project language.

One possible solution we've found to this issue would be to ensure that the settings are visible after the parsing, and easy to evaluate. In JavaScript we could define targets like this:

    var corelib = new Object()
corelib.target = "QtCore"
corelib.defines = ["QT_BUILD_CORE_LIB",
"QT_NO_USING_NAMESPACE"]
corelib.defines["mac && bundle"]= ["QT_NO_DEBUG_PLUGIN_CHECK"];
corelib.sources = ["Foo.cpp", "Bar.cpp"]
corelib.sources["windows"] = ["Foo_win.cpp", "Bar_win.cpp"]
corelib.sources["unix && !mac"] = ["Foo_x11.cpp", "Bar_x11.cpp"]

or, even more optimal for an IDE, as JSON:

    var corelib = {
"target" : "QtCore",
"defines" : { "all" : ["QT_BUILD_CORE_LIB",
"QT_NO_USING_NAMESPACE"],
"mac && bundle" : ["QT_NO_DEBUG_PLUGIN_CHECK"] },
"sources" : { "all" : ["Foo.cpp", "Bar.cpp"],
"windows" : ["Foo_win.cpp", "Bar_win.cpp"],
"unix && !mac" : ["Foo_x11.cpp", "Bar_x11.cpp"] }
}

(Note that the "all" entry in the array would equal the normal array in the non-JSON code)
So, the IDE would be able to see all the possible sources, and know which configuration they belong to. Then the backends would evaluate the final sources based on the current configuration, when adding the target "corelib" to the DAG (Directed Acyclic Graph), or project dependency tree, if you will.

3) Build directly from tool
Since the tool itself has the whole DAG tree, it should be able to build all the targets directly, without going through another build system. CMake, for example, relies on the Makefile generator, and creates Makefiles which in turn calls back into CMake to do the build. This solution is not only fork-heavy, but also limiting, since you can only parallelize as well as the backend generator allows you to. So, unless the output Makefile is one single huge Makefile, you'll run into limitations. Scons will build directly, but is too slow at parsing dependencies, so each build you do takes forever to start. Waf is better in this respect, but both lack a proper set of backends for project generations (Vcproj, XCode, Makefiles etc). They are also based on Python, which adds a dependency of a 3rd party library, which we want to avoid due to both the multitude of platforms Qt supports, and because of all the utility libraries.

4) Integration towards distributed build systems
We internally use several distributed build system, depending on which platforms we're on (Teambuilder on Linux, Distributed Network Builds on Mac and IncrediBuild on Windows), and they all have the same limitation when we use Makefiles: They fork off more processes than what is necessary/useful on the system.

Say you have a build farm with 20 open slots. You fire off 'make -j20' in good faith to fill the farm. However, at the same time someone else does the same, and you both get only 10 slots each. You'll now have 10 'sleeping' processes, all which are just waiting for their slot in the farm, when those resources would be better spent elsewhere.

The building backend should be able to interface with the various tools to see how much resources is available at any given time. That way it scale up or down the number of parallel jobs to the optimum. Also, the distributed build system probably knows better, as it might NOT be optimal to run 1 local compile and 20 remote ones, while your machine is linking on the other core; maybe 0 local and 10 remote is better for *your* machine, even if there are 20 open slots in the farm?

The build tool shouldn't have to know this, and the developer shouldn't have to think about how parallel he wants to be. This is why such an interface is important, so that it's possible for the distributed systems to add these algorithms to the build tool.

5) Cross-compiling
In large mixed cross-compiled (were you compile projects for both host and target systems at the same time) projects, most of the sub-projects normally compile for the target system, while only a few (supporting) projects are intended for the host. This means that projects should easily and automatically pick up the target platform, while a bit more attention should be required for host platform projects only.

The project shouldn't have to worry too much about which platform, nor how to manipulate the project file to achieve the result. Ideally they would only have to mark that a project is intended for the host platform, like for example:

    var moc = {
"target" : "moc",
"platform": "Host",
"defines" : { "all" : ["QT_MOC"]},
...
}

As the configure process has already figured out which parameters are needed for the host and target platform, the build system would know which defaults to apply to each project type in the cross-compiling setup; so the project developer need not do anything else to the project, unless he/she needs to override the cross-compiling defaults.

6) pkg-config support
The build system needs to both be able to use pkg-config information, and to output the pkg-config files for a built project. Individual projects should not need to maintain a separate .pc file with replacement variables.

7) Deployment
The build system should be able to create deployment files for the various popular platforms, like .msi, .deb, .rpm, .dmg. Of course it could do this by creating scripts which are run with common platform specific tools to create these deployment files.

8 ) Configuration
The tool should handle the configuration of projects. That way we only need to maintain one set of configuration rules on all platforms, and extending with new options would be uniform. Ideally the system should also automatically generate the -h/--help documentation, based on the configuration script, so we don't need to maintain a separate doc file together with the configuration script.
This is the same reason why we keep as much documentation together with the Qt source code, to make the maintenance of the documentation as easy and coupled as possible.

So, these are some of the things we've been thinking about, however, there's a lot more to a build system than just these 8 items. Also, it's worth mentioning that nothing is set in stone yet. We're at the thinking stage. No matter what we decide, QMake will still around for a long time.

So, what's your opinion?


Blog Topics:

Comments