22 September 2012

Getting started with CMake

 Working on so many projects, you might have noticed that compilation process of a project involves searching for required libraries and their paths on the system, building a bunch of source files and some of these source files depend on each other. Finally all the source files and libraries located in different folders are compiled to generate a binary file or a library file. A description of the build process is stored in a group of files.

CMake is a cross-platform build systems generator which makes it easier to build software in a unified manner on a broad set of platforms. http://www.scons.org/wiki/SconsVsOtherBuildTools provides a comparison of several software building tools available presently. Cmake is a generator: it generates native build system files like Makefile, Visual Studio or other IDE projects, etc. CMake scripting language is used to describe the build. The developer has to edit the CMakeLists.txt and invoke CMake but should not change any of the generated files directly.

CMake workflow -
1. CMake time: CMake is running and processing CMakeLists.txt
2. Build time: The build tool runs and invokes the compiler.
3. Install time: The compiled binaries are installed i.e. from the build area to an install location.
4. CPack time: CPack is running for building package.
5. Package install time: The time taken to install the package developed in previous step.

Installing CMake
For several platforms such as Windows, Linux, Mac, etc, CMake is available as a standard package or binaries which can be downloaded from its official website. You can type cmake --help in the terminal to check the version of cmake and usage information.Generally CMake path is added to the system path automatically by the installer. If its not added, then you can run it directly from the installed location.

Simple CMake
cmake_minimum_required (VERSION 2.8)
# This project uses C source code.
PROJECT (A Simple C Project)
ADD_LIBRARY (mylibrary STATIC libsource.c)
# Build executable using specified list of source files.
ADD_EXECUTABLE (simple main.c)
TARGET_LINK_LIBRARIES (simple mylibrary z m)
 
This generates makefile to compile main.c file and link it to mylibrary. To build this project in linux, just go the terminal, go the folder where this cmake is present and type
cmake .
make

CMake Commands
CMake is in essence a simple interpreter. CMake input files have an extremely simple but powerful syntax. It consists of commands, primitive flow control constructs, macros and variables. All commands have exactly the same syntax:
COMMAND_NAME (ARGUMENT1, ARGUMENT2)

You can check the cmake help on any of its command by typing
cmake --help-command add_executable
 Output: Add an executable to the project using the specified source files.

cmake --help-command project
Syntax: PROJECT(<projectName> [languageName1, languageName2])
Set a name for entire project. Additionally this sets the variables <projectName>_BINARY_DIR and <projectName>_SOURCE_DIR to the respective values. Optionally you can specify which languages your project supports. Eg - CXX for C++, C, Fortran, etc. By default C and CXX are enabled.

cmake --help-command add_library
Syntax:  ADD_LIBRARY(<name> [STATIC | SHARED | MODULE] [EXCLUDE FROM ALL] source1 source2 ... sourceN)
Adds a library target called <name> to be build from the source files listed in the command invocation. STATIC, SHARED or MODULE may be given to specify the type of library to be created. By default the library file will be created in the build tree directory corresponding to the source tree directory in which the command was invoked.

