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.
~ $ ls -R hello
hello:
AUTHORS CMakeLists.txt src ChangeLog README
COPYING INSTALL
hello/src:
CMakeLists.txt main.cpp
We start with a very simple code that serves only as a support:
// main.cpp
#include <iostream>
#include <cstdlib>
using namespace std;
int main (void)
{
cout << "Hello World !" << endl;
exit (EXIT_SUCCESS);
}
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.
# top-level CMakeLists.txt
cmake_minimum_required (VERSION 2.4.0 FATAL_ERROR)
project (Hello)
include (CheckIncludeFileCXX)
set (Hello_RQ_HEADERS iostream cstdlib)
foreach (RQ_HDR ${Hello_RQ_HEADERS})
check_include_file_cxx (${RQ_HDR} RQ_HDR_RET)
if (NOT RQ_HDR_RET)
message (FATAL_ERROR "missing ${RQ_HDR} !")
endif (NOT RQ_HDR_RET)
endforeach (RQ_HDR ${Hello_RQ_HEADERS})
add_subdirectory (src)
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.
# src CMakeLists.txt
include_directories (${Hello_SOURCE_DIR}/src)
add_executable (hello main.cpp)
install (TARGETS hello DESTINATION bin)
Let’s create a build directory for our project and a directory for its installation (this will allow us to check the installed files):
mkdir hello_build
mkdir hello_install
Let’s build and install our project:
cd hello_build
cmake -DCMAKE_INSTALL_PREFIX=../hello_install ../hello
make
make install
ls -R ../hello_install
../hello_install:
bin
../hello_install/bin:
hello
Hello World !
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:
// main.cpp
#include "config.h"
...
And add the location of config.h to the compilation directives. This file will be saved in the project’s build directory.
src CMakeLists.txt
include_directories (${Hello_SOURCE_DIR}/src ${Hello_BINARY_DIR}/src)
add_executable (hello main.cpp)
install (TARGETS hello DESTINATION bin)
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.
// main.cpp
...
const int hcount = HELLO_COUNT;
int main (void)
{
for (int i=hcount; i>0; i--)
cout << "Hello World !" << endl;
...
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.
// config.h.cmake
#ifndef _CONFIG_H
#define _CONFIG_H
#define HELLO_COUNT @WITH_HELLO_COUNT@
#endif /* _CONFIG_H */
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:
# top-level CMakeLists.txt
if (NOT WITH_HELLO_COUNT)
set (WITH_HELLO_COUNT 1 CACHE STRING "Repeating count")
endif (NOT WITH_HELLO_COUNT)
...
configure_file (${Hello_SOURCE_DIR}/src/config.h.cmake ${Hello_BINARY_DIR}/src/config.h)
...
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:
$ cmake -LH ../hello
...
// Repeating count
WITH_HELLO_COUNT:STRING=1
...
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).
cmake -DCMAKE_INSTALL_PREFIX=../hello_install -DWITH_HELLO_COUNT=2 ../hello
make
make install
../hello_install/bin/hello
Hello World !
Hello World !
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:
# Hello version number
set (Hello_MAJOR 0)
set (Hello_MINOR 0)
set (Hello_PATCH 0)
set (Hello_VERSION ${Hello_MAJOR}.${Hello_MINOR}.${Hello_PATCH})
# Hello release date
set (Hello_RELEASE "2008-12-03")
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:
top-level CmakeLists.txt
...
include (version.cmake)
message (STATUS "*** Building Hello ${Hello_VERSION} ***")
That’s for the cosmetics… Yes, it’s needed! But we can also use it in the program itself:
// config.h.cmake
...
#define HELLO_MAJOR @Hello_MAJOR@
#define HELLO_MINOR @Hello_MINOR@
#define HELLO_PATCH @Hello_PATCH@
#define HELLO_RELEASE @Hello_RELEASE@
...
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:
//main.cpp
...
#ifdef HELLO_NLS_ENABLED
#include <locale>
#include <libintl.h>
#define _(String) dgettext (HELLO_NLS_PACKAGE, String)
#else /* HELLO_NLS_ENABLED */
#define _(String) String
#endif /* HELLO_NLS_ENABLED */
...
{
#ifdef HELLO_NLS_ENABLED
locale::global (locale (""));
bindtextdomain (HELLO_NLS_PACKAGE, HELLO_NLS_LOCALEDIR);
#endif /* HELLO_NLS_ENABLED */
...
cout << _("Hello World !") << endl;
...
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:
...
#cmakedefine HELLO_NLS_ENABLED
#cmakedefine HELLO_NLS_PACKAGE "@HELLO_NLS_PACKAGE@"
#cmakedefine HELLO_NLS_LOCALEDIR "@HELLO_NLS_LOCALEDIR@"
...
With the implementation ready in your program, you need to generate the po/hello.pot file using xgettext.
mkdir po
xgettext -k_ -o po/hello.pot -D src --package-name=hello main.cpp
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:
...
option (ENABLE_NLS "Native Language Support" ON)
...
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:
cmake -LH ../hello
...
// Native Language Support
ENABLE_NLS:BOOL=ON
...
Then, first, we perform detection of the necessary files and utilities:
...
if (ENABLE_NLS)
set (HELLO_NLS_ENABLED TRUE)
set (Hello_I18N_HEADERS locale.h libintl.h)
foreach (HDR ${Hello_I18N_HEADERS})
check_include_file_cxx (${HDR} HDR_RET)
if (NOT HDR_RET)
message (STATUS "missing ${HDR} ! Internationalization aborted.")
set (HELLO_NLS_ENABLED FALSE)
endif (NOT HDR_RET)
endforeach (HDR ${Hello_I18N_HEADERS})
if (HELLO_NLS_ENABLED)
find_program (MSGFMT_EXECUTABLE msgfmt)
if (NOT MSGFMT_EXECUTABLE)
message (STATUS "msgfmt not found! Internationalization aborted.")
set (HELLO_NLS_ENABLED FALSE)
endif (NOT MSGFMT_EXECUTABLE)
endif (HELLO_NLS_ENABLED)
else (ENABLE_NLS)
set (HELLO_NLS_ENABLED FALSE)
endif (ENABLE_NLS)
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:
if (HELLO_NLS_ENABLED)
set (HELLO_NLS_PACKAGE hello)
set (HELLO_NLS_LOCALEDIR ${CMAKE_INSTALL_PREFIX}/share/locale)
add_subdirectory (po)
message (STATUS "Native language support enabled.")
else (HELLO_NLS_ENABLED)
message (STATUS "Native language support disabled.")
endif (HELLO_NLS_ENABLED)
...
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:
# po CmakeLists.txt
add_custom_target (i18n ALL COMMENT "Building i18n messages.")
file (GLOB Hello_PO_FILES ${Hello_SOURCE_DIR}/po/*.po)
foreach (Hello_PO_INPUT ${Hello_PO_FILES})
get_filename_component (Hello_PO_INPUT_BASE ${Hello_PO_INPUT} NAME_WE)
set (Hello_MO_OUTPUT ${Hello_BINARY_DIR}/po/${Hello_PO_INPUT_BASE}.mo)
add_custom_command (TARGET i18n COMMAND ${MSGFMT_EXECUTABLE} -o ${Hello_MO_OUTPUT} ${Hello_PO_INPUT})
install (FILES ${Hello_MO_OUTPUT} DESTINATION share/locale/${Hello_PO_INPUT_BASE}/LC_MESSAGES RENAME ${HELLO_NLS_PACKAGE}.mo)
endforeach (Hello_PO_INPUT ${Hello_PO_FILES})
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:
cmake -DCMAKE_INSTALL_PREFIX=../hello_install ../hello
...
-- Native language support enabled.
...
make
...
make install
...
-- Installing /home/patrice/hello_install/share/locale/LC_MESSAGES/fr/hello.mo
...
../hello_install/bin/hello
Hello World!
“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.
...
set (CPACK_GENERATOR "TGZ")
set (CPACK_PACKAGE_VERSION_MAJOR ${Hello_MAJOR})
set (CPACK_PACKAGE_VERSION_MINOR ${Hello_MINOR})
set (CPACK_PACKAGE_VERSION_PATCH ${Hello_MAJOR})
set (CPACK_SOURCE_GENERATOR "TBZ2")
set (CPACK_SOURCE_PACKAGE_FILE_NAME Hello-${Hello_VERSION})
set (CPACK_SOURCE_IGNORE_FILES "~$" ".bz2$" ".gz$")
include (CPack)
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:
cmake -DCMAKE_INSTALL_PREFIX=../hello_install ../hello
...
make package
...
make package_source
...
ls *.tar*
Hello-0.0.0-Linux.tar.gz Hello-0.0.0.tar.bz2
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.