CMake: The Cross Platform Build System

时间:2022-12-27 12:18:05

CMake: The Cross Platform Build System

Here's an article I wrote for the July 2006 issue of Linux Magazine. This article is Copyright (c) 2006 by Tanner Lovelace, All Rights Reserved.


Abstract:

CMake is a portable build system: Create a single source definition and build your code on one or many platforms. Learn how to use CMake and see how KDE uses the tool for the project’s next generation build system.

CMake: The Cross Platform Build System

How many times have you downloaded a source tarball only to find that your system’s version of autotools fails to work? While autotools is quite capable, variants abound, many of which are simply incompatible, and fixing autotools requires knowledge of at least make, m4, and shell scripting. Moreover, autotools can grow so complex that additional tools, such as automake, are required just to manage the intricacies. Still, many open source projects use autotools as a build system.

However, some projects have started to switch to other tools, such as Cross Platform Make, or CMake, which has several interesting features not found in autotools. Like autotools, CMake can generate a Unix Makefile. Unlike autotools, CMake can also create KDevelop, Visual Studio, and (Apple) XCode project files from the same configuration file. Furthermore, both configuration and build commands are specified in one location and handled entirely by CMake. Because of these and other advantages over autotools, CMake was recently chosen as the configuration and build system for KDE4.

While many Linux distributions offer CMake, the version included is probably out of date. A lot of new CMake features have been added recently, and you should download the latest 2.4. x version from http://www.cmake.org/HTML/Download.html. (KDE4 requires at least version 2.4.1. As the magazine went to press, version 2.4.2 had just been released.)

After you download the newest release, un-tar it in a temporary directory. CMake uses itself as a build system, so the application must be bootstrapped before you can compile it. To bootstrap, compile, and install CMake, type these commands:

$ ./bootstrap
$ make
$ sudo make install

Without any options, CMake is installed in /usr/local/. If you want to install to another directory, add ––prefix= /other/ directory to the bootstrap command.

Using CMake

Once CMake is installed, it’s ready for use. To begin, you need a project that uses CMake. Since CMake uses CMake to build itself, let’s use it. Create a new temporary directory, change to that directory, and un-tar the CMake sources anew.

One of the most important concepts in CMake is out of source builds. While CMake allows source and build directories to be the same, the practice isn’t recommended. Instead, it’s better to separate the source and build directories so a single source directory can serve as the basis for different types of builds, such as so-called "debug" and "non-debug" builds, and even a number of builds, one for each target platform.

With that in mind, create a directory for your build files. From within that directory type:

$ cmake /path/to/CMAKE/SOURCE

This configures CMake and generates Unix Makefiles that can be used to compile CMake using normal make commands. If you prefer to use an integrated development environment (IDE) like KDevelop, CMake supports that too. To generate KDevelop project files for CMake, type this command:

$ cmake –G KDevelop3 /path/to/CMAKE/SOURCE

This configures CMAKE and generates a KDevelop project file that can be loaded and used directly.

If you prefer not to use the command-line at all, CMake also provides a curses interface that will let you interactively edit all available options. It’s called ccmake and you run it exactly like CMake. To try ccmake, remain in the build directory and run:

$ ccmake /path/to/CMAKE/SOURCE

This launches a curses interface to all of the configuration variables. If you run ccmake before CMake, the interface is rather bare. There’s nothing to worry about, though: CMake only aggregates all options from the source tree into a single cache file after the first configuration.

So, let’s configure the tree. Press c to configure. You’ll see some messages on the status line just above the CMake version banner, and shortly after, you should see more variables on the screen. If the display still seems rather sparse, press t to switch to advanced mode, which shows you all available variables. Use the Up and Down (arrow) keys to traverse the list, and hit Enter if you want to modify any.

One variable that’s commonly modified is CMAKE_INSTALL_PREFIX. As you might suspect, this variable determines the base directory of where a project is installed. By default, it points to /usr/local, but let’s change it to be something else. Center the cursor on the current value for CMAKE_INSTALL_PREFIX and press Enter. Use the arrow keys to go to the end of the current value, then press Backspace repeatedly to delete it. In it’s place, type /opt/CMAKE. Press Enter to record the new value. Press c to configure again, then press g to generate the Makefiles. Now, when you type make install, /opt/CMAKE is the install prefix.

If you subsequently want to change the installation prefix from the command-line, use this command:

$ cmake –DCMAKE_INSTALL_PREFIX=/opt/CMAKE

This tells CMake to set CMAKE_INSTALL_PREFIX to /opt/CMAKE.

Creating Projects with CMake

So, how do you use CMake for your own project?

