A clear directory layout keeps CMake files small and lets components be reused or extracted later.
As a project grows, split it: the top-level CMakeLists.txt sets up the project and includes subdirectories, and each component directory defines its own targets.
project/
CMakeLists.txt # cmake_minimum_required, project, add_subdirectory(...)
libs/core/CMakeLists.txt
apps/cli/CMakeLists.txt
tests/CMakeLists.txtadd_subdirectory(libs/core) processes that directory's CMakeLists.txt right away, with its own variable scope (a copy of the parent's variables) and its own binary directory under build/. Targets, unlike variables, are global: a target defined in libs/core can be linked from apps/cli as long as it's defined first.
Paths worth knowing:
CMAKE_CURRENT_SOURCE_DIR/CMAKE_CURRENT_BINARY_DIRthe directory being processed and its build counterpart. Relative paths intarget_sourcesandtarget_include_directoriesresolve against the current source directory.PROJECT_SOURCE_DIRthe directory of the latestproject()call; prefer it toCMAKE_SOURCE_DIR, which is the top of the whole build and wrong when your project is itself included by another.
Guard optional parts with options: option(MYPROJ_BUILD_TESTS "Build tests" ON) and if(MYPROJ_BUILD_TESTS) add_subdirectory(tests) endif(). Prefix options with the project name to avoid clashes when the project is used as a subproject.
Example
# ---- CMakeLists.txt (top level) ----
cmake_minimum_required(VERSION 3.25)
project(todo VERSION 0.3 LANGUAGES CXX)
option(TODO_BUILD_TESTS "Build the todo tests" ${PROJECT_IS_TOP_LEVEL})
add_subdirectory(libs/core) # defines target todo_core
add_subdirectory(apps/cli) # links todo_core
if(TODO_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# ---- libs/core/CMakeLists.txt ----
# add_library(todo_core STATIC store.cpp item.cpp)
# target_include_directories(todo_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
# ---- apps/cli/CMakeLists.txt ----
# add_executable(todo main.cpp)
# target_link_libraries(todo PRIVATE todo_core)
# $ cmake -S . -B build -DTODO_BUILD_TESTS=OFFRemember
- add_subdirectory gives each directory its own variable scope; targets are global
- Use CMAKE_CURRENT_SOURCE_DIR and PROJECT_SOURCE_DIR, not CMAKE_SOURCE_DIR
- Prefix options with the project name, e.g. TODO_BUILD_TESTS
Watch out for
- Setting a variable in a subdirectory and expecting the parent to see it; use PARENT_SCOPE or, better, target properties.