Intro to CMake

5 Sep 2017

Building C/C++ projects is clearly a non-trivial problem given the many, many, different options available for helping you do that. CMake is one of the more popular systems, and one that I've been meaning to look at for a while so if you're wondering what a CMakeList is and want a basic introduction, then read on!

What does CMake do

First things first: CMake doesn't actually compile anything, it just generates build scripts for you (that you can then run to do the actual compiling). One benefit of this is that you can define your build once and then let cmake generate builds for GNU Make, Visual Studio, Ninja, or even Eclipse (to name a few), which is great for making it easy to compile on different platforms.

One convenient benefit of using CMake (especially given its popularity) is that it makes compiling your code extraodinarily easy if all of its dependencies also use/support CMake (and as far as I can tell shouldn't get that much more complicated if they don't, but thats a topic for another time).

So how do I access this convenience?

Consider the following example code:

// main.cpp
#include <stdio.h>

#include "GLFW/glfw3.h"

int main()
{
    if(!glfwInit())
    {
        printf("Error: Unable to initialize GLFW\n");
        return -1;
    }

    GLFWwindow* window = glfwCreateWindow(640, 480, "Hello World", nullptr, nullptr);
    if(!window)
    {
        printf("Error: Unable to create GLFW window\n");
        glfwTerminate();
        return -1;
    }

    glfwMakeContextCurrent(window);
    while(!glfwWindowShouldClose(window))
    {
        glClear(GL_COLOR_BUFFER_BIT);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

As a program, this isn't very interesting - it just opens a window that does nothing until you close it - but we aren't here for the code, we here for the build and this build demonstrates the most critical things that we need from our compilation process. The above code needs includes and libraries from a third-party library, as well as the local OpenGL libraries. We're going to wrangle CMake into giving them to us with the following CMakeLists.txt file:

cmake_minimum_required(VERSION 3.2)
project(Tryout_Cmake)

set(CMAKE_BINARY_DIR ${CMAKE_SOURCE_DIR}/bin)
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
add_executable(Tryout_Cmake ${PROJECT_SOURCE_DIR}/main.cpp)

include(FindOpenGL)
target_link_libraries(Tryout_Cmake ${OPENGL_gl_LIBRARY})

include(ExternalProject)
ExternalProject_Add(
    glfw
    GIT_REPOSITORY git@github.com:glfw/glfw.git
    GIT_TAG 3.2.1
    CMAKE_ARGS -DGLFW_BUILD_EXAMPLES=OFF
               -DGLFW_BUILD_TESTS=OFF
               -DGLFW_BUILD_DOCS=OFF
               -DGLFW_VULKAN_STATIC=OFF
               -DCMAKE_INSTALL_PREFIX=${CMAKE_SOURCE_DIR}/thirdparty/glfw
    PREFIX glfw
    UPDATE_COMMAND ""
    )

set(GLFW_LIB ${CMAKE_SOURCE_DIR}/thirdparty/glfw/lib/${CMAKE_STATIC_LIBRARY_PREFIX}glfw3${CMAKE_STATIC_LIBRARY_SUFFIX})
set(GLFW_INCLUDE ${CMAKE_SOURCE_DIR}/thirdparty/glfw/include)

add_dependencies(Tryout_Cmake glfw)
include_directories(Tryout_Cmake ${GLFW_INCLUDE})
target_link_libraries(Tryout_Cmake ${GLFW_LIB})

Let's break that down a bit:

cmake_minimum_required(VERSION 3.2)
project(Tryout_Cmake)

This tells cmake that we're building a project called “Tryout_Cmake” and that the build requires at least cmake 3.2. We'll use the Tryout_Cmake identifier later to add things to this build project.

set(CMAKE_BINARY_DIR ${CMAKE_SOURCE_DIR}/bin)
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
add_executable(Tryout_Cmake ${PROJECT_SOURCE_DIR}/main.cpp)

Next we tell cmake where we want the binaries to be stored, and we add an executable by specifying a list of source files to create it from. Note that with more source files you could split the add_executable arguments up across multiple lines (in the same way that we'll do for CMAKE_ARGS in ExternalProject_Add below).

include(FindOpenGL)
target_link_libraries(Tryout_Cmake ${OPENGL_gl_LIBRARY})

Here we're telling cmake to include the FindOpenGL module, which is a script that ships with cmake and detects both the OpenGL and GLU libraries (as well as setting flags to check if they were found). We then tell cmake that we want our Tryout_Cmake project to link with the OpenGL libraries that it found.

include(ExternalProject)

We'll use the ExternalProject module to include dependencies. Note that its also possible to include dependencies by downloading their source and then using the add_subdirectory command to tell cmake to look for (and run) a CMakeLists.txt in that directory as well. That works a bit differently though, and I think its cleaner to use ExternalProject so we'll stick to that for now.

ExternalProject_Add(
    glfw
    GIT_REPOSITORY git@github.com:glfw/glfw.git
    GIT_TAG 3.2.1
    CMAKE_ARGS -DGLFW_BUILD_EXAMPLES=OFF
               -DGLFW_BUILD_TESTS=OFF
               -DGLFW_BUILD_DOCS=OFF
               -DGLFW_VULKAN_STATIC=OFF
               -DCMAKE_INSTALL_PREFIX=${CMAKE_SOURCE_DIR}/thirdparty/glfw
    PREFIX glfw
    UPDATE_COMMAND ""
    )

Now things are getting interesting. ExternalProject_Add will get the source for a project from the internet (or wherever you say it is) and build it as part of your compilation. Here we've added a project that we call “glfw” (this is the identifier that we'll use later on to reference it), and we've gotten it from the GLFW github repo (specifically getting version 3.2.1).

CMAKE_ARGS are simply the arguments that will get passed to cmake when it runs for the GLFW project and PREFIX specifies where to place these files (in this case in a directory called “glfw”). The CMAKE_INSTALL_PREFIX define warrants mentioning because it can be very confusing that we need to define this when ExternalProject_Add has an INSTALL_DIR option. CMAKE_INSTALL_PREFIX specifies a prefix for the path into which cmake will install the artifacts from the glfw build. This lets us define where we want the libraries and include files to go, which makes later commands much cleaner. As far as I can tell, the INSTALL_DIR argument just passes through the value we give as a variable called “INSTALL_DIR” and it's up to that project to use it (and in this case that doesn't happen, so we don't use it).

ExternalProject_Add has UPDATE_COMMAND and INSTALL_COMMAND options which let you override what command gets executed when that project should be updated/rebuilt or installed. In this case GLFW defines a suitable installation procedure (it creates the lib and include directies mentioned above) so we leave that as-is. However, the default update command involves pulling changes from git, which I don't want it to do because its a few extra seconds on the build time (which is pretty pointless because we've specified which commit to use).

set(GLFW_LIB ${CMAKE_SOURCE_DIR}/thirdparty/glfw/lib/${CMAKE_STATIC_LIBRARY_PREFIX}glfw3${CMAKE_STATIC_LIBRARY_SUFFIX})
set(GLFW_INCLUDE ${CMAKE_SOURCE_DIR}/thirdparty/glfw/include)

We're just setting variables here to make our lives easier later. The library prefix and suffix variables are defined by cmake to let us refer to library files in a cross-platform-safe manner (for example by expanding the suffix to “.lib” on Windows and “.a” on Linux).

add_dependencies(Tryout_Cmake glfw)
include_directories(Tryout_Cmake ${GLFW_INCLUDE})
target_link_libraries(Tryout_Cmake ${GLFW_LIB})

Finally, this just tells cmake that the glfw project is a dependency for the Tryout_Cmake project (so it'll get built in the correct order), and adds the appropriate include directories and libraries to link against.

That's it!

You can now create a build script by placing CMakeLists.txt next to main.cpp, creating a “build directory” and executing “cmake ..” from inside it (to point to your CMakeLists.txt file). This will generate build scripts for whatever your platform's default build tool is. You can also execute the build via cmake with “cmake –build .” (from inside the build directory, so that . refers to where the generated scripts are).

Of course this guide has skipped over a huge amount of detail, but hopefully it helped you get started. If you want to learn more, I would encourage you to read over some other tutorials on cmake (the cmake documentation is also pretty good).

Now go forth and build!