RichyHBM

Software engineer with a focus on game development and scalable backend development

My Take on CMake

As of recently I have been picking up C++ again, and having used Premake in the past, I thought it would be a great time to try out a new build management tool. I chose CMake as it seems the more mature of the build management tools as well as having a large adoption rate meaning a lot, if not most, 3rd party libraries have CMake configs readily available.

For anyone that hasn’t heard of or used Premake, CMake or any of the other build tools before, they are essentially tools that can be configured based on your development environment and can then either build your code, or more likely produce project files for other tools such as IDE’s. For example, you could use them to maintain both a Makefile on Linux and a Visual Studio project on Windows.

So to illustrate how CMake works in a small example, I have copied a CMake config I use in my projects. It should be simple enough to understand yet display the capabilities of tools like CMake.

The convention in CMake is to have your config in a file named CMakeLists.txt, at the root of your project. The contents of a sample from one of my projects can be seen below.

cmake_minimum_required (VERSION 3.1)
project (sample)

set (VERSION_MAJOR 0)
set (VERSION_MINOR 1)
set (EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)

execute_process(
  COMMAND git rev-parse --abbrev-ref HEAD
  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
  OUTPUT_VARIABLE GIT_BRANCH
  OUTPUT_STRIP_TRAILING_WHITESPACE
)

execute_process(
  COMMAND git log -1 --format=%h
  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
  OUTPUT_VARIABLE GIT_COMMIT_HASH
  OUTPUT_STRIP_TRAILING_WHITESPACE
)

configure_file (
  "${PROJECT_SOURCE_DIR}/src/Config.h.in"
  "${PROJECT_BINARY_DIR}/Config.h"
)

set (CMAKE_CXX_STANDARD 11)
set (CMAKE_CXX_STANDARD_REQUIRED ON)
set (CMAKE_CXX_EXTENSIONS OFF)

include_directories("${PROJECT_BINARY_DIR}")

file(GLOB_RECURSE SRCS src/**)

add_executable("${PROJECT_NAME}" ${SRCS})

set (CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")

include(no-exceptions)
include(no-rtti)
include(glfw)
include(opengl)
include(wren)

To begin with, we have:

cmake_minimum_required (VERSION 3.1)
project (sample)

These lines set up your project, giving it a name, as well as setting a minimum version of CMake required to run the configuration. This would generally be set to the minimum version of CMake that supplies the features you use in your CMake script. There are other ways around this, for example you can require a lower version of CMake and then check at runtime the version of CMake being used to determine how to accomplish a task.

set (VERSION_MAJOR 0)
set (VERSION_MINOR 1)
set (EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)

This is the convention for setting variables in CMake, these variables then become available to the rest of your script, and as I have used below, also to your code. The third line is setting a CMake specific variable, this sets the location for the built executable to be placed.

execute_process(
  COMMAND git rev-parse --abbrev-ref HEAD
  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
  OUTPUT_VARIABLE GIT_BRANCH
  OUTPUT_STRIP_TRAILING_WHITESPACE
)

execute_process(
  COMMAND git log -1 --format=%h
  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
  OUTPUT_VARIABLE GIT_COMMIT_HASH
  OUTPUT_STRIP_TRAILING_WHITESPACE
)

This is a good example of running an external command from within the CMake process, the above lines make CMake run a couple of git commands to fetch the current branch and commit hash, from within the source directory, and storing their output into a CMake variable.

configure_file (
  "${PROJECT_SOURCE_DIR}/src/Config.h.in"
  "${PROJECT_BINARY_DIR}/Config.h"
)

The configure_file command is used to pass CMake variables into your code, it takes in a specifically formatted code file and produces an output containing the CMake variables. The formatting is in the style of a normal c++ header with consts set to @VARIABLE_NAME@, remember that if the variable is a string it will need to be wrapped in quotes. For example the Config.h.in for this CMake project would look like:

#define VERSION_MAJOR @VERSION_MAJOR@
#define VERSION_MINOR @VERSION_MINOR@
#define GIT_BRANCH "@GIT_BRANCH@"
#define GIT_COMMIT_HASH "@GIT_COMMIT_HASH@"
set (CMAKE_CXX_STANDARD 11)
set (CMAKE_CXX_STANDARD_REQUIRED ON)
set (CMAKE_CXX_EXTENSIONS OFF)

This again sets some CMake built in variables to enable modern C++11 functionality in your code, these variables are only available in the newer versions of CMake but there are other ways of achieving the same result if you require an older version of CMake.

include_directories("${PROJECT_BINARY_DIR}")

The include_directories specifies locations that the compiler should use in order to look for header files, and to resolve #includes within your code. In this case adding PROJECT_BINARY_DIR, a CMake defined location, in order to find Config.h.

file(GLOB_RECURSE SRCS src/**)

add_executable("${PROJECT_NAME}" ${SRCS})

The first line in this section tells CMake to recursively search the src directory looking for all files, adding their location into the SRCS array, after which the SRCS array will be an array of file locations to all files within src. The found files are then used to build the executable, if for example you were only having the 1 source file you could just specify that instead of using a SRCS array.

set (CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")

include(no-rtti)
include(glfw)
include(opengl)

And finally, the way I have my dependencies set up I like to have an individual CMake file for each one, this allows me to easily remove a dependency and keeps my CMake relatively tidy. My project directory looks like:

$ tree
.
├── cmake
│   ├── glfw.cmake
│   ├── no-rtti.cmake
│   ├── opengl.cmake
├── CMakeLists.txt
├── deps
│   └── dependencies as git submodules
└── src
    └── source files

With each cmake file in the cmake directory setting up a piece of functionality or a dependency.

set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)

add_subdirectory("${PROJECT_SOURCE_DIR}/deps/glfw")
target_link_libraries("${PROJECT_NAME}" glfw)

The GLFW cmake script simply sets some GLFW specific variables and then includes the existing CMake config in the GLFW project, the variables are the same ones you would manually set if building GLFW from source, as I don’t want the tests, examples or docs I disable these. Finally the target_link_libraries specifies that glfw should be linked to the project.

if (WIN32)
  if (MSVC)
    set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /GR-" )
  elseif (CMAKE_COMPILER_IS_GNUCXX)
    set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti" )
  else ()
    message (SEND_ERROR "You are using an unsupported Windows compiler! (Not MSVC or GCC)")
  endif ()
elseif (UNIX)
  set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti" )
  set(CMAKE_XCODE_ATTRIBUTE_GCC_ENABLE_CPP_RTTI "NO")
else ()
  message (SEND_ERROR "You are on an unsupported platform! (Not Win32 or Unix)")
endif ()

The no-rtti config looks at what OS and what compiler CMake is being run for and sets specific flags for the compiler to disable run-time type identification. As CMake does not include a generic way of disabling this via one of its built in variables, this needs to be done manually for each supported compiler.

find_package(OpenGL REQUIRED)

include_directories("${OPENGL_INCLUDE_DIR}")
target_link_libraries("${PROJECT_NAME}" "${OPENGL_gl_LIBRARY}")

Finally the opengl cmake script is a good example of finding an external library using CMakes built in search ability, find_package will search a set of predefined locations for FindXXX files, these tell CMake how to add specific libraries to a CMake project allowing you to use them in your own projects. If you have manually added a library that includes FindXXX files and these have not been added to one of the CMake locations, you may need to specify additional locations to be searched. Luckily OpenGL libraries are included by default with CMake, so adding them to your project is a very easy task.