Working with Subdirectories
The add_subdirectory() Command
add_subdirectory() tells CMake to process another directory containing its own CMakeLists.txt. This is fundamental for organizing multi-component projects.
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
Parameters:
source_dir: Path to directory with CMakeLists.txtbinary_dir: (Optional) Where to put build filesEXCLUDE_FROM_ALL: Don't build by default
How It Works
When CMake encounters add_subdirectory():
- Enters the specified directory
- Processes that directory's
CMakeLists.txt - Creates a new scope (variables can be isolated)
- Returns to the parent when done
cmake_minimum_required(VERSION 3.15)
project(MyProject)
message(STATUS "Processing root")
add_subdirectory(libs) # Process libs/CMakeLists.txt
add_subdirectory(app) # Then process app/CMakeLists.txt
message(STATUS "Back in root")
message(STATUS "Processing libs")
add_library(mylib mylib.cpp)
Output:
-- Processing root
-- Processing libs
-- Back in root
Variable Scope
Subdirectories create new variable scopes, but it's not complete isolation:
Variables Are Inherited
Child directories see parent variables:
set(MY_VAR "from parent")
add_subdirectory(child)
message(STATUS "MY_VAR = ${MY_VAR}") # Output: "from parent"
Changes Don't Propagate Upward
By default, changes in child don't affect parent:
set(CHILD_VAR "value")
message(STATUS "${CHILD_VAR}") # Empty! Not set in parent scope
Explicit Parent Scope
Use PARENT_SCOPE to modify parent variables:
set(RESULT "computed value" PARENT_SCOPE)
add_subdirectory(child)
message(STATUS "${RESULT}") # Output: "computed value"
Important: Setting PARENT_SCOPE doesn't set the variable in the current scope:
set(VAR "value" PARENT_SCOPE)
message(STATUS "${VAR}") # Empty in current scope!
# To set both:
set(VAR "value" PARENT_SCOPE)
set(VAR "value")
Targets Are Global
Unlike variables, targets (executables, libraries) are globally visible after creation:
add_library(mylib mylib.cpp)
add_executable(myapp main.cpp)
# Can link to library from sibling directory
target_link_libraries(myapp PRIVATE mylib)
This works because targets exist in a global namespace. However, best practice is to use ALIAS targets for clarity:
add_library(mylib mylib.cpp)
add_library(MyProject::mylib ALIAS mylib) # Namespaced
target_link_libraries(myapp PRIVATE MyProject::mylib)
Directory Properties
Beyond variables and targets, directories have properties you can set:
# Set property for current directory
set_property(DIRECTORY PROPERTY COMPILE_OPTIONS -Wall)
# Set for specific directory
set_property(DIRECTORY libs PROPERTY COMPILE_DEFINITIONS DEBUG_MODE)
# Get directory property
get_property(options DIRECTORY PROPERTY COMPILE_OPTIONS)
Common directory properties:
COMPILE_OPTIONS: Compiler flags for all targets in directoryCOMPILE_DEFINITIONS: Preprocessor definitionsINCLUDE_DIRECTORIES: Include pathsLINK_DIRECTORIES: Library search paths (avoid, use target commands)
Binary Directory Structure
CMake mirrors source directory structure in build directory:
Source tree: Build tree:
project/ build/
├── CMakeLists.txt ├── CMakeFiles/
├── libs/ ├── libs/
│ └── CMakeLists.txt │ └── CMakeFiles/
└── app/ └── app/
└── CMakeLists.txt └── CMakeFiles/
Each subdirectory gets its own build directory. Access them with:
CMAKE_CURRENT_SOURCE_DIR: Source directory being processedCMAKE_CURRENT_BINARY_DIR: Corresponding build directory
message(STATUS "Source: ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "Binary: ${CMAKE_CURRENT_BINARY_DIR}")
Custom Binary Directory
Override default build location:
add_subdirectory(libs ${CMAKE_BINARY_DIR}/mylibs)
Now libs/ builds to build/mylibs/ instead of build/libs/. Rarely needed but useful for organizing complex builds.
Include vs add_subdirectory
add_subdirectory()
- Processes
CMakeLists.txtin another directory - Creates new scope
- Has its own binary directory
- Use for: components with their own build
add_subdirectory(libs)
include()
- Processes a
.cmakefile inline - No new scope (unless you create one)
- No binary directory
- Use for: shared CMake code, utilities, macros
include(cmake/CompilerWarnings.cmake)
When to use each:
- add_subdirectory(): Component has targets (library, executable)
- include(): Shared CMake functions, variables, or configuration
EXCLUDE_FROM_ALL
Prevent subdirectory targets from building by default:
add_subdirectory(optional_tools EXCLUDE_FROM_ALL)
add_executable(tool1 tool1.cpp)
add_executable(tool2 tool2.cpp)
Behavior:
cmake --build build→ tools won't buildcmake --build build --target tool1→ only tool1 buildscmake --build build --target all→ tools still excluded
Use cases:
- Optional utilities
- Documentation generators
- Development-only tools
- Large examples that slow down builds
Ordering Matters
Subdirectories are processed in order listed:
add_subdirectory(libs) # Build libraries first
add_subdirectory(app) # Then app that uses libraries
If app depends on targets from libs, they must be added in this order. Wrong order causes errors:
add_subdirectory(app) # ❌ Error: 'mylib' target not found
add_subdirectory(libs) # Defines mylib too late
Dependencies determine order:
# Correct order based on dependencies
add_subdirectory(external) # Third-party libs (no deps)
add_subdirectory(libs/utils) # Utils (no deps)
add_subdirectory(libs/core) # Core (depends on utils)
add_subdirectory(libs/ui) # UI (depends on core)
add_subdirectory(app) # App (depends on ui)
add_subdirectory(tests) # Tests (depend on everything)
Relative Paths
add_subdirectory() accepts relative paths from current directory:
# From root
add_subdirectory(libs/core) # Relative to root
add_subdirectory(../shared) # Parent directory (unusual)
# Absolute paths work but aren't portable
add_subdirectory(/usr/src/lib) # Avoid
Best practice: Keep subdirectories within your project tree and use relative paths.
Conditional Subdirectories
Add subdirectories based on conditions:
option(BUILD_TESTS "Build test suite" ON)
option(BUILD_EXAMPLES "Build examples" OFF)
if(BUILD_TESTS)
add_subdirectory(tests)
endif()
if(BUILD_EXAMPLES)
add_subdirectory(examples)
endif()
# Platform-specific
if(WIN32)
add_subdirectory(windows)
elseif(UNIX)
add_subdirectory(unix)
endif()
Communicating Between Subdirectories
Via Global Targets
Most common - one subdirectory creates target, another uses it:
add_library(mylib mylib.cpp)
add_library(Project::mylib ALIAS mylib)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE Project::mylib)
Via Cache Variables
Share configuration across subdirectories:
set(SHARED_OPTION ON CACHE BOOL "Shared option")
add_subdirectory(component1)
add_subdirectory(component2)
Both components see SHARED_OPTION.
Via Parent Scope
Child can pass information to parent:
set(STATUS_MESSAGE "Child completed successfully" PARENT_SCOPE)
add_subdirectory(child)
message(STATUS "${STATUS_MESSAGE}")
Common Patterns
Library Collection
Building multiple libraries:
add_subdirectory(libs)
add_subdirectory(core)
add_subdirectory(utils)
add_subdirectory(network)
Each library directory has its own CMakeLists.txt defining the library target.
Optional Features
option(ENABLE_NETWORKING "Enable network features" ON)
if(ENABLE_NETWORKING)
add_subdirectory(network)
set(HAS_NETWORKING TRUE CACHE INTERNAL "")
else()
set(HAS_NETWORKING FALSE CACHE INTERNAL "")
endif()
# Later, other code can check:
if(HAS_NETWORKING)
target_compile_definitions(app PRIVATE HAS_NETWORKING)
endif()
Automatic Discovery
Find all subdirectories with CMakeLists.txt:
file(GLOB children RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} */CMakeLists.txt)
foreach(child ${children})
get_filename_component(dir ${child} DIRECTORY)
message(STATUS "Adding subdirectory: ${dir}")
add_subdirectory(${dir})
endforeach()
Warning: This makes build non-deterministic - order depends on filesystem. Prefer explicit lists.
Practical Example
A complete multi-subdirectory project:
cmake_minimum_required(VERSION 3.15)
project(MultiComponent VERSION 1.0.0)
set(CMAKE_CXX_STANDARD 17)
# Configuration
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
option(BUILD_TESTS "Build tests" ON)
# Output directories
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
# External dependencies
add_subdirectory(external)
# Project components (dependency order)
add_subdirectory(libs/math)
add_subdirectory(libs/core)
add_subdirectory(libs/graphics)
# Main application
add_subdirectory(app)
# Optional components
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
add_library(core
src/engine.cpp
src/system.cpp
)
add_library(MultiComponent::core ALIAS core)
target_include_directories(core
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
PRIVATE
src/
)
target_link_libraries(core
PUBLIC MultiComponent::math
)
target_compile_features(core PUBLIC cxx_std_17)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE
MultiComponent::core
MultiComponent::graphics
)
Debugging Subdirectories
See What's Being Processed
message(STATUS "Entering directory: ${CMAKE_CURRENT_SOURCE_DIR}")
Check Variables
# At end of subdirectory
get_cmake_property(_vars VARIABLES)
foreach(_var ${_vars})
message(STATUS "${_var} = ${${_var}}")
endforeach()
Verify Targets Exist
if(NOT TARGET Project::mylib)
message(FATAL_ERROR "Expected target Project::mylib not found")
endif()
Best Practices
- One component per subdirectory - clear boundaries
- Use ALIAS targets - namespace your targets
- Respect dependency order - dependencies before dependents
- Limit PARENT_SCOPE - prefer cache variables or global targets
- Don't rely on subdirectory order - make dependencies explicit
- Use relative paths - stay within project
- Document subdirectory purpose - comment in root CMakeLists.txt
- Keep subdirectory CMakeLists.txt focused - just that component
❌ Wrong order causing "target not found" errors
❌ Assuming variables propagate back to parent
❌ Overusing global commands in root affecting all subdirs
❌ Circular dependencies between subdirectories
❌ Modifying parent directory properties from child
Quick Reference
# Add subdirectory
add_subdirectory(path)
add_subdirectory(path binary_dir)
add_subdirectory(path EXCLUDE_FROM_ALL)
# Conditional subdirectory
if(BUILD_FEATURE)
add_subdirectory(feature)
endif()
# Current directories
${CMAKE_CURRENT_SOURCE_DIR} # Current source dir
${CMAKE_CURRENT_BINARY_DIR} # Current build dir
# Parent scope
set(VAR "value" PARENT_SCOPE)
# Check target exists
if(TARGET mylib)
# Target available
endif()
# Include vs add_subdirectory
include(file.cmake) # For CMake code
add_subdirectory(component) # For components with targets
Understanding add_subdirectory() is key to organizing scalable CMake projects. It provides structure while maintaining the flexibility to share targets and configuration across components.