Lesson 15 · Organize your project
Organize with add_subdirectory
Split a project into directories, each with its own CMakeLists.txt. Scope rules, and the layout that scales.
One CMakeLists.txt for a whole project stops scaling at a few hundred lines. The fix is add_subdirectory, the command that turns your project into a tree of small, focused files.
The pattern
myapp/
├── CMakeLists.txt # top level. Versions, options, includes the rest.
├── src/
│ ├── CMakeLists.txt # the app and its libraries
│ └── main.cpp
├── libs/
│ └── calc/
│ ├── CMakeLists.txt # the calc library, self-contained
│ ├── calc.cpp
│ └── include/calc/calc.h
└── tests/
├── CMakeLists.txt # tests (lesson 25)
└── test_main.cpp
CMakeLists.txtcmake_minimum_required(VERSION 4.4) project(myapp VERSION 1.0.0 LANGUAGES CXX) # Each subdirectory adds its own targets. add_subdirectory(src) add_subdirectory(libs/calc) add_subdirectory(tests)
Each subdirectory has its own small CMakeLists.txt:
libs/calc/CMakeLists.txtadd_library(calc STATIC calc.cpp) target_include_directories(calc PUBLIC include)
src/CMakeLists.txtadd_executable(myapp main.cpp) target_link_libraries(myapp PRIVATE calc)
Every target knows every other target. The app in src/ links calc from libs/calc/ with no path games. That is the key property of add_subdirectory. All targets live in one global namespace, no matter where they were declared.
Scope. Targets global, variables not
Two scopes coexist in CMake:
- Targets are global.
add_library(calc ...)in any subdirectory is visible everywhere. - Variables are directory-scoped. A
set(...)inlibs/calc/is not visible insrc/, unless it is aCACHEvariable (lesson 16) or you explicitly travel withPARENT_SCOPE.
libs/calc/CMakeLists.txtset(CALC_IMPL "fast") # local to this directory set(CALC_API 2 CACHE STRING "") # global. Cache variables are visible everywhere.
Why
This asymmetry is deliberate. Variables should not leak, that is the global-settings problem from lesson 7. Targets must be linkable from anywhere. Modern CMake leans on this. Share things by linking targets, not by threading variables through directories.
What each level owns
| Level | Owns |
|---|---|
| Top-level | cmake_minimum_required, project(), options (lesson 16), add_subdirectory calls |
| Library dirs | Their library targets and requirements. Self-contained, no knowledge of the app |
| App dir | The executable, linking the libraries |
| Tests dir | Test targets (lesson 25) |
The rule. No crossing
A library’s CMakeLists.txt should know nothing about the app. If libs/calc needs something from src/, that is a design smell. Move the shared thing into its own directory.
Pitfall
Do not use
add_subdirectoryfor external dependencies, things you do not own, from git. It pulls their targets and options into your build, and lesson 7’s leak problem becomes theirs, your globals become theirs. External dependencies belong infind_package(lesson 17) orFetchContent(lesson 18).
Tip
If a subdirectory is optional, guard it with an option.
option(MYAPP_BUILD_TESTS "Build tests" ON)plusif(MYAPP_BUILD_TESTS) add_subdirectory(tests) endif()(lesson 16). The structure is identical. Only the wrapper changes.
Read the docs