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:
-
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