Note: This answer is long because the innocent-looking question and the multiple topics it covers are deceptively broad, and the question's intended audience is for beginners (people new to CMake). Trust me- this answer could have been a lot longer than it is.
What CMake is
Somehow I am totally confused by how CMake works. Every time I think that I am getting closer to understanding how CMake is meant to be written, it vanishes in the next example I read.
CMake is a buildsystem generator. You write a configuration to describe a buildsystem (a project and its build targets and how they should be built) (and optionally, tested, installed, and packaged). You give the CMake program that configuration and tell it what kind of buildsystem to generate, and it generates it (providing that that buildsystem is supported). Such supported buildsystems include (but are not limited to): Ninja, Unix Makefiles, Visual Studio solutions, and XCode.
The buildsystem is the thing that understands (because you instruct it on) how your project needs to be built- what source files it has, and how those source files should get compiled into object files, and how those object files should get linked together into executables or dynamic/shared or static libraries.
The advantages of using CMake should not be understated. If you want to support multiple buildsystems (which is especially common for cross-platform library authors who want to allow their users to make their own buildsystem choices and save those users the work of writing those buildsystem configurations), it is a lot less work to write one CMake configuration than N
configurations for N
different buildsystems in their own languages and ways of doing things.
Actually, CMake supports other programming languages and their buildsystems than just C and C++, but since you're just asking about C++, I'll leave that out.
Tricks to avoid CMake when using CMake (don't try at home, kids)
All I want to know is, how should I structure my project, so that my CMake requires the least amount of maintainance in the future. For example, I don't want to update my CMakeList.txt when I am adding a new folder in my src tree, that works exactly like all other src folders.
Contrary to what you think, a small degree of having to modify CMakeLists.txt files whenever you add new source files is a very small cost in return for all the benefits of what CMake can provide, and trying to circumvent that cost has its own costs that become problems at scale. That's why those circumvention techniques (namely, file(GLOB)
) that people often use are discouraged for use by the maintainers of CMake, and by various long-time users of CMake on Stack Overflow, as seen here and here.
When you have a small project with a few files, it's not very cluttery to list out those few source files in your CMakeLists.txt files, and when you have big projects with lots of source files, you're still better off listing out the source files explicitly for the reasons previously listed in the linked resources. In short, it's for your own good and sanity. Don't try to fight it.
A basic CMake configuration
This is how I imagine my project's structure, but please this is only an example. If the recommended way differs, please tell me, and tell me how to do it.
myProject
src/
module1/
module1.h
module1.cpp
module2/
[...]
main.cpp
test/
test1.cpp
resources/
file.png
bin
[execute cmake ..]
If you're looking for a convention to use for project filesystem layout, one well-specified layout spec is The Pitchfork Layout Convention (PFL), which was written based on conventions that emerged in the C++ community over time.
Here's what this project layout might look like following the PFL spec with split headers and split tests:
myProject/
CMakeLists.txt
libs/
module1/
CMakeLists.txt
include/myProject_module1/
module1.h
src/myProject_module1/
module1.cpp
tests/
CMakeLists.txt
test1.cpp
data/
file.png
module2/
CMakeLists.txt
src/module2
main.cpp
[...]
build/
Note: it doesn't have to be include/myProject_module1/
- it can just be include/
, but adding the myProject_module1/
makes the #includes
for each module "namespaced" so that two modules (even if one is from a separate project) can have header files of the same name, and that those headers can all be included in one source file without clashing or ambiguities, like so:
#include <myProject_module1/foo.h>
#include <myProject_module2/foo.h>
#include <yourProject_module1/foo.h>
// Look, ma! No clashing or ambiguities!
Since you allowed so in your question, for the rest of the config code examples, I will use the above PFL layout.
myProject/CMakeLists.txt:
cmake_minimum_required(VERSION 3.25)
# ^choose a CMake version to support (its own can of worms)
# see https://alexreinking.com/blog/how-to-use-cmake-without-the-agonizing-pain-part-1.html
project(example_project
VERSION 0.1.0 # https://semver.org/spec/v0.1.0.html
DESCRIPTION "a simple CMake example project"
# HOMEPAGE_URL ""
LANGUAGES CXX
)
if(EXAMPLE_PROJECT_BUILD_TESTING)
enable_testing()
# or alternatively, `include(CTest)`, if you want to use CDash
# https://cmake.org/cmake/help/book/mastering-cmake/chapter/CDash.html
endif()
add_subdirectory(libs/module1)
add_subdirectory(libs/module2)
# ^I generally order these from lower to higher abstraction levels.
# Ex. if module1 uses module2, then add_subdirectory it _after_ module2.
# That allows doing target_link_libraries inside moduleN/CmakeLists.txt
# instead of here (although that's equally fine. a matter of preference).
The cmake_minimum_required()
command is where you declare what version of CMake is required to parse and run your CMake configuration. The project()
command is where you declare the basic project information, the enable_testing()
command enables testing for the current directory and below, and each add_subdirectory
command changes the "current directory", creates a new subdirectory "scope", and parses the CMakeLists.txt file found at that path.
Here are the docs for cmake_minimum_required()
, project()
, enable_testing()
and add_subdirectory()
.
myProject/libs/module1/CMakeLists.txt (and similar for module2):
# if module1 is a library, use add_library() instead
add_executable(module1
src/module1.cpp
)
target_compile_features(okiidoku PUBLIC cxx_std_20) # or whatever language standard you are using
target_include_directories(module1 PUBLIC include)
if(EXAMPLE_PROJECT_BUILD_TESTING)
add_subdirectory(tests)
endif()
The add_executable()
command is how you declare a new executable target to be built. The add_library
is similar, but is how you declare a library target to be built, where the library can be linked to via the target_link_libraries()
command.
The target_compile_features()
command is how you tell CMake what flag to pass to the compiler to pick a C++ language standard to use, and the target_include_directories()
command is how you tell CMake what include directories to specify when compiling implementation files. PUBLIC
means that the target itself will need that include directory for its #include
s, and that other dependent targets that link to that target will as well. If dependent targets don't need it, use PRIVATE
. If only dependent targets need it, use INTERFACE
. PUBLIC
, PRIVATE
, and INTERFACE
are relevant for library targets, but I'm not aware of them having any use for executable targets (since I'm pretty sure nothing ever depends on executables linkage-wise), so either PUBLIC
or PRIVATE
should work when specifying include directories for executable targets.
Here are the docs for add_library()
, add_executable()
, target_compile_features()
target_include_directories()
, and target_link_libraries()
.
If you want to learn more about any CMake command (listed in cmake --help-command-list
), just do cmake --help <command>
, or google cmake command <command>
.
In terms of the tests folder, you can use CMake's testing support without using any C++ testing libraries of frameworks, or use it with a testing library or framework that supports CMake. To read more about CMake and testing in general, read the chapter in the Mastering CMake book. It's too much material to cover in the form of a Stack Overflow answer.
Installation is also its own can of worms (more on that later), so since the question didn't ask for it, I think it's better to leave out of the answer post to avoid an super long post and scope creep. Again, see the dedicated chapter in the Mastering CMake book. One thing to especially watch out for with installation is making sure you make the install package relocatable.
In terms of a very very simple project, that's all you need, although of course, there can be much more configuration to do based on your project's specific needs, but you can burn yourself on those bridges when you get there.
Compile options / flags is also its own can of worms and better covered in separate Q&A posts.
If you want to start using dependencies, I suggest reading the official "Using Dependencies" guide.
If you really have a lot of source files, and your myProject/src/module1/CMakeLists.txt file starts to get unwieldy because of all the lines of add_executable
/add_library
, then you can factor that out into a separate file using target_sources()
and either another CmakeLists.txt file in a subdirectory included via add_subdirectory
, or a sources.cmake file in the same subdirectory included via include()
.
To generate the buildsystem as shown in your directory tree diagram, change your current directory to .../myProject
and then run cmake -S . -B build <...>
, where <...>
is any other configuration arguments you want to use.
As always, the CMake reference docs can be quite helpful, but overwhelming to look at the first couple of times, since they're not meant for beginners to learn from. If you want to learn more about how to use CMake, try out the official CMake tutorial, and reading relevant chapters in the Mastering CMake book. If you really want to dive deep, check out "Professional CMake"- written by Craig Scott (one of the CMake maintainers). It costs money, but having read the sample chapter, the table of contents, and other blog posts and proposals on the CMake GitLab by Craig, I have faith in its value, and new editions to the book don't cost extra.
Tricks for Resource Files
By the way, it is important that my program knows where the resources are. I would like to know the recommended way of managing resources. I do not want to access my resources with "../resources/file.png"
This response is written assuming you chose CMake to use it for all it's worth- cross-platform, flexible-toolchain builds, which not everybody uses CMake for (which is fine).
This is its own can of worms. One tricky part is the filesystem placement compared between the build folder and the install folder. The build folder is where the binaries and other ingredients like object files get built, and the install folder is anywhere you install those built binaries to. They can have very different filesystem structures, where the build folder layout is up to CMake, and it does sensible things by default, but the layout of things in the install folder is largely up to how you want it to be configured. That can be different than what CMake does in the build folder, and you'll probably want to be able to run and test your binaries from both the build folder and the install folder. So you need to find a way to support your binaries finding your resource files in both where they are at "development time" (when you're running from the build folder), and after installation (when you or your users are running the installed binaries).
There are also various different conventions on different platforms for where to place resource files for installation. There's a convention defined by the GNU Coding Standards, which CMake has a bit of integration with / support for, but of course, Microsoft Windows has another thing with \Program Files\
and \Users\...\AppData\
, and MacOS has another thing with app bundles and /library/Application Support/
. Things might not be as simple as you thought they were. I don't know a lot about this (and I could be wrong), but it seems to me that this is big enough of a topic to have its own question, or several of its own questions here on Stack Overflow.
For other related CMake bits, see:
For examples of shortcomings with simple/naive approaches, the answer by sgvd works for the build directory, but will not work wherever the project is installed to, and even if it's made to somehow work on the builder's machine, the fact that it uses an absolute path makes it unlikely to work when distributed to other machines or platforms with different conventions.
If you're using CMake but only want to support a specific platform, then consider yourself lucky and find or write a question here on Stack Overflow about how to do that.
See also this related question (which at the time of this writing still has no answers): How to specify asset paths that work across builds.
Send-off
Welcome to the world of CMake! Just wait 'till you get to generator expressions! Then you'll really start having fun!
^said in jest, but no joking- generator expressions can be very useful, and I'd choose them any day over suffering in the Visual Studio configurations UI or manually editing Visual Studio solution files (just to give an example for one buildsystem).