CMake stores its commands in a file named CMakeLists.txt. Here’s a CMakeLists.txt for the familiar "Hello, World! " C program:

PROJECT(hello C)
ADD_EXECUTABLE(hello hello.c)

That’s it. The first line specifies a C language project called hello. The second line creates an executable called hello from a source file named hello.c. If you have more than one source file, just add the files after hello.c. You can build your new project with the same commands used to build CMake.

Now let’s try something a bit more complicated. Suppose you have a library of code that you want to build and link with your "Hello, World!" program. The CMake code for that would be:

PROJECT (hello C)
ADD_LIBRARY(hellolib STATIC hellolib.c)
ADD_EXECUTABLE(hello hello.c)
TARGET_LINK_LIBRARIES(hello hellolib)

This short file creates a statically linked hellolib library from the file hellolib.c, and links it to the hello program. You can also specify system libraries in the TARGET_LINK_LIBRARIES command.

If shorthand was the extent of what CMake offered, it wouldn’t offer much advantage over straight Makefiles. But CMake also gives you full control over any options your program may need. As an example, consider a project that optionally includes a component that communiates via USB and allows the user, at compile time, to specify whether to include that component or not. The CMakeLists.txt file to do that looks like this:

PROJECT(myproject)
OPTION(WITH_USB "Include our USB component" OFF)
SET(SRCS file1.c file2.c file3.c)
IF(WITH_USB)
  SET(SRCS ${SRCS} usbfile1.c usbfile2.c)
ENDIF(WITH_USB)
ADD_EXECUTABLE(myproject ${SRCS})

The OPTION(...) line creates a parameter WITH_USB and set its default value to OFF. CMake also includes a help string to describe what the option does. Based on the value of WITH_USB, the variable SRCS either includes or excludes the two USB-related files usbfile1.c and usbfile2.c. SRCS is then passed to the ADD_EXECUTABLE call to define the program’s source files. To include USB support, simply enable WITH_USB like this:

$ cmake –DWITH_USB=ON /path/to/source/files

Let’s go a little bit further and make the optional USB support a library in a subdirectory. The new CMakeLists.txt file would look like this:

PROJECT(myproject)
OPTION(WITH_USB "Include our USB component" OFF)
SET(SRCS file1.c file2.c file3.c)
ADD_EXECUTABLE(myproject ${SRCS})
IF(WITH_USB)
  ADD_DIRECTORY(usblib)
  TARGET_LINK_LIBRARIES(myproject usblib)
ENDIF(WITH_USB)

Given this file, the USB source files would be placed in a subdirectory called usblib along with a new CMakeLists.txt file:

PROJECT(usblib)
SET(SRCS usbfile1.c usbfile2.c)
ADD_LIBRARY(usblib ${SRCS})

Now, if USB support is enabled, CMake builds the USB library and link sit into the executable.

But how does the code code know to use the optional USB code? First, the USB-specific code should be surrounded with a #ifdef WITH_USB. Next, CMake processes a special configuration file, substituting placeholders with CMake variables.

Let’s make a new file called config.h.cmake that contains this:

#ifndef TEST_CONFIG_H
#define TEST_CONFIG_H

#cmakedefine WITH_USB

#endif

Then, in the CMakeLists.txt file, modify the first part to look like this:

PROJECT(myproject)
OPTION(WITH_USB "Include our USB component" OFF)
CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/config.h.cmake 
  ${CMAKE_BINARY_DIR}/config.h)
INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR})

Given this, CMake translates config.h.cmake to create config.h in the build directory. The INCLUDE_DIRECTORIES command tells the compiler to add both the source directory and the build directory to the include path. If WITH_USB is enabled, CMake replaces #cmakedefine WITH_USB with #define WITH_USB. Otherwise, it replaces #cmakedefine WITH_USB with /*#undef WITH_USB*/. As long as you #include config.h and surround the USB code with #ifdef WITH_USB, everything just works. The only downside at the moment is that config.h.cmake must be created manually. CMake includes standard macros to check for header files, functions, variables, libraries, symbols, and sizes of types. For instance, if you’d like to know if the header file unistd.h exists, use these commands:

INCLUDE(CheckIncludeFile) 
CHECK_INCLUDE_FILE(unistd.h HAVE_UNISTD) 

This checks if the compiler can find unistd.h and sets the HAVE_UNISTD variable accordingly. Now add #cmakedefine HAVE_UNISTD to config.h.cmake and you’re good to go.

Functions can be found by including CheckFunctionExists and using the CHECK_FUNCTION_EXISTS macro. Variables can be checked by including CheckVariableExists and using CHECK_VARIABLE_EXISTS. Both features use the same syntax as CHECK_INCLUDE_FILE.

