Skip to content

Instantly share code, notes, and snippets.

@mbinna
Last active July 14, 2025 22:11
Show Gist options
  • Save mbinna/c61dbb39bca0e4fb7d1f73b0d66a4fd1 to your computer and use it in GitHub Desktop.
Save mbinna/c61dbb39bca0e4fb7d1f73b0d66a4fd1 to your computer and use it in GitHub Desktop.
Effective Modern CMake

Effective Modern CMake

Getting Started

For a brief user-level introduction to CMake, watch C++ Weekly, Episode 78, Intro to CMake by Jason Turner. LLVM’s CMake Primer provides a good high-level introduction to the CMake syntax. Go read it now.

After that, watch Mathieu Ropert’s CppCon 2017 talk Using Modern CMake Patterns to Enforce a Good Modular Design (slides). It provides a thorough explanation of what modern CMake is and why it is so much better than “old school” CMake. The modular design ideas in this talk are based on the book Large-Scale C++ Software Design by John Lakos. The next video that goes more into the details of modern CMake is Daniel Pfeifer’s C++Now 2017 talk Effective CMake (slides).

This text is heavily influenced by Mathieu Ropert’s and Daniel Pfeifer’s talks.

If you are interested in the history and internal architecture of CMake, have a look at the article CMake in the book The Architecture of Open Source Applications.

General

Use at least CMake version 3.0.0.

Modern CMake is only available starting with version 3.0.0.

Treat CMake code like production code.

CMake is code. Therefore, it should be clean. Use the same principles for CMakeLists.txt and modules as for the rest of codebase.

Define project properties globally.

For example, a project might use a common set of compiler warnings. Defining such properties globally in the top-level CMakeLists.txt file prevents scenarios where public headers of a dependent target causing a depending target not to compile because the depending target uses stricter compiler options. Defining such project properties globally makes it easier to manage the project with all its targets.

Forget the commands add_compiler_options, include_directories, link_directories, link_libraries. Those commands operate on the directory level. All targets defined on that level inherit those properties. This increases the chance of hidden dependencies. Better operate on the targets directly.

Get your hands off CMAKE_CXX_FLAGS.

Different compilers use different command-line parameter formats. Setting the C++ standard via -std=c++14 in CMAKE_CXX_FLAGS will brake in the future, because those requirements are also fulfilled in other standards like C++17 and the compiler option is not the same on old compilers. So it’s much better to tell CMake the compile features so that it can figure out the appropriate compiler option to use.

Don’t abuse usage requirements.

As an example, don’t add -Wall to target_compile_options, since it is not required to build depending targets.

@patrick-fromberg
Copy link

@rockerbacon, I have written a script that checks for new files and touches the corresponding CMakeLists file. Quoting Linus Thorwald freely I would say: "people who do not use GLOB are smart but ugly"

@wuziq
Copy link

wuziq commented Feb 2, 2020

"Using a library defined in the same CMake tree should look the same as using an external library."

Pfiefer's slides don't explain this point. I tried to find_package() on a target I defined earlier in the cmake tree, but I just get this:

By not providing "Findmytarget.cmake" in CMAKE_MODULE_PATH this
project has asked CMake to find a package configuration file provided by
"mytarget", but CMake did not find one.

On the other hand, if I define an ALIAS, then I don't need to use find_package():

mytarget/CMakeLists.txt:

add_library(mytarget STATIC "")
add_library(foo::mytarget ALIAS mytarget)
...

client/CMakeLists.txt:

add_library(client SHARED client.cpp)
target_link_libraries(client PRIVATE foo::mytarget)

No find_package() is needed. So what am I missing?

@Xeverous
Copy link

Xeverous commented Feb 2, 2020

You should always add an alias, even for libraries you have in the same project. ALIAS targets can't be modified which increases safety and allows the namespace-like target_link_libraries syntax as any external library.

@wuziq
Copy link

wuziq commented Feb 2, 2020

You should always add an alias, even for libraries you have in the same project. ALIAS targets can't be modified which increases safety and allows the namespace-like target_link_libraries syntax as any external library.

This is exactly what I've done, which makes me wonder why I should bother with find_package(). Is it just to have consistent target_link_libraries() usage between external libraries and internal targets?

@Xeverous
Copy link

Xeverous commented Feb 3, 2020

No idea.

@Xeverous
Copy link

Can someone give a practical example of it causing problems?
I still feel like avoid GLOB achieves nothing. The build process is still the same when a new file is added: Rerun CMake then build.
GLOB will just create a list of files automatically instead of the list being kept manually by developers and for me that's a plus, not a minus. The glob doesn't get added to the build configuration even on build systems which support it, such as GNU Make, so in the end all generated rules are also the exact same.

@rockerbacon CMake documentation explicitly discourages the GLOB:

We do not recommend using GLOB to collect a list of source files from your source tree. If no CMakeLists.txt file changes when a source is added or removed then the generated build system cannot know when to ask CMake to regenerate. The CONFIGURE_DEPENDS flag may not work reliably on all generators, or if a new generator is added in the future that cannot support it, projects using it will be stuck. Even if CONFIGURE_DEPENDS works reliably, there is still a cost to perform the check on every rebuild.

