Vanquishing CMake
I'm not overly fond of CMake, possibly because I have only seen it used for evil.
What form of evil one asks? My observation is that users of CMake will use it as a crutch to write code that cannot be compiled in any less complicated environment, in order to save themselves a trivial quantity of typing.
For my work, I routinely use a program of byzantine complexity which was developed internally for data analysis. Make no mistake, the software itself is of the highest caliber; very efficient and carefully coded C++. Over time, however, I have become dissatisfied with the overall usage cycle that is standard: Write a python script which does setup and then actually sets the compiled C++ code running. This script will usually do standard processing tasks and then output the the data into ROOT format. Write a ROOT pseudo-C++ script to actually analyze the data, draw plots and so forth. I'm not a huge fan of Python and i rabidly hate ROOT, so i would like to eventually cut both of them out of the chain. Removing Python seems like an easy first step, since it's only acting as a driver for what is underneath a bunch of C++ code.
So what's the big obstacle? The CMake build environment. All of the C++ code is written with the assumption that it will only be compiled using the included custom CMake scripts. This is great if all you want to do is check out the code, hit build1, and get on with your Python script. On the other hand, this is hellish if you want to write another piece of C++ code compiled against the software's headers and linked with its binaries.
Firstly, header files are scattered all over, yet referenced as though they are not. The software is composed from a number of projects, each of which is kept within its own directory named for the project. So, each project's header files are in project_name/public/project_name/, yet the code will contain only #include <project_name/header.h> How can it work? GCC's -I flag. By copious use of this flag, the make file can add every (necessary) one of the scattered header directories to the compiler's search path. Then, if you want to be able to compile those headers, you have to do the same. Of course, since the makefiles themselves are auto-generated and incredibly tugh to read, you won't get a lot of help there. On the other hand, the CMake scripts are even worse, since they work via custom functions defined in CMake's ugly little scripting language. Experience shows that one is usually best off just trying to compile, checking the error messages, adding the relevant project headers to the search path, and repeating.
The above step is annoying, but fairly simple and mechanical to carry out. I don't greatly begrudge the people who set up the build environment using -I, as it really is a pretty logical way to make everything else more straightforward. The next trick is much more low down: enter -include. You see, if you make all of the necessary fixes to the include path so that all #includes will actually be valid, the code will still fail utterly to compile. The errors will be rather perplexing ones, as they all refer to very simple of very fundamental things being undefined. For instance, we use boost shared_ptrs everywhere. Yet, a naïve compilation attempt will be swamped in "error: 'shared_ptr' was not declared in this scope". A frantic search of the code will show that in fact none of the files ever #includes any file which #includes shared_ptr.hpp. How can it ever compile at all? It turns out that, somewhere within the bowels of the CMake script, CMake has been instructed in insert a -include flag in the flags for every call to g++. This is used to inject a single, vitally important base header into every single file. Fun, no?
It took me three hours to wade through this and get a simple program (which doesn't actually do anything) to actually compile and link.
-
Or, you know, run CMake for 10 minutes so that you can get the makefiles to run for the next hour. But it is conceptually simple for the user. ↩