DEV Community

loading...

Starting a journey with CMake

Alexandre Plateau
C++ fanatic, video game maker, working on my own scripting language
・4 min read

Very often, I have to help friends and colleagues to build a C or C++ project, and my solution is always the same: "use CMake!", but their documentation isn't pretty good so we have to spend a few hours on stackoverflow to get it to work for their specific use cases.

This blog post will try to provide basic CMake knowledge and how to produce a nice and working CMakeLists.txt.

Introduction: what's CMake? Why should I use it?

CMake is called a project generator. It can generate Makefiles, Visual Studio projects, ninja files... In a nutshell it generates files used to compile a project.

It's awesome in a way because you only have a single CMakeLists.txt for your project, and then run cmake on this file to generate a project for the current platform. It means that we don't have to bother anymore with creating and maintaining a lot of build files to compile your code on Linux, Windows, MacOS and many more operating systems.

Compiling some files into an executable/library

Let's go through a basic CMakeLists.txt:

# using the latest version currently
cmake_minimum_required(VERSION 3.18)

# the name of our project is put as an argument of this command
project(project_name)

# configure file takes a file using CMake variables between @@
# and output another file with the defined values
# example in Constants.hpp.in: #define PROJECT_NAME "@PROJECT_NAME@"
configure_file(
    ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/Constants.hpp.in
    ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/Constants.hpp
)

# every header file from include/ or ext/ will be used like this:
# #include <lib/file.hpp>
# folder structure:
# ext/
#     lib/
#         file.hpp
include_directories(
    ${PROJECT_SOURCE_DIR}/ext  # ext libs
    ${PROJECT_SOURCE_DIR}/include  # includes for the project
)

# fetching all .cpp
file(GLOB_RECURSE SOURCE_FILES
    ${PROJECT_SOURCE_DIR}/src/*.cpp

    # add this if there are .c/.cpp to compile in the ext folder
    ${PROJECT_SOURCE_DIR}/ext/*.cpp
    ${PROJECT_SOURCE_DIR}/ext/*.c
)

# create an executable named after the project's name
# built from the source files we gathered previously
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
# link external libraries, if any
target_link_libraries(${PROJECT_NAME} PUBLIC lib_name1 lib_name2)

# set target properties for C++ projects to use C++17
set_target_properties(
    ${PROJECT_NAME}
    PROPERTIES
        CXX_STANDARD 17
        CXX_STANDARD_REQUIRED ON
        CXX_EXTENSIONS OFF
)
Enter fullscreen mode Exit fullscreen mode

Basically what it does is:

  1. declares a project named project_name, name that will be available in CMake by using the variable PROJECT_NAME
  2. configure a project file with CMake variables, useful to set parameters like the project's version, debug options
  3. add every header file under include/ and ext/ to the includes path
  4. list all the .cpp files of the project under src/ and all the .c/.cpp files from ext/
  5. compile the project as an executable by using the list of source files we fetch
  6. link libraries to our project (eg. opengl, X11, fs...)
  7. set the project properties to request at least C++ 17

Launching CMake and compiling

Now that we have a nice CMakeLists.txt, we need to tell CMake to use it:

cmake -Bbuild -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=g++-8
Enter fullscreen mode Exit fullscreen mode

Here I gave the following options:

  • -Bbuild to tell CMake to output its files into a build/ folder
  • -DCMAKE_BUILD_TYPE can take either Debug or Release with G++, and also RelWithDebInfo with MSVC, it's optional, default value is Debug. If you give this parameter one time and then re-run CMake, its value will be cached and re-used
  • -DCMAKE_CXX_COMPILER is also optional, but might be useful if you have multiple compilers on your computer and want to use a specific one

Another useful option is -G "generator name" if you want to generate Ninja files, Unix makefile...

My favorite on Windows is -G "Visual Studio <version> Win64" to force the generation of the 64 bits project.

Then we build a project by using

cmake --build build --config Debug
Enter fullscreen mode Exit fullscreen mode

The --build option tells CMake where its generated files are, and the --config option tells CMake in which mode the project shall be generated (this is useful only with multi-target compilers like MSVC, which thus ignores -DCMAKE_BUILD_TYPE).

With multi-target compilers, the output files will be under build/<config>/<project name>, with single-target compilers like G++, under build/<project name>.

Including a project B in a project A

each with its own CMakeLists.txt

When dealing with a large codebase, we often need to use external dependencies which already come with their own CMakeLists.txt. The goal is to make use of this power in a very lines to avoid rewriting everything.

add_subdirectory("${PROJECT_SOURCE_DIR}/submodules/nice-cmake-project")

# ...
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
target_link_libraries(${PROJECT_NAME} PUBLIC NiceCmakeProjectLib)
Enter fullscreen mode Exit fullscreen mode

And we're done. Here what happens is that CMake will register the subdirectory as a separate CMake project (I assumed this project was generating a lib by using add_library instead of add_executable, name NiceCmakeProjectLib).

Conclusion

A few tips before leaving:

  • when adding new files to a project, you need to regenerate the CMake files by using the command cmake -Bbuild. The previous settings sent by using -D<name>=<value> are kept and re-used
  • to change a previous CMake setting sent using -D<name>=<value>, just add -D<name>=<new value> again when regenerating CMake files
  • if you have conflicts with external libraries when compiling, check if everything is compiled using the same set of instructions (32 bits or 64 bits)

Discussion (2)

Collapse
pgradot profile image
Pierre Gradot

Hello,

One small remark: prefer target_include_directories() over include_directories(). As a rule of thumb, prefer the functions with the target_ prefix. Why? If you have only one exectuable, is makes no difference at all. But as soon as you have several executables (or libraries => several targets), you may need per target configurations.

One important warning: don't use GLOB_RECURSE. It may seems easy at first glance, but experience has shown it is painfull on the long term. The two main issues I faced:

  1. You need to exclude some files from a target ; or you add a library and don't want to build all its files.
  2. you get an update from your CVS (git, svn, whatever), new files are added, but you don't rerun CMake. You compile your project and get unexpected link errors and waste time before thinking about reloading your project.

When starting a project from scratch, it is easy to add files one by one. When you already have a large codebase, you can use bash trick to list your files ; )

Collapse
dzintars profile image
Dzintars Klavins

Currently diving into Bazel as it supports multiple languages and could unify my full stack workflow.

Forem Open with the Forem app