Some examples of CMake usage
Introduction
CMake is a cross-platform build system. It is comparable to Make in that the software build process is entirely controlled by configuration files, called CMakeLists.txt in the case of CMake. But CMake doesn’t directly produce the final software; it handles the generation of standard build files: makefiles on Unix, and Visual Studio project files on Windows. This allows developers to use their preferred development environment as usual. This use of common development tools is what distinguishes CMake from other build systems like SCons or Autotools.
The name “CMake” is an abbreviation for “cross platform make”. Despite the use of “make” in its name, CMake is a separate and higher-level application than the make tool.
Preamble
As a summary of the previous article, I propose to build a small base project that will serve as a support for the rest of the article. I’m using CMake version 2.4 here.
Let’s consider an ultra-classic and not at all original project that we will call Hello. Wait! That sounds like “Hello World”! I could have done a “Goodbye Everybody!”, but I was afraid you might leave before the end ;-). Let’s get serious: all the files for this project will be stored in the hello directory. This directory contains the files AUTHORS, COPYING, INSTALL, README, ChangeLog, and the src subdirectory, which contains the source files.
|
|
We start with a very simple code that serves only as a support:
|
|
The integration of CMake is done by adding 2 CMakeLists.txt files: one in the main directory and one in the src directory.
The hello/CMakeLists.txt file is the first to be read by CMake during execution. It is in this file that we do the tests necessary to ensure the availability of the project dependencies. We will also place the code reading and using the compilation options and generating a package for our project.
|
|
For now, we just have a loop that tests for the presence of header files essential for compiling the project on the system. If they are not present, we interrupt the process. Then, we call the CMakeLists.txt file from the src subdirectory.
The hello/src/CMakeLists.txt file contains the definition of the project’s compilation targets and their configuration.
|
|
Let’s create a build directory for our project and a directory for its installation (this will allow us to check the installed files):
|
|
Let’s build and install our project:
|
|
The project is compiled and installed in the desired directory, the necessary directory structure is created. You can execute it.
Throughout the article, we will amend the various files of the project and add others. The line numbers specified in the code snippets below indicate where to insert the new code in the file concerned. You will find the Hello-0.0.0.tar.bz2 package containing the entirety of this example in its final state on the site https://www.arvernux.fr in the Downloads section.
Managing a configuration file
To manage the project configuration, for example to add optional capabilities, we will use a config.h header file generated by CMake from an input file config.h.cmake. So we need to include it in the project sources:
|
|
And add the location of config.h to the compilation directives. This file will be saved in the project’s build directory.
|
|
This technique can be used to handle two types of options: enable/disable type options and with_stuff=foo type options. We will see the first kind in the section on internationalization. Here, we focus on the second kind… We will use it to repeat the message that is displayed.
|
|
The value of the HELLO_COUNT definition will be imported from config.h. Let’s create the template file for config.h, that is, the config.h.cmake file in the src subdirectory.
|
|
When generating the config.h, CMake will replace the string @WITH_HELLO_COUNT@ with its value, which is configured by the CMakeLists.txt file in the root folder of the project:
|
|
The test in lines 7 to 9 serves to ensure an initial value if the user does not define it. Additionally, we use this definition in line 8 to provide a description of the option to the user. The options can be viewed as follows:
|
|
Let’s go back to the parameters of the variable definition in line 8. First, the name of the variable, then its value (the one we want it to have by default). The CACHE parameter is mandatory, otherwise the option doesn’t appear. STRING corresponds to the option type, which is used to choose the right “widget” for editing in ccmake (the ncurses and interactive version of CMake). Finally, the documentation string.
And how can the user define it? By adding this option -DWITH_HELLO_COUNT=2 to the CMake call (or by using ccmake).
|
|
Centralized management of a version number
We can very well use the previous technique to manage a version number in a centralized way. Let’s create a version.cmake file in the hello directory with the following content:
|
|
There! So we will only modify the version number in this file, and we will use CMake to distribute this number wherever we want it; example:
|
|
That’s for the cosmetics… Yes, it’s needed! But we can also use it in the program itself:
|
|
Thus, after configuration, we will have the version numbers defined in our config.h file and we will be able to use them in the source code.
This version number management will also be used later in the packages section, and we can also use it in documentation generation.
Internationalization
You want to internationalize your code. Prepare your main.cpp file:
|
|
The definitions HELLO_NLS_ENABLED, HELLO_NLS_LOCALEDIR, and HELLO_NLS_PACKAGE depend on the content of the config.h file, and their values will be configured by CMake. Don’t forget to mark the string to translate in line 17.
Adapt your config.h.cmake by adding the following lines:
|
|
With the implementation ready in your program, you need to generate the po/hello.pot file using xgettext.
|
|
I refer you to the manual pages and various how-tos for implementing internationalization and using the tools that realize it: this is not the subject of our study.
We create the translation for French (for example): po/fr.po.
How to configure CMake to compile the MO files and install them?
In the hello/CMakeLists.txt file, we start by making the activation optional:
|
|
The option is enabled by default, but the user can disable it by passing -DENABLE_NLS=OFF to CMake when calling it. The string that describes the option will be displayed if the user runs CMake with the -LH options:
|
|
Then, first, we perform detection of the necessary files and utilities:
|
|
If the option is enabled, we verify that the locale.h and libintl.h headers are present on the system. If they are not there, we display a warning message and disable internationalization support. Here, I made the choice to build the project anyway… If you think it is necessary to stop the build and require the headers, simply change the STATUS directive on line 33 to FATAL_ERROR. The next section looks for the msgfmt utility that is used to compile PO files into MO. I make the same remark as before concerning the possibility of stopping the process rather than disabling the option in case this utility is absent.
Note: In CMake version 2.5, a break command will appear allowing you to exit a foreach loop. This will greatly simplify the implementation of the previous loop.
In a second step, we configure the necessary variables:
|
|
In line 51, we include the CMakeLists.txt file from the po subdirectory. It is in this file that we will add the target allowing the compilation of translated messages:
|
|
Let’s detail the code a bit:
First, we create a new target i18n linked to the all target. Then, we get the list of all .po files to compile in the Hello_PO_FILES variable. This is followed by a loop that is repeated for each of these files (Hello_PO_INPUT). In this loop, we start by extracting the filename without the extension (Hello_PO_INPUT_BASE), i.e., the language for which the file provides the translation. Then, we generate the name and complete path of the .mo file where the translation will be compiled (Hello_MO_OUTPUT). Next, we add to the i18n target the command allowing this compilation (call to msgfmt). Finally, we add the command that will install the obtained file in the right directory structure and with the right name during make install.
Let’s see if we’ve done a good job:
|
|
“Oh! It works!”
Generating packages
Your project is beautiful! Very beautiful! You’d like to distribute it. So you need to generate packages… No problem! CPack, the “package management” component of CMake, is here to help us.
Let’s generate a package containing the binaries and a package containing the sources of our project. To do this, simply place the following code at the end of the CMakeLists.txt file in the root directory of our project.
|
|
The CPACK_GENERATOR and CPACK_SOURCE_GENERATOR variables define the formats of the generated packages. Here, the binary package will be a tar archive compressed by gzip and the source package will be a tar archive compressed by bzip2. The version number definitions are used to configure the package name. Regarding the source package, the CPACK_SOURCE_IGNORE_FILES variable allows specifying files and directories that should not be part of the package. This variable accepts regular expressions. Thus, in “~$”, the $ sign indicates the end of the filename. All files ending with ~ will be ignored.
Let’s generate the packages:
|
|
Note:
CPack, when generating the binary package, only takes into account files installed by an install command under one condition: the destination must be a path relative to the content of the CMAKE_INSTALL_PREFIX variable. Files whose installation path is absolute will be ignored. This is a current limitation of CPack, perhaps it will disappear?
So, we’ve reached the end of this article. We’ve covered some methods to get from CMake what we used to do with autotools (and even a bit more). However, we still need to look at some particular techniques regarding library distribution. That’s an opportunity for us to meet again next time.
Resources
Last updated 06 Dec 2009, 16:17 +0200.