When managing multiple source files in C/C++ projects, CMake has become the industry standard for build systems. This article will analyze the core methods for handling multiple source file operations and common basic usages of CMake statements, documenting how to eliminate the tedious task of manually maintaining compilation lists.
1. Project Structure
When there are a large number of folders and files in a project, a single CMakeList can compile them all, but maintaining it becomes very cumbersome. Therefore, when dealing with a large codebase, it is common to divide it by modules, placing the code for each module in a separate CMakeList for compilation configuration. If a module’s code is still extensive, this module can be further subdivided into smaller modules managed by multiple CMakeLists.
These CMakeLists are then nested according to their paths, with each CMakeList in the project forming a tree structure, ultimately being nested into the top-level CMakeList. The structure is similar to the following:
Proxy/
├── CMakeLists.txt
├── Client
│ ├── CMakeLists.txt
│ └── main_client.cc
├── Ipc
│ ├── CMakeLists.txt
│ ├── msg_manager.cc
│ └── msg_manager.h
├── Server
│ ├── Api
│ │ ├── common_type.h
│ │ ├── led_manager_proxy.cc
│ │ └── led_manager_proxy.h
│ ├── CMakeLists.txt
│ ├── Led
│ │ ├── led_manager.cc
│ │ └── led_manager.h
│ └── main_server.cc
└── build
└── build.sh
In the build system of this project, the top-level <span>Proxy/CMakeLists.txt</span> file will include the <span>Client</span>, <span>Ipc</span>, and <span>Server</span> subdirectory CMakeLists. If there are subfolders under the <span>Server</span> directory, their corresponding <span>CMakeLists.txt</span> files will continue to recursively include the build scripts of these subdirectories.
This hierarchical inclusion structure has the following advantages:
-
Focus: Each level’s
<span>CMakeLists.txt</span>is only responsible for defining the compilation of the source code it directly manages. -
Modular Control: Easily enable or disable (by including/excluding) the compilation of any submodule (such as
<span>Client</span>,<span>Ipc</span>,<span>Server</span>, and their subtrees). -
High Maintainability: Since each build file is small and has a single function, the code is easy to read and maintain.
2. CMakeList
<span>Top-Level CMakeList</span>
# File structure
# Specify minimum version
cmake_minimum_required(VERSION 2.8)
# Specify C++11 version
set(CMAKE_CXX_STANDARD 11)
# Specify project name
project(ProxyMode)
# Add precompiled definitions for the current path and subdirectory source files
# add_definitions(-DFOO -DDEBUG ...)
# Compiler settings
set(CMAKE_C_COMPILER "gcc")
set(CMAKE_CXX_COMPILER "g++")
# Set C++ compilation parameters (CMAKE_CXX_FLAGS is a global variable)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -g3")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11 -g3 -fpermissive")
# Include subdirectories
add_subdirectory(Server)
add_subdirectory(Client)
add_subdirectory(Ipc)
The top-level CMakeList generally needs to perform the following tasks:
1. Configure project-related properties: CMake version, project name
2. Configure cross-tool: set compiler, add compilation parameters
3. Include the nested subdirectory CMakeLists
The CMakeList under the Server path
## Specify minimum version
cmake_minimum_required(VERSION 2.8)
## Macro
set(PROJECT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/..)
cmake_policy(SET CMP0046 NEW)
## Output path
set(OUTPUT_PATH ${PROJECT_PATH}/Out)
set(LIBRARY_OUTPUT_PATH ${OUTPUT_PATH}/lib)
set(EXECUTABLE_OUTPUT_PATH ${OUTPUT_PATH}/bin)
## Includes
include_directories(${PROJECT_PATH}/Ipc)
include_directories(${PROJECT_PATH}/Server/Api)
## Library path
link_directories(${OUTPUT_PATH}/lib)
## main_server.exe
set(SRC_BIN_CLIENT main_client.cc)
add_executable (client ${SRC_BIN_CLIENT})
set_target_properties(client PROPERTIES OUTPUT_NAME "mainclient")
target_link_libraries(client c pthread ledapi)
add_dependencies (client libledapi)
The CMakeList under the subdirectory needs to focus on the compilation files:
1. Include header file paths
2. Set target generation paths
3. Set compilation targets, bin or so
Then, based on the expected compilation results, use the relevant variables. In the example, for convenience, a build.sh compilation script has been added. This script replaces the execution of compilation commands while managing the generated cache files in the specified path.
## build.sh
rm -rf ../Out/Cache
rm -rf ../Out/bin/*
rm -rf ../Out/lib/*
mkdir -p ../Out/cache/
cd ../Out/cache/
cmake ../../make
3. Common Usages of CMake Statements
Setting Local Variables
## Set local variable
PROJECT_PATH
set(PROJECT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/..)
## Use local variable PROJECT_PATH
include_directories(${PROJECT_PATH}/Ipc)
Setting Custom Global Variables
## Proxy/CMakeLists.txt
## Set custom global variable
PROJECT_DESC
set(PROJECT_DESC "This is project")
set_property(GLOBAL PROPERTY source_list_property "${PROJECT_DESC}")
Getting Custom Global Variables
## Proxy/Ipc/CMakeLists.txt
## Get custom global variable
PROJECT_DESC
get_property(PROJECT_DESC GLOBAL PROPERTY source_list_property)
message("PROJECT_DESC=${PROJECT_DESC}")
Specifying Target (bin/library) Output Paths
## Set library output path
set(LIBRARY_OUTPUT_PATH xx/Out/lib)
## Set bin file output path
set(EXECUTABLE_OUTPUT_PATH xx/Out/bin)
Setting Environment Variables
set(ENV{<variable>} [<value>]) ENV: Environment variable prefix
variable: Variable name
value: Variable value
</value></variable>
For example, set environment CMAKE_FILE
## Set environment variable
set(ENV{CMAKE_FILE} "./IPC")
Getting Environment Variables
# Check if CMAKE_FILE environment variable is defined
if(DEFINED ENV{CMAKE_FILE})
message("CMAKE_FILE: $ENV{CMAKE_FILE}")
else()
message("NOT DEFINED CMAKE_FILE VARIABLES")
endif()
Setting Compiler
## Specify C compiler
set(CMAKE_C_COMPILER "gcc")
## Specify C++ compiler
set(CMAKE_CXX_COMPILER "g++")
When the compiler toolchain path is added to the environment variable, you can directly write the name of the compiler tool. When configuring cross-compilation tools, the absolute path of the corresponding cross-compilation toolchain should be written here.
Setting Dependency Library Paths
## Parentheses are the absolute path of the dependency library
link_directories(${OUTPUT_PATH}/lib)
Including Header File Paths
## Parentheses are the absolute path of the header files
include_directories (${PROJECT_PATH}/Ipc)
Adding Compiler Options
## Enable compilation warnings for all compilers (including C and C++ compilers)
add_compile_options("-Wall -Werror")
## Enable compilation warnings for C compiler
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror")
## Enable compilation warnings for C++ compiler
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror")
Adding Print Statements
## Print the value of CMAKE_CXX_FLAGS
message("${CMAKE_CXX_FLAGS}")
Passing Macros from Shell Scripts to CMakeLists
When executing cmake from the command line, follow with -DXXX to pass the macro XXX to CMakeLists from the command line. Writing this command line into a script allows passing macros from shell scripts to CMakeLists.
## Add TEST macro
cmake . -DTEST
## Add TEST_OPTION=ON
cmake . -DTEST_OPTION=ON
Passing Variables from CMakeLists to Code Projects
## Add TEST macro to code project
add_definitions(-DTEST)
Code checks if the macro TEST is defined to implement macro control
// *.c / *.cpp
#ifdef TEST
... // code
#endif
#if defined TEST
... // code
#endif
CMakeLists Path Nesting
## Add current path Client files
add_subdirectory(Client)
Controlling the Compilation Process
Option syntax
## Option compilation process control
option(<variable> "<help_text>" [value])
variable: Option name
help_text: Description, explanation, remarks
value: Option initialization value (all OFF except ON)
</help_text></variable>
For example, the TEST_OPTION macro controls the compilation process[1]
option(TEST_OPTION "test option" ON)
if (DEFINED TEST_OPTION)
message(STATUS "TEST_OPTION defined: " ${TEST_OPTION})
else ()
message(STATUS "TEST_OPTION un-defined: " ${TEST_OPTION})
endif()
if (TEST_OPTION)
message(STATUS "TEST_OPTION ON.")
add_definitions(-DTEST_OPTION)
else ()
message(STATUS "TEST_OPTION OFF.")
endif()
if (NOT TEST_OPTION)
message(STATUS "NOT-TEST_OPTION ON.")
else ()
message(STATUS "NOT-TEST_OPTION OFF.")
endif()
Conclusion: The Art of Building
The essence of CMake managing multiple source files lies in:
-
Modularity: Decomposing into independent components
-
Automation: Utilizing wildcards and scripts
-
Maintainability: Clear target dependencies
-
Scalability: Supporting incremental additions
Ultimate Advice: In projects with over 100,000 lines of code, it is essential to adopt:
Modular CMake structure
Goal-oriented dependency management
Automated test integration
Continuous integration configuration
References
[1] Introduction to Using Options in CMake:
https://blog.csdn.net/lhl_blog/article/details/123553686
[2]
Setting Compilation Warnings and Errors for gcc and cmake:
https://blog.csdn.net/LCHUESTC/article/details/122602001
[3]
Common Compilation Options for GCC:
https://zhuanlan.zhihu.com/p/393419013
[4]
Warning-Options:
https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
Thank you for your attention and support. I will update various useful content from time to time. If you have any questions or different opinions, feel free to comment or leave a message for us to learn and exchange together~
Feel free to follow my public account, and give a “Share”, “Like”, or “View”~Wishing you successful compilation and no debugging errors!