@JAE-UH
Copy link

JAE-UH commented Feb 25, 2020

Not using GLOB with CONFIGURE_DEPENDS is walking to school ten miles barefoot in snow uphill both ways.

@bjcosta
Copy link

bjcosta commented Mar 4, 2020

* find_package

I looked at the linked pages in the slides (Page 34):
https://github.com/boostcon/cppnow_presentations_2017/blob/master/05-19-2017_friday/effective_cmake__daniel_pfeifer__cppnow_05-19-2017.pdf

It shows he overrides the find_package() with a new macro that checks for a local package first and if finds it then skips the actual _find_package() call making it a no-op. This makes using local/remote packages look identical, but I don't like the idea of replacing the normal find_package() behavior but I do like that local and remote packages look identical.

@Xeverous
Copy link

Xeverous commented Mar 4, 2020

I find it much better and more stable for many libraries to use pkg_check_modules(libname REQUIRED IMPORTED_TARGET libname). Kinda bad CMake doesn't use it by default in find_package(). I then add an option for each library how it should be discovered.

@danielmevi
Copy link

danielmevi commented Jun 16, 2020

Is there a recommendation to incorporate "gcovr" as part of a target? Or is an external script a better option?

@simonspa
Copy link

Is there a recommendation to incorporate "gcovr" as part of a target? Or is an external script a better option?

INCLUDE("cmake/CodeCoverage.cmake")
APPEND_COVERAGE_COMPILER_FLAGS()
SET(COVERAGE_GCOVR_EXCLUDES "${PROJECT_SOURCE_DIR}/to_be_excluded" "${PROJECT_BINARY_DIR}")
SETUP_TARGET_FOR_COVERAGE_GCOVR(NAME coverage_gcovr
                                EXECUTABLE ctest
                                DEPENDENCIES yourTargets)
SETUP_TARGET_FOR_COVERAGE_GCOVR_HTML(NAME coverage_gcovr_html
                                EXECUTABLE ctest
                                DEPENDENCIES yourTargets)

@L4stR1t3s
Copy link

"Don't do this. Don't do that."

Just tell me what I should do!

I have 20 years of experience as a software developer and I've never come across a technology that is harder to learn than CMake, just because it's community seems to not understand how to best communicate things to NEW users.

Any resource on CMake I've come across fails to provide decent examples, assumes prior knowledge, ...

Is an easy-to-understand, simple example and explanation of anything related to CMake really too much to ask for?

People are badmouthing CMake for various reasons, and it's probably not perfect, but the real issue is the lack of ability to teach others in its community if you ask me.

@zchrissirhcz
Copy link

@L4stR1t3s You may have a look at cmake-cookbook:
https://github.com/dev-cafe/cmake-cookbook

@blackliner
Copy link

blackliner commented Sep 28, 2020

@carlosgalvezp

Hi,

Great post about modern CMake! I'm having a bit of trouble understanding the following rules, they look contradicting to me:

-Define project properties globally.
-Forget the commands add_compile_options

How are we supposed to set properties globally if not via add_compile_options? Should we create an INTERFACE target that all other targets must link against? This forces the developers to remember to link against the "global options target" every time they create a new target. It would be better that this is enforced automatically.

Thanks!

I would also be interested in how to follow both of these rules. Currently, we set the following in our toolchain files:

SET (
  COMPILER_WARNING_SWITCHES
  "-Wall -Wextra -Wwrite-strings -Wunreachable-code -Wpointer-arith -Winit-self -Wredundant-decls -Wpedantic -fdiagnostics-color=always -Wno-error=deprecated-declarations -pedantic-errors"
)
SET (
  CMAKE_C_FLAGS
  "${COMPILER_WARNING_SWITCHES}"
  CACHE STRING "gcc c flags" FORCE)
SET (
  CMAKE_CXX_FLAGS
  "${COMPILER_WARNING_SWITCHES}"
  CACHE STRING "gcc cxx flags" FORCE)

@serpent7776
Copy link

@ccossou
Copy link

ccossou commented Nov 17, 2020

I have a question regarding custom variable in project commands.
I want to install header for a library that I need to import later using find_packages. Header are already defined in target_sources PUBLIC section.

target_sources(toto_shared
    PRIVATE
        toto_shared.cpp
    PUBLIC
        "$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/toto_shared.h>"
    )

but does that mean I need to duplicate the list of header in the INSTALL/FILES to make sure they are effectively copied?

install(FILES ${CMAKE_CURRENT_LIST_DIR}/subdir/toto_shared.h DESTINATION include)

@Talkless
Copy link

I would also be interested in how to follow both of these rules.

Same. It feels cumbersome to remember to link some INTERFACE "library" for all sub-directory libraries your application has.

@Qix-
Copy link

Qix- commented Dec 11, 2020

s/will brake in the future/will break in the future/

@davidHysom
Copy link

Thanks for some valuable information. However, re, "Getting Started," please don't refer me to an hour long video; I'd rather spend ten minutes reading than watching for an hour. Reading is FAR MORE information dense than videos.

