Converting Existing Systems To CMake

For many people, the first thing they will do with CMake is convert an existing project from using an older build system to use CMake. This can be a fairly easy process, but there are a few issues to consider. This section will address those issues and provide some suggestions for effectively converting a project over to CMake. The first issue to consider when converting to CMake is the project’s directory structure.

Source Code Directory Structures

Most small projects will have their source code in either the top level directory or in a directory named src or source. Even if all of the source code is in a subdirectory, we highly recommend creating a CMakeLists file for the top level directory. There are two reasons for this. First, it can be confusing to some people that they must run CMake on the subdirectory of the project, instead of the main directory. Second, you may want to install documentation or other support files from the other directories. By having a CMakeLists file at the top of the project, you can use the add_subdirectory command to step down into the documentation directory where its CMakeLists file can install the documentation (you can have a CMakeLists file for a documentation directory with no targets or source code).

For projects that have source code in multiple directories, there are a few options. One option that many Makefile-based projects use is to have a single Makefile at the top-level directory that lists all the source files to compile in their subdirectories. For example:

SOURCES=\
  subdir1/foo.cxx \
  subdir1/foo2.cxx \
  subdir2/gah.cxx \
  subdir2/bar.cxx

This approach works just as well with CMake using a similar syntax:

set(SOURCES
  subdir1/foo.cxx
  subdir1/foo2.cxx
  subdir1/gah.cxx
  subdir2/bar.cxx
  )

Another option is to have each subdirectory build a library or libraries that can then be linked into the executables. In those cases, each subdirectory would define its own list of source files and add the appropriate targets. A third option is a mixture of the first two; each subdirectory can have a CMakeLists file that lists its sources, but the top-level CMakeLists file will not use the add_subdirectory command to step into the subdirectories. Instead, the top-level CMakeLists file will use the include command to include each of the subdirectory’s CMakeLists files. For example, a top-level CMakeLists file might include the following code

# collect the files for subdir1
include(subdir1/CMakeLists.txt)
foreach(FILE ${FILES})
  set(subdir1Files ${subdir1Files} subdir1/${FILE})
endforeach()

# collect the files for subdir2
include(subdir2/CMakeLists.txt)
foreach(FILE ${FILES})
  set(subdir2Files ${subdir2Files} subdir2/${FILE})
endforeach()

# add the source files to the executable
add_executable(foo ${subdir1Files} ${subdir2Files})

While the CMakeLists files in the subdirectories might look like the following:

# list the source files for this directory
set(FILES
  foo1.cxx
  foo2.cxx
  )

The approach you use is entirely up to you. For large projects, having multiple shared libraries can certainly improve build times when changes are made. For smaller projects, the other two approaches have their advantages. The main suggestion here is to choose a strategy and stick with it.

Build Directories

The next issue to consider is where to put the resulting object files, libraries, and executables. There are a few different, commonly used approaches, and some work better with CMake than others. The recommended strategy is to put the binary files into a separate tree that has the same structure as the source tree. For example, if the source tree looked like the following:

foo/
  subdir1
  subdir2

the binary tree might look like:

foobin/
  subdir1
  subdir2

For some Windows generators, such as Visual Studio, build files are actually kept in a subdirectory matching the selected configuration; e.g. debug, release, etc.

If you need to support multiple architectures from one source tree, we highly recommend a directory structure like the following:

projectfoo/
  foo/
    subdir1
    subdir2
  foo-linux/
    subdir1
    subdir2
  foo-osx/
    subdir1
    subdir2
  foo-solaris/
    subdir1
    subdir2

That way, each architecture has its own build directory and will not interfere with any other architecture. Remember that not only are the object files kept in the binary directories, but also any configured files that are typically written to the binary tree. Another tree structure found primarily on UNIX projects is one where the binary files for different architectures are kept in subdirectories of the source tree (see below). CMake does not work well with this structure, so we recommend switching to the separate build tree structure shown above.

foo/
  subdir1/
    linux
    solaris
    hpux
  subdir2/
    linux
    solaris
    hpux

CMake provides three variables for controlling where binary targets are written. They are the CMAKE_RUNTIME_OUTPUT_DIRECTORY, CMAKE_LIBRARY_OUTPUT_DIRECTORY, and CMAKE_ARCHIVE_OUTPUT_DIRECTORY variables. These variables are used to initialize properties of libraries and executables to control where they will be written. Setting these enables a project to place all the libraries and executables into a single directory. For projects with many subdirectories, this can be a real time saver. A typical implementation is shown below:

# Setup output directories.
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)

In this example, all the “runtime” binaries will be written to the bin subdirectory of the project’s binary tree, including executable files on all platforms and DLLs on Windows. Other binaries will be written to the lib directory. This approach is very useful for projects that make use of shared libraries (DLLs) because it collects all of the shared libraries into one directory. If the executables are placed in the same directory, then they can find the required shared libraries more easily when run on Windows.

One final note on directory structures: with CMake, it is perfectly acceptable to have a project within a project. For example, within the Visualization Toolkit’s source tree is a directory that contains a complete copy of the zlib compression library. In writing the CMakeLists file for that library, we use the project command to create a project named VTKZLIB even though it is within the VTK source tree and project. This has no real impact on VTK, but it does allow us to build zlib independent of VTK without having to modify its CMakeLists file.

Useful CMake Commands When Converting Projects

There are a few CMake commands that can make the job of converting an existing project easier and faster. The file command with the GLOB argument allows you to quickly set a variable containing a list of all the files that match the glob expression. For example:

# collect up the source files
file(GLOB SRC_FILES "*.cxx")

# create the executable
add_executable(foo ${SRC_FILES})

will set the SRC_FILES variable to a list of all the .cxx files in the current source directory. It will then create an executable using those source files. Windows developers should be aware that glob matches are case sensitive.

Converting UNIX Makefiles

If your project is currently based on standard UNIX Makefiles then their conversion to CMake should be fairly straightforward. Essentially, for every directory in your project that has a Makefile, you will create a matching CMakeLists file. How you handle multiple Makefiles in a directory depends on their function. If the additional Makefiles (or Makefile type files) are simply included in the main Makefile, you can create matching CMake input (.cmake) files and include them into your main CMakeLists file in a similar manner. If the different Makefiles are meant to be invoked on the command line for different situations, consider creating a main CMakeLists file that uses some logic to choose which one to include based on a CMake option.

Frequently Makefiles have a list of object files to compile. These can be converted to CMake variables as follows:

OBJS= \
  foo1.o \
  foo2.o \
  foo3.o

becomes

set(SOURCES
  foo1.c
  foo2.c
  foo3.c
)

While the object files are typically listed in a Makefile, in CMake the focus is on the source files. If you used conditional statements in your Makefiles, they can be converted over to CMake if commands. Since CMake handles generating dependencies, most dependencies or rules to generate dependencies can be eliminated. Where you have rules to build libraries or executables, replace them with add_library or add_executable commands.

Some UNIX build systems (and source code) make heavy use of the system architecture to determine which files to compile or what flags to use. Typically this information is stored in a Makefile variable called ARCH or UNAME. The first choice in these cases is to replace the architecture-specific code with a more generic test. With some software packages, there is too much architecture specific code for such a change to be reasonable, or you may want to make decisions based on architecture for other reasons. In those cases, you can use the variables CMAKE_SYSTEM_NAME and CMAKE_SYSTEM_VERSION. They provide fairly detailed information on the operating system and version of the host computer.

Converting Autoconf Based Projects

Autoconf-based projects primarily consist of three key pieces. The first is the configure.in file which drives the process. The second is Makefile.in which will become the resulting Makefile, and the third piece is the remaining configured files that result from running configure. In converting an autoconf based project to CMake, start with the configure.in and Makefile.in files.

The Makefile.in file can be converted to CMake syntax as explained in the preceding section on converting UNIX Makefiles. Once this has been done, convert the configure.in file into CMake syntax. Most functions (macros) in autoconf have corresponding commands in CMake. A short table of some of the basic conversions is listed below:

AC_ARG_WITH

Use the option command.

AC_CHECK_HEADER

Use the check_include_file macro from the CheckIncludeFile module.

AC_MSG_CHECKING

Use the message command with the STATUS argument.

AC_SUBST

Done automatically when using the configure_file command.

AC_CHECK_LIB

Use the check_libary_exists macro from the CheckLibraryExists module.

AC_CONFIG_SUBDIRS

Use the add_subdirectory command.

AC_OUTPUT

Use the configure_file command.

AC_TRY_COMPILE

Use the try_compile command.

If your configure script performs test compilations using AC_TRY_COMPILE, you can use the same code for CMake. Either put it directly into your CMakeLists file if it is short, or preferably put it into a source code file for your project. We typically put such files into a CMake subdirectory for large projects that require such testing.

Where you are relying on autoconf to configure files, you can use CMake’s configure_file command. The basic approach is the same and we typically name input files to be configured with a .in extension just as autoconf does. This command replaces any variables in the input file referenced as ${VAR} or @VAR@ with their values as determined by CMake. If a variable is not defined, it will be replaced with nothing. Optionally, only variables of the form @VAR@ will be replaced and ${VAR} will be ignored. This is useful for configuring files for languages that use ${VAR} as a syntax for evaluating variables. You can also conditionally define variables using the C pre processor by using #cmakedefine VAR. If the variable is defined then configure_file will convert the #cmakedefine into a #define; if it is not defined, it will become a commented out #undef. For example:

/* what byte order is this system */
#cmakedefine CMAKE_WORDS_BIGENDIAN

/* what size is an INT */
#cmakedefine SIZEOF_INT @SIZEOF_INT@

Converting Windows Based Workspaces

To convert a Visual Studio solution to CMake involves a few steps. First you will need to create a CMakeLists file at the top of your source code directory. As always, this file should start with cmake_minimum_required and project command that defines the name of the CMake project. This will become the name of the resulting Visual Studio solution. Next, add all of your source files into CMake variables. For large projects that have multiple directories, create a CMakeLists file in each directory as described in the section on source directory structures at the beginning of this chapter. You will then add your libraries and executables using add_library and add_executable. By default, add_executable assumes that your executable is a console application. Adding the WIN32 argument to add_executable indicates that it is a Windows application (using WinMain instead of main).

There are a few nice features that Visual Studio supports and CMake can take advantage of. One is support for class browsing. Typically in CMake, only source files are added to a target, not header files. If you add header files to a target, they will show up in the workspace and then you will be able to browse them as usual. Visual Studio also supports the notion of groups of files. By default, CMake creates groups for source files and header files. Using the source_group command, you can create your own groups and assign files to them. If you have any custom build steps in your workspace, these can be added to your CMakeLists files using the add_custom_command command. Custom targets (Utility Targets) in Visual Studio can be added with the add_custom_target command.