To search for a library, use the CHECK_LIBRARY_EXISTS macro. It requires you to specify the library name, the name of a function provided by the library, a location to search, and a variable to store the result of the search. For example, to check for the dynamic loader library, use these commands:

INCLUDE(CheckLibraryExists) 
CHECK_LIBRARY_EXISTS(dl dlopen "" HAVE_LIBDL) 

This checks for the dlopen() function in the libdl library and sets the HAVE_LIBDL variable to true if it’s found. CMake automatically checks standard system libraries, so an empty string suffices for the library location.

Searching for a symbol finds both symbols and functions names. In addition to a symbol and an output variable, you must also specify the header files to include. Here’s an example that searches for the symbol LC_MESSAGES in the header file locale.h and sets the HAVE_LC_MESSAGES variable accordingly:

INCLUDE(CheckSymbolExists)
CHECK_SYMBOL_EXISTS(LC_MESSAGES "locale.h" HAVE_LC_MESSAGES)

Checking for the size of a variable works a bit differently. In this feature, the output variable is set to the size in bytes of the type being checked and a new variable, HAVE_ ${VARIABLE}, is created to specify if the type exists or not. Here’s an example:

INCLUDE(CheckTypeSize)
CHECK_TYPE_SIZE (int INT_SIZE)

If you’re on a 32-bit machine, INT_SIZE would be set to 4 (bytes) and HAVE_INT_SIZE would be set to true.

In addition to mechanisms for detecting low-level configuration options, CMake includes options to find entire packages. For instance, many Linux programs make use of the X Windows System Version 11 (X11). To easily discover if X11 is installed and what options should be used for include files and linking, use this command:

FIND_PACKAGE(X11 REQUIRED)

The optional REQUIRED parameter tells CMake to stop with an error if the package isn’t found. FIND_PACKAGE (X11 REQUIRED) sets the following variables:

  • X11_FOUND is true if X11 is available.
  • X11_INCLUDE_DIR contains the include directories to use X11.
  • X11_LIBRARIES points to the libraries to link against to use X11.

The last two variables can be fed directly into INCLUDE_DIRECTORIES and TARGET_LINK_LIBRARIES. CMake provides modules to search for many standard packages. Type cmake ––help-module-list for a complete list of all installed modules including FIND_PACKAGE modules.

If all else fails, CMake also lets you compile and run C or C++ code snippets using the TRY_COMPILE or TRY_RUN commands. See the CMake documentation for more information on how to use them.

CMake provides extensive online help for all of its built-in commands and modules. cmake ––help will get you started. cmake ––help-command commandname and cmake ––help-module modulename yields help for a specific command or module. To get a list of available commands or modules use cmake ––help-command-list or cmake ––help-module-list.

CMake and KDE4

Armed with the basics of CMake, how do you get started with CMake and KDE4 development? First, make sure you have at least version 2.4.1 of CMake. (The CMake developers went above and beyond the call of duty to add things for KDE, and as a result KDE4 requires at least CMake 2.4.1.) You also need Qt 4.1.1 or later and recent Subversion copies of kdelibs snapshot and kdebase. See http://developer.kde.org/ source/anonsvn.html for information on how to retrieve KDE from Subversion. (Make sure you get the kdelibs snapshot and not kdelibs from trunk. Since kdelibs is currently changing rapidly, kdebase is kept in sync with a regular snapshot of kdelibs.)

KDE4 requires that all builds be out of the source directory. Therefore, you’ll need to create a build directory for kdelibs. From that build directory, use this command to configure kdelibs:

$ cmake –DCMAKE_INSTALL_PREFIX=/opt/kde4 \
  –DCMAKE_BUILD_TYPE=debug \
  /path/to/kdelib/sources

This command configures kdelibs to build with regular debugging and installs the result in /opt/kde4. If you prefer full debugging instead, change debug to debugfull. KDE4 also supports several optional packages. After configuring the build with CMake, s you can run ccmake from the build directory and turn options off or on as needed.

Once you have the configuration you want, generate new Makefiles by pressing g and then type…

$ make
$ sudo make install

... to build and install kdelibs. Once the kdelibs snapshot is installed, you can compile kdebase the same way. Make sure you also read the file COMPILING-WITH-CMAKE in the kdelibs source directory and check the KDE Wiki for more information. If you have any questions about cmake and KDE, you can ask them on the kde-buildsystem mailing list.

Builds Made Simple

CMake’s simple syntax allow developers to specify configuration and build options and allows the end-user to build using their preferred native build system. Thanks to CMake, KDE4 development is currently happening at a fast pace on Linux, many flavors of Unix, Windows, and OS X. Learn more about CMake at http://www.cmake.org/.