Flow Control Constructs
A few flow control constructs, such as IF and FOREACH are used. The IF construct uses one of several types of expressions, such as boolean(NOT, AND, OR), check if command exists(COMMAND) and check if file exists(EXISTS). Expressions however cannot contain other commands. An example of commonly used IF statement would be:
IF(UNIX)
    IF(APPLE)
         SET(GUI "Cocoa"
     ELSE(APPLE)
         SET(GUI "X11"
     ENDIF(APPLE)
ELSE(UNIX)
     IF(WIN32)
          SET(GUI "Win32"
     ELSE(WIN32)
          SET(GUI "Unknown"
     ENDIF(WIN32)
ENDIF(UNIX)
MESSAGE("GUI system is ${GUI}")

This example shows simple us of IF statement and variables. FOREACH is used int the same fashion. For example if a list of executables are to be created, where every executable is created from a source file with the same name, following FOREACH would be used:

SET(SOURCES source1 source2 source3)
FOREACH(source ${SOURCES})
       ADD_EXECUTABLE(${source} ${source}.c)
ENDFOREACH(source)

Macros use a syntax of both commands and flow control constructs. MACRO construct is used to define a macro. For example, lets create a macro which creates executable from the source by linking it to libraries.

MACRO(CREATE_EXECUTABLE NAME SOURCES LIBRARIES)
      ADD_EXCUTABLE(${NAME} ${SOURCES})
      TARGET_LINK_LIBRARIES(${NAME} ${LIBRARIES})
ENDMACRO(CREATE_EXECUTABLE)
ADD_LIBRARY(mylibrary libSource.c)
CREATE_EXECUTABLE(myprogram main.c mylibrary)

Macros are not equivalent to procedures or functions from the programming languages and do not allow recursion.

Conditional Compiling
The build process also should be able to find and set locations to the system resources your project needs. All these functions are achieved in CMake using conditional compiling. E.g. -  If the project has to compile in two different modes namely DEBUG and REGULAR, you can define sections to compile in different mode based on the enabled variable.
#ifdef DEBUG
      fprintf(stderr, "The value of i is: %d\n", i);
#endif

In order to tell CMake to add -DDEBUG to compile lines, you can use the SET_SOURCE_FILE_PROPERTIES with the COMPILE_FLAGS property. But you probably do not want to edit the CMakeLists.txt every time you switch between the debug and regular build. The OPTION command creates a boolean variable that can be set before building the project. The syntax for above example will be:

OPTION(MYPROJECT_DEBUG    "Build the project using debugging code" ON)
IF(MYPROJECT_DEBUG)
    SET_SOURCE_FILE_PROPERTIES( libsource.c main.c COMPILE_FLAGS -DDEBUG)
#ENDIF(MYPROJECT_DEBUG)

The flags can be set/unset using any cmake gui component installed on the system like CMake-Gui. After using cmake command, go to that folder and initiate the gui in that folder. This will open a GUI where you can select or deselect components.

Another sample of user controlled build option
CMAKE_MINIMUM_REQUIRED (VERSION 2.8)
# This project uses C source code
PROJECT(TotallyFree C)
# Build option with default value to ON
OPTION(WITH_ACRODICT "include/acronym/dictionary/support" ON)
SET(BUILD_SHARED_LIBS true)
#build executable using specified list of source files
ADD_EXECUTABLE(Acrolibre acrolibre.c)
IF(WITH_ACRODICT)
       SET(LIBSRC acrodict.h acrodict.c)
       ADD_LIBRARY(acrodict ${LIBSRC})
       ADD_EXECUTABLE(Acrodictlibre acrolibre.c)
       TARGET_LINK_LIBRARIES(Acrodictlibre acrodict)
       SET_TARGET_PROPERTIES(Acrodictlibre PROPERTIES COMPILE_FLAGS "-DUSE_ACRODICT")
ENDIF(WITH_ACRODICT)

Another common type of variable is a PATH which specifies the location of some file on the system. If the program relies on the the file Python.h, this would be put in cmake file like this -
FIND_PATH(PYTHON_INCLUDE_PATH  Python.h    /usr/include   /usr/local/include)

FIND_PATH command is used to find a directory containing the named file. A cache entry named by <VAR> is created to store the result of this command. If the file in a directory is found, the result is stored in the variable and the search will not be repeated unless the variable is cleared. If nothing is found, the result will be <VAR>-NOTFOUND and the search will be attempted again the next time find_path is invoked with the same variable.

But the above command looks for that file in only those folder for the current project. So you'll have to add the same line for every project. To avoid this, you can include other CMake files, called modules. CMake comes with several useful modules, from the type that searches for different packages to the type that actually performs some tasks or define MACROS. For the list of all modules, check the Modules subdirectory of CMake. E.g. - FindPythonLibs.cmake which finds python libraries and header files on almost every system.

Handling Subdirectories
As a software developer, you probably organize source code in subdirectories. Different subdirectories can represent libraries, executables, testing or even documentation. We can enable or disable subdirectories to build parts of project and skip other parts. To tell CMake to process a subdirectory, use SUBDIRS comand. This command tells CMake to go to the specified subdirectory and find the CMakeLists.txt file.
The toplevel project looks something like this -
PROJECT(MyProject C)
SUBDIRS(SomeLibrary)
INCLUDE_DIRECTORIES(SomeLibrary)
ADD_EXECUTABLE(MyProgram main.c)
TARGET_LINK_LIBRARY(MyProgram MyLibrary)

INCLUDE_DIRECTORIES command tells the compiler where to find the header files for main.c

Lets see an example to cover all the steps covered till now. The CMake file has to be written for project called Zlib.

PROJECT(ZLIB)
#source files for zlib
SET(ZLIB_SRC adler32.c   gzio.c   inftrees.c   uncompr.c   compress.c   infblock.c   infutil.c   zutil.c   crc32.c   infcodes.c   deflate.c   inffast.c   inflate.c   trees.c )
ADD_LIBRARY(zlib ${ZLIB_SRCS})
ADD_EXECUTABLE(example example.c)
TARGET_LINK_LIBRARIES(example zlib)

ZLib needs unistd.h on some platforms. So a test has to be added to the project.

INCLUDE( ${CMAKE_ROOT}/Modules/CheckIncludeFile.cmake)
CHECK_INCLUDE_FILE( "unistd.h" HAVE_UNISTD_H)
IF(HAVE_UNISTD_H)
    ADD_DEFINITION(-DHAVE_UNISTD_H)
ENDIF(HAVE_UNISTD_H)

Also, something has to be done abou the shared libraries on Windows. ZLIB needs to be compiled with -DZLIB_DLL, for proper export macros. So the following options have to be added.

OPTION(ZLIB_BUILD_SHARED)
  "Build ZLIB shared" ON)
IF(WIN32)
    IF(ZLIB_BUILD_SHARED)
         SET(ZLIB_DLL 1)
    ENDIF(ZLIB_BUILD_SHARED)
ENDIF(WIN32)
IF(ZLIB_DLL)
      ADD_DEFINITION(-DZLIB_DLL)
ENDIF(ZLIB_DLL)

This is a very simple example and will get you started with cmake for your basic projects. CMake is capable of doing several other tasks. It can now do platform independent TRY_RUN and TRY_COMPILE builds, wich come in handy when you want to test the capabilities of the system. CMake natively supports C and C++ and with a little effort, it can generate builds for most available languages.