@neoblizz
Copy link

neoblizz commented Feb 8, 2021

Hey guys, I need some explanation on the following, I am doing the following to set CUDA properties for a INTERFACE library;

set_target_properties(foo
    PROPERTIES
        CUDA_ARCHITECTURES 61
)

And later, I want the targets that link to this library to automatically inherit the CUDA_ARCHITECTURES, but they seem to default to an initial value of 52 in my case. How can I automatically inherit the properties in the core library in my targets later on?

I have managed to do the following, but it doesn't look like an elegant solution as I will have to do that for every single property and target;

get_target_property(FOO_CUDA_ARCHITECTURES foo CUDA_ARCHITECTURES)
set_target_properties(bar 
    PROPERTIES 
        CUDA_ARCHITECTURES ${FOO_CUDA_ARCHITECTURES}
)

Flags are automatically inherited, but I don't see properties set in the way above to function the same way.

@Xeverous
Copy link

Xeverous commented Feb 8, 2021

@neoblizz Target properties are specific and their PUBLIC, PRIVATE, INTERFACE keyword is already encoded in their name. For example: INCLUDE_DIRECTORIES and INTERFACE_INCLUDE_DIRECTORIES - target_include_directories(<target> <keyword> dirs...) sets the first one if the keyword is PRIVATE, sets the second if the keyword is INTERFACE and sets both if the keyword is PUBLIC.

Unfortunately, I could not find any property named similarly to CUDA_ARCHITECTURES. Apparently it is always interface or always private. The documentation only mentions that the default is affected by CMAKE_CUDA_ARCHITECTURES variable when the target is created.

@neoblizz
Copy link

neoblizz commented Feb 9, 2021

Unfortunately, I could not find any property named similarly to CUDA_ARCHITECTURES. Apparently it is always interface or always private. The documentation only mentions that the default is affected by CMAKE_CUDA_ARCHITECTURES variable when the target is created.

Thank you! That answer helps a lot, I found out that it is not in the list of properties that get inherited, yet. But, maybe with future updates, it will be in one.

@JohelEGP
Copy link

JohelEGP commented Sep 7, 2021

Define the macro find_package to wrap the original find_package command (now accessible via _find_package).

This was recently clarified to be bad practice. I can't find the source. Something about this being an implementation detail. I think the author himself clarified it.

@iago-lito
Copy link

Don’t use target_include_directories with a path outside the component’s directory.
Using a path outside a component’s directory is a hidden dependency. Instead, use target_include_directories to propagate include directories as usage requirements to depending targets via target_link_directories.

I just.. don't understand this alternative written in italics :( Is there a more explicit rephrasing available?

@friendlyanon
Copy link

For anyone interested in how to definitively do CMake right, you are welcome to take https://github.com/friendlyanon/cmake-init for a spin.


Define the macro find_package to wrap the original find_package command (now accessible via _find_package).

This was recently clarified to be bad practice. I can't find the source. Something about this being an implementation detail. I think the author himself clarified it.

Not only is it bad practice, but it's an undocumented debug feature left in CMake. Doing this is effectively UB. More details here https://crascit.com/2018/09/14/do-not-redefine-cmake-commands/

@mbinna
Copy link
Author

mbinna commented Jun 2, 2022

Thank you for all your comments. 🎉 When I created this Gist, a good reference book that teaches modern CMake didn't exist (at least not to my knowledge). Nowadays, I'd recommend the book Professional CMake: A Practical Guide by @craigscott-crascit. It's comprehensive and the quality is outstanding.

@qlibp
Copy link

qlibp commented May 11, 2023

Don’t use target_include_directories with a path outside the component’s directory.
Using a path outside a component’s directory is a hidden dependency. Instead, use target_include_directories to propagate include directories as usage requirements to depending targets via target_link_directories.

I just.. don't understand this alternative written in italics :( Is there a more explicit rephrasing available?

Not quite understand the reasoning behind this. Can anyone explain it a little bit?

@rpavlik
Copy link

rpavlik commented May 16, 2023

It's unexpected to have a directory include a dependency from above it, directly. Make an interface target at the correct level to not need .. in the include path, then add that in the list for target_link_libraries.

Basically, if you take just a subdirectory of a repo, you should be able to easily see based on its cmakelists.txt the things you need from elsewhere.

It's kind of an opinion thing but probably makes it more readable.

@zeroxia
Copy link

zeroxia commented Jun 23, 2023

Don’t use target_include_directories with a path outside the component’s directory.
Using a path outside a component’s directory is a hidden dependency. Instead, use target_include_directories to propagate include directories as usage requirements to depending targets via target_link_directories.

I just.. don't understand this alternative written in italics :( Is there a more explicit rephrasing available?

I think this is a mistake. The last target_link_directories should be target_link_libraries.

From what I understand, if you need to have an include path outside of the component's directory, there should be a separate target that declares that path as an interface include directory. Then the depending target should "link" to that new target to get its usage requirement (i.e., the include path).

@zchrissirhcz
Copy link

zchrissirhcz commented Jun 23, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment