Some nice and accurate CMake tips

Tags: programming, howtos

Published on
« Previous post: Using CMake, C++, and pybind11 on hard … — Next post: Keeping a Lab Book »

CMake arguably makes life a lot easier for developers and users alike. Instead of fiddling around with autotools madness, we ideally now just issue the following sequence of commands to build a piece of software:

$ mkdir build
$ cd build
$ cmake ../
$ make

However, often things are not that easy. I have found that sometimes, anarchy reigns supreme in the world of CMake—different modules, different ways of doing the same thing, and a complete lack of enforced standards are big hurdles in using or integrating software written by other people.

In this article, I want to outline some rules for writing a simple CMake find module for your own software or for unsupported libraries. Moreover, I want to present some snippets for common situations. The code for this blog post is available on GitHub.

Writing a CMake module

CMake modules are the prime reason for its success. Briefly put, they permit you to find other libraries that you need to link against. A good module can make your life easy. A bad module can lead to weird error messages.

Finding a header-only library

For an easy start, assume that we are looking for a header-only library. The same structure will be used when looking for other libraries, by the way—it will just be a little bit longer.

Suppose we want to look for a header-only library foo that has one main header foo.h within some subdirectory foo. Yes, the creativity in names is strong here, but bear with me. The module below will look for this package in a standardized manner:

INCLUDE( FindPackageHandleStandardArgs )

# Checks an environment variable; note that the first check
# does not require the usual CMake $-sign.
IF( DEFINED ENV{FOO_DIR} )
  SET( FOO_DIR "$ENV{FOO_DIR}" )
ENDIF()

FIND_PATH(
  FOO_INCLUDE_DIR
    foo/foo.h
  HINTS
    ${FOO_DIR}
)

FIND_PACKAGE_HANDLE_STANDARD_ARGS( FOO DEFAULT_MSG
  FOO_INCLUDE_DIR
)

IF( FOO_FOUND )
  SET( FOO_INCLUDE_DIRS ${FOO_INCLUDE_DIR} )

  MARK_AS_ADVANCED(
    FOO_INCLUDE_DIR
    FOO_DIR
  )
ELSE()
  SET( FOO_DIR "" CACHE STRING
    "An optional hint to a directory for finding `foo`"
  )
ENDIF()

Let us take a look at this in more detail. The module first imports a CMake standard module, the FindPackageHandleStandardArgs macro, which permits us to delegate and standardize package finding. Next, we check the environment variables of the client for FOO_DIR. The user can specify such a variable to point to a non-standard include directory for the package, such as $HOME, or any directory that is not typically associated with libraries. A classical use case is the local installation on a machine where you lack root privileges.

In any case, information about the variable is being used and a new variable called FOO_DIR is not set in CMake. Next, we supply it to the FIND_PATH function of CMake. This function tries to find a specified path or file (foo/foo.h in our case) while looking in a standardized set of directories. See the CMake documentation for more details.

Information about the path is stored in FOO_INCLUDE_DIR. The nice thing is that we do not need to evaluate this variable, because the function FIND_PACKAGE_HANDLE_STANDARD_ARGS handles it: using some short descriptor (FOO) of the package, we can hand all the paths that we need to the function and it will automatically result in the appropriate status or warning message. Moreover, it will set the variable FOO_FOUND if the package was found.

If this is the case, we set FOO_INCLUDE_DIRS to point to the path that we found before. Notice that it is customary to use the plural form here because a package might conceivably have multiple include paths. Using the plural in all cases makes it simpler for clients to employ our module because they can just issue

TARGET_INCLUDE_DIRECTORIES( example ${FOO_INCLUDE_DIRS} )

somewhere in their code.

As a final step, we hide the variables by marking them as advanced, so that CMake users have to explicitly toggle them. This is merely for not cluttering up the output of cmake-gui.

This is the most basic skeleton for finding a header-only library. To actually use this module, you can now just issue

FIND_PACKAGE( foo REQUIRED )
TARGET_INCLUDE_DIRECTORIES( example ${FOO_INCLUDE_DIRS} )

in your code. Provided that CMake knows where to look for modules, this is all you need to do. To extend the module search path, just create a directory cmake/Modules in your main project folder and add the following lines to the main CMakeLists.txt:

LIST( APPEND CMAKE_MODULE_PATH
  ${CMAKE_SOURCE_DIR}/cmake/Modules
)

A caveat: the FIND_PACKAGE call is one of the few parts in CMake where capitalization matters. If you do FIND_PACKAGE( FOO ), the CMake parser will look for a file named FindFOO.cmake. Hence, in this case, since we are doing FIND_PACKAGE( foo ), the module is named Findfoo.cmake. Notice that I am strongly encouraging you to use uppercase spelling in all the variables that you export, as it makes life easier and developers do not have to think about the proper capitalization.

Finding a shared object or a static library

As a slightly more advanced topic, suppose you are looking for one library called bar that comes with an include directory plus a shared object. This requires some additions to the code above:

INCLUDE( FindPackageHandleStandardArgs )

# Checks an environment variable; note that the first check
# does not require the usual CMake $-sign.
IF( DEFINED ENV{BAR_DIR} )
  SET( BAR_DIR "$ENV{BAR_DIR}" )
ENDIF()

FIND_PATH(
  BAR_INCLUDE_DIR
    bar/bar.h
  HINTS
    ${BAR_DIR}
)

FIND_LIBRARY( BAR_LIBRARY
  NAMES bar
  HINTS ${BAR_DIR}
)

FIND_PACKAGE_HANDLE_STANDARD_ARGS( BAR DEFAULT_MSG
  BAR_INCLUDE_DIR
  BAR_LIBRARY
)

IF( BAR_FOUND )
  SET( BAR_INCLUDE_DIRS ${BAR_INCLUDE_DIR} )
  SET( BAR_LIBRARIES ${BAR_LIBRARY} )

  MARK_AS_ADVANCED(
    BAR_LIBRARY
    BAR_INCLUDE_DIR
    BAR_DIR
  )
ELSE()
  SET( BAR_DIR "" CACHE STRING
    "An optional hint to a directory for finding `bar`"
  )
ENDIF()

The most salient change is the use of FIND_LIBRARY to find, you guessed it, the library. The optional NAMES argument can be used to supply more names for a library, which is useful if a library ships with different flavours, such as bar_cxx or bar_hl.

Similar to what I wrote above, I am also exporting the single library as BAR_LIBRARIES in order to simplify usage. In the best case, clients can just use

TARGET_LINK_LIBRARIES( example ${BAR_LIBRARIES} )

and the code will continue to work even if, some years down the road, bar suddenly starts shipping with two libraries. Again, I advocate for having a sane and simple standard rather than having to think hard about how to use the darn module.

Other than that, this works exactly the same as the previous example from above!

Things that frequently need doing

Having written almost exhaustively about how to find libraries, I want to end this post with several common tasks. For each of them, I have seen various kinds of weird workarounds, so I would like to point out a more official way.

Versions checks

Sometimes, it is unavoidable to support previous versions of CMake, or detect whether a specific version of a library has been installed. For this purpose, there are special VERSION comparison operators. Do not write your own code to do so but rather do something like this:

IF( CMAKE_CXX_COMPILER_VERSION VERSION_LESS "5.4.1" )
  MESSAGE( STATUS "This compiler version might cause problems" )
ENDIF()

Similarly, there are VERSION_EQUAL and VERSION_GREATER checks. They are tested and bound to work—even when you are comparing packages and their versions.

Detecting an operating system

Your code can be as agnostic with respect to the operating system as you want, but there might still be that one situation where you need to have a way of determining whether your code is being compiled under a certain operating system.

This is easy to accomplish:

IF( APPLE )
  MESSAGE( STATUS "Running under MacOS X" )
# Watch out, for this check also is TRUE under MacOS X because it
# falls under the category of Unix-like.
ELSEIF( UNIX )
  MESSAGE( STATUS "Running under Unix or a Unix-like OS" )
# Despite what you might think given this name, the variable is also
# true for 64bit versions of Windows.
ELSEIF( WIN32 )
  MESSAGE( STATUS "Running under Windows (either 32bit or 64bit)" )
ENDIF()

Detecting a compiler

Sometimes, you need to disable or enable certain things depending on the compiler. Suppose you want to figure out the version of the C++ compiler and its type:

IF( CMAKE_CXX_COMPILER_ID MATCHES "GNU" )
  MESSAGE( STATUS "g++ for the win!" )
  MESSAGE( STATUS ${CMAKE_CXX_COMPILER_VERSION} )
ENDIF()

For LLVM/clang, you can use:

IF( CMAKE_CXX_COMPILER_ID MATCHES "Clang" )
  MESSAGE( STATUS "LLVM, yeah!" )
ENDIF()

Please refer to the documentation for more IDs.

Enabling C++11 or C++14

While it is possible (and also necessary for older versions) to enable C++11 by modifying CMAKE_CXX_FLAGS directly, the standard way involves only two lines:

SET( CMAKE_CXX_STANDARD 11 )
SET( CMAKE_CXX_STANDARD_REQUIRED ON )

This is guaranteed to work with all supported compilers.

Coda

I hope this article convinced you of the power of CMake and of the need for standardizing its usage. You can find the code of the modules, plus some boilerplate CMake code, in the GitHub repository for this post.

Have fun using CMake, until next time!

Update (2018-05-30): Using TARGET_INCLUDE_DIRECTORIES as suggested on HN. Thanks!