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.txt
cmake_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.txt
add_library(calc STATIC calc.cpp) target_include_directories(calc PUBLIC include)
src/CMakeLists.txt
add_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(...) in libs/calc/ is not visible in src/, unless it is a CACHE variable (lesson 16) or you explicitly travel with PARENT_SCOPE.
libs/calc/CMakeLists.txt
set(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

LevelOwns
Top-levelcmake_minimum_required, project(), options (lesson 16), add_subdirectory calls
Library dirsTheir library targets and requirements. Self-contained, no knowledge of the app
App dirThe executable, linking the libraries
Tests dirTest 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_subdirectory for 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 in find_package (lesson 17) or FetchContent (lesson 18).

Tip

If a subdirectory is optional, guard it with an option. option(MYAPP_BUILD_TESTS "Build tests" ON) plus if(MYAPP_BUILD_TESTS) add_subdirectory(tests) endif() (lesson 16). The structure is identical. Only the wrapper changes.

Read the docs