CMake from Beginner to Expert (4): Multi-Directory Project Management

Foundation of Modular Builds: In-Depth Practice from Directory Isolation to Variable Passing

1. Why is Multi-Directory Project Management Necessary?

As the project scales to hundreds of source files, piling all code into a single directory leads to:

  • Difficult Maintenance: Hard to quickly locate the code corresponding to a module or function

  • Low Compilation Efficiency: Full compilation takes time, and incremental updates to submodules are not possible

  • Confused Dependencies: Global variables and header file paths pollute each other

Multi-directory management can achieve:

  • Logical Layering: <span>src/</span> (main program), <span>lib/</span> (core library), <span>test/</span> (unit tests)

  • Compilation Isolation: Submodules are built independently, avoiding redundant compilation

  • Clear Dependencies: Control variable and header file paths through scope

2. Core Function Details

2.1 <span>add_subdirectory()</span>: Submodule Build Controller

Function Definition:

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
  • Parameter Analysis:

    • <span>source_dir</span>: Source directory of the submodule (must contain <span>CMakeLists.txt</span>)

    • <span>binary_dir</span>: Build output directory of the submodule (default generated in a directory with the same name under <span>build</span>)

    • <span>EXCLUDE_FROM_ALL</span>: The submodule does not participate in the default build (must be specified manually)

Example:

# Main project CMakeLists.txt
add_subdirectory(lib/math)        # Build math library
add_subdirectory(lib/network)     # Build network library
add_subdirectory(tests EXCLUDE_FROM_ALL)  # Test module needs to be built manually

2.2 <span>PARENT_SCOPE</span>: Cross-Directory Variable Passing

  • Scope Rules:

    • In the subdirectory, use <span>set(var value PARENT_SCOPE)</span> to modify the parent directory variable

    • The parent directory cannot directly access local variables of the subdirectory

Example:

# Parent directory CMakeLists.txt
set(ROOT_VAR "Hello")
add_subdirectory(subdir)
message("Parent directory received: ${SUB_VAR}")  # Outputs World

# subdir/CMakeLists.txt
set(SUB_VAR "World" PARENT_SCOPE)  # Pass up
message("Subdirectory reads: ${ROOT_VAR}")  # Outputs Hello

2.3 <span>include()</span> vs <span>add_subdirectory()</span>

Feature <span>include()</span> <span>add_subdirectory()</span>
Scope Current scope Creates a new sub-scope
Variable Passing Directly modifies current variable Requires <span>PARENT_SCOPE</span> for explicit passing
Build Output Directory No independent directory Generated in specified <span>binary_dir</span>
Typical Scenario Loading scripts or configurations Managing code modules

3. Practical: Multi-Directory Project Architecture Design

3.1 Project Structure

├── CMakeLists.txt
├── lib
│   └── math
│       ├── CMakeLists.txt
│       ├── include
│       │   └── math_utils.h
│       └── src
│           ├── matrix.cpp
│           └── vector.cpp
├── src
│   ├── CMakeLists.txt
│   ├── main.cpp
│   └── utils
│       └── logger.cpp
└── tests
    ├── CMakeLists.txt
    └── test_math.cpp

3.2 Main Project Configuration (Autopilot/CMakeLists.txt)

cmake_minimum_required(VERSION 3.12)
project(Autopilot LANGUAGES CXX)

# Add submodules
add_subdirectory(lib/math)
add_subdirectory(src)
add_subdirectory(tests EXCLUDE_FROM_ALL)  # Test module not built by default

# Global variable passing example
set(ENABLE_DEBUG ON)
message("Global variable passing test: ${ENABLE_DEBUG}")

3.3 Submodule Configuration (lib/math/CMakeLists.txt)

# Define math library
add_library(math_utils STATIC
      src/matrix.cpp
      src/vector.cpp
)

# Header file paths (PUBLIC property passing)
target_include_directories(math_utils
      PUBLIC
          ${CMAKE_CURRENT_SOURCE_DIR}/include  # Must be included by users
      PRIVATE
          ${CMAKE_CURRENT_SOURCE_DIR}/src  # For internal use only
)

# Read parent directory variable
if(ENABLE_DEBUG)
      target_compile_options(math_utils PRIVATE -g3)
endif()

# Pass variable up
set(MATH_LIB_READY TRUE PARENT_SCOPE)

3.3 Submodule Configuration (src/CMakeLists.txt)

add_executable(lesson4 main.cpp)
target_link_libraries(lesson4 PRIVATE math_utils)

4. Variable Scope Traps and Solutions

4.1 Trap: Accidental Overwriting of Parent Variable

# Parent directory
set(COMPILE_MODE "release")
add_subdirectory(subdir)
message("Parent directory mode: ${COMPILE_MODE}")  # Outputs debug (accidentally modified)

# subdir/CMakeLists.txt
set(COMPILE_MODE "debug")  # Did not use PARENT_SCOPE, but polluted the parent directory?

Reason:<span>add_subdirectory</span><span><span> creates a sub-scope by default, but if the parent directory variable is not defined, the subdirectory's </span></span><code><span>set()</span><span><span> will implicitly create that variable in the parent scope.</span></span>

4.2 Solution: Strict Scope Control

  • Solution 1: Predefine parent directory variables and mark them with <span>CACHE</span>

# Parent directory
set(COMPILE_MODE "release" CACHE STRING "Build mode")

Solution 2: Subdirectory uses <span>PARENT_SCOPE</span><span><span> for explicit passing</span></span>

# subdir/CMakeLists.txt
if(NOT DEFINED COMPILE_MODE)
    set(COMPILE_MODE "debug" PARENT_SCOPE)
endif()

4.3 Best Practices

  • Minimize Global Variables: Prefer using target_* series commands to manage properties

  • Scope Isolation: Subdirectories encapsulate logic through <span>function()</span><span> to avoid pollution</span>

  • Variable Naming Conventions: Prefix global variables (e.g., <span>PROJECT_COMPILE_MODE</span>)

5. Build and Verification

# Build main program and library
mkdir -p build && cd build
cmake .. -DENABLE_DEBUG=ON
make

# Build test module separately
# make tests

Verification Points:

  1. Check the library file generation path:

ls build/lib/math/libmath_utils.a

Confirm header file passing:

// src/main.cpp
#include <math_utils.h>  // Should have no errors

6. Summary and Next Article Preview

Key Points of This Article:

  • Master <span>add_subdirectory</span><span> to build multi-directory projects</span>

  • Understand the variable passing mechanism of <span>PARENT_SCOPE</span>

  • Avoid scope traps

  • Source code reference:

    https://github.com/vinmanager/hellocmake/tree/main/lesson4

Next Article Preview:

  • “Part 5: Static Libraries and Dynamic Libraries – <span>add_library</span><span> Complete Guide”</span>

    • <span>STATIC</span>/<span>SHARED</span> library compilation differences

    • Symbol hiding and <span>fPIC</span> principles

    • Practical: Building OpenCV-style layered libraries

Follow our public account for push notifications.

Discussion: What “pits” have you encountered in variable passing in multi-directory projects? Feel free to share solutions in the comments!

CMake from Beginner to Expert (1): CMake Minimal Introduction – Environment Configuration and First Project

CMake from Beginner to Expert (2): Variables and Scope

CMake from Beginner to Expert (3): Functions and Parameter Passing

Detailed explanation of target_include_directories in CMake

Leave a Comment