Wiki
Detailed Explanation of CMake for src and lib Modules
Existing English translations were reused. 19 current source blocks remain in Simplified Chinese, without triggering paid translation.
This section covers three levels:
src/CMakeLists.txtHow to create the main program.src/lib1/CMakeLists.txtHow to create a library.src/lib2/CMakeLists.txtWhy it is similar tolib1but target names cannot be duplicated.
1.src/CMakeLists.txt
File contents:
add_executable(${PROJECT_NAME}
${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
)
target_link_libraries(${PROJECT_NAME}
PRIVATE
project_options
project_warnings
)
add_subdirectory(lib1)
add_subdirectory(lib2)
target_link_libraries(lib1_src_lib
PRIVATE
lib2_src_lib
)
target_link_libraries(${PROJECT_NAME}
PRIVATE
lib1_src_lib
lib2_src_lib
)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set_target_properties(${PROJECT_NAME} PROPERTIES
INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}"
)
endif()
install(TARGETS ${PROJECT_NAME}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
This file is responsible for the final executable file.
2.add_executable
add_executable(${PROJECT_NAME}
${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
)
Purpose: Create the executable file target.
Common notation:
add_executable(app main.cpp)
add_executable(robot_main main.cpp robot.cpp)
add_executable(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp)
The first parameter is the target name, which is also usually the name of the generated executable file.
This template uses:
${PROJECT_NAME}
and PROJECT_NAME comes from the top level:
project(cmake_template VERSION 1.0.0 LANGUAGES C CXX)
So the executable file is called:
cmake_template
2.1.CMAKE_CURRENT_SOURCE_DIR
${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
Represents the current source code directory of CMakeLists.txt.
in src/CMakeLists.txt:
CMAKE_CURRENT_SOURCE_DIR = 项目根目录/src
Therefore:
${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
That is:
项目根目录/src/main.cpp
3.Link common options to the main program
target_link_libraries(${PROJECT_NAME}
PRIVATE
project_options
project_warnings
)
Although project_options and project_warnings are not ordinary libraries, they are CMake targets, so they are also passed through target_link_libraries.
Here, PRIVATE is used to indicate:
- 主程序自己使用模板默认的 C++20 编译要求。
- 主程序自己开启 warning。
- 主程序不需要把这些要求再传给别人,因为可执行文件一般不会被其他 target 链接。
4.Add subdirectory
add_subdirectory(lib1)
add_subdirectory(lib2)
Function: enter src/lib1 and src/lib2 and read their respective CMakeLists.txt.
After executing these two lines, the following two targets exist:
lib1_src_lib
lib2_src_lib
So usually, first:
add_subdirectory(lib1)
add_subdirectory(lib2)
Then set up the dependencies between libraries and the libraries that the main program should link against:
target_link_libraries(lib1_src_lib
PRIVATE
lib2_src_lib
)
target_link_libraries(${PROJECT_NAME}
PRIVATE
lib1_src_lib
lib2_src_lib
)
5.Let lib1 make a one-way call to lib2
This template now uses lib1 to call lib2 to make a one-way dependency example.src/main.cpp only directly calls lib1:
#include "lib1/eigen3_test.hpp"
int main()
{
lib1::run_eigen_vector_example();
return 0;
}
Running results (complete project link lib1 and lib2 after):
[lib1] Vector v = 1 2 3
[lib1] Norm = 3.74166
[lib2] Matrix m =
1 2
3 4
[lib2] Determinant = -2
Then in src/lib1/src/eigen3_test.cpp , lib1 call lib2:
#include "lib1/eigen3_test.hpp"
#include "lib2/eigen3_test.hpp"
#include <Eigen/Dense>
#include <iostream>
namespace lib1 {
void run_eigen_vector_example()
{
const Eigen::Vector3d vector(1.0, 2.0, 3.0);
std::cout << "[lib1] Vector v = " << vector.transpose() << '\n';
std::cout << "[lib1] Norm = " << vector.norm() << '\n';
lib2::run_eigen_matrix_example();
}
} // namespace lib1
Therefore, lib1_src_lib must be explicitly linked to lib2_src_lib:
target_link_libraries(lib1_src_lib
PRIVATE
lib2_src_lib
)
This sentence is suggested to be placed in:
add_subdirectory(lib1)
add_subdirectory(lib2)
afterwards. Because only after executing add_subdirectory(lib1) and add_subdirectory(lib2) are both targets lib1_src_lib and lib2_src_lib already present.
这里用 PRIVATE,表示 lib2 只是 lib1 自己实现时需要用到的库。CMake 会让 lib1 在编译时找到 lib2/eigen3_test.hpp,也会在链接时找到 lib2 里的函数实现,但不会强迫所有链接 lib1 的目标都自动使用 lib2 的头文件。
If in the future lib1 's public header file directly includes the header file of lib2 , or the public interface uses the type of lib2 , it should be changed to PUBLIC :
target_link_libraries(lib1_src_lib
PUBLIC
lib2_src_lib
)
Note that this is a unidirectional dependency:
lib1 -> lib2
Do not, in turn, let lib2 link lib1. If two libraries really need to use the same piece of code, the common part should usually be extracted into a new library such as common.
6.Link lib1 and lib2
target_link_libraries(${PROJECT_NAME}
PRIVATE
lib1_src_lib
lib2_src_lib
)
Function: Link the two example libraries in the template to the main program.
Current main.cpp only direct calls:
lib1::run_eigen_vector_example();
and lib1::run_eigen_vector_example() will internally call:
lib2::run_eigen_matrix_example();
So the actual call path of this template is:
main.cpp -> lib1 -> lib2
The function implementation is located at:
src/lib1/src/eigen3_test.cpp
src/lib2/src/eigen3_test.cpp
They are respectively compiled into:
lib1_src_lib
lib2_src_lib
So the main program needs to link at least lib1_src_lib , while lib1_src_lib in turn needs to link lib2_src_lib.
This template still has the main program link both lib1_src_lib and lib2_src_lib here, in order to preserve the intuitive structure of "the main program mounting two example libraries." In an actual project, if main.cpp does not directly call lib2 at all, you can also have the main program link only lib1_src_lib, with lib1 -> lib2 bringing in lib2 through this dependency relationship.
7.INSTALL_RPATH
本节讲的是 Linux 安装后的动态库搜索路径,所以当前模板把它放在 if(CMAKE_SYSTEM_NAME STREQUAL "Linux") 中。Windows 预设使用静态项目库,不进入这个分支。
set_target_properties(${PROJECT_NAME} PROPERTIES
INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}"
)
Purpose: Sets how the installed executable finds dynamic libraries at runtime.
下面以 ${CMAKE_INSTALL_LIBDIR} 为 lib64 的系统为例,库安装到:
install/linux-debug/lib64/
Executable installed to:
install/linux-debug/bin/
Run:
./install/linux-debug/bin/cmake_template
The program needs to find:
install/linux-debug/lib64/liblib1_src_lib.so
install/linux-debug/lib64/liblib2_src_lib.so
这里的属性设置在可执行文件上,$ORIGIN 表示该可执行文件所在目录,也就是:
install/linux-debug/bin
Therefore:
$ORIGIN/../lib64
That is:
install/linux-debug/lib64
where ${CMAKE_INSTALL_LIBDIR} may be lib or may be lib64.
Common fillable values:
| 写法 | 说明 |
|---|---|
"$ORIGIN/../${CMAKE_INSTALL_LIBDIR}" |
推荐,安装后相对查找库 |
"$ORIGIN" |
设置在动态库上时,从该动态库所在目录查找同目录的依赖库 |
"/usr/local/lib" |
写死系统路径,不适合模板 |
"" |
不设置 RPATH,需要依赖系统库路径或 LD_LIBRARY_PATH |
模板中还存在 main.cpp -> lib1 -> lib2 的依赖关系,因此 src/lib1/CMakeLists.txt 也为 lib1 设置了自己的搜索路径:
set_target_properties(${PREFIX}_src_lib PROPERTIES
INSTALL_RPATH "$ORIGIN"
)
这段配置位于 lib1 的 CMake 文件中,所以 ${PREFIX} 为 lib1。此处 $ORIGIN 表示 liblib1_src_lib.so 所在目录,用来查找同目录的 liblib2_src_lib.so。
Linux 上常见的 DT_RUNPATH 只用于当前对象的直接依赖,不会自动传给依赖库继续使用;因此只给主程序设置路径,仍可能找不到 lib1 所需的 lib2。相关规则见 Linux 动态链接器说明。
主程序和 lib1 分别设置路径后,保持 bin/ 与库目录的相对位置即可运行本模板,无需额外设置 LD_LIBRARY_PATH;整体复制安装目录后仍然适用。
8.Install the main program
install(TARGETS ${PROJECT_NAME}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
Function: Install executable files.
install(TARGETS ...) Common classifications:
| 类型 | 对应产物 | 常用安装目录 |
|---|---|---|
RUNTIME |
本模板的可执行文件 | ${CMAKE_INSTALL_BINDIR} |
LIBRARY |
Linux 动态库 .so |
${CMAKE_INSTALL_LIBDIR} |
ARCHIVE |
静态库 | ${CMAKE_INSTALL_LIBDIR} |
The main program is an executable file, so use:
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
The path after installation is usually:
install/linux-debug/bin/cmake_template
9.CMakeLists for lib1
src/lib1/CMakeLists.txt:
set(PREFIX "lib1")
file(GLOB_RECURSE ${PREFIX}_SRC_LIST CONFIGURE_DEPENDS
"${CMAKE_CURRENT_LIST_DIR}/src/*.c"
"${CMAKE_CURRENT_LIST_DIR}/src/*.cpp"
)
add_library(${PREFIX}_src_lib
${${PREFIX}_SRC_LIST}
)
target_include_directories(${PREFIX}_src_lib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/inc>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_link_libraries(${PREFIX}_src_lib
PUBLIC
project_options
PRIVATE
project_warnings
)
# ========================
# Third-party dependencies
# ========================
find_package(Eigen3 REQUIRED)
target_link_libraries(${PREFIX}_src_lib
PUBLIC
Eigen3::Eigen
)
# =======
# Install
# =======
# On Linux, find lib2 in the same installed library directory.
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set_target_properties(${PREFIX}_src_lib PROPERTIES
INSTALL_RPATH "$ORIGIN"
)
endif()
install(TARGETS ${PREFIX}_src_lib
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/inc/"
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
10.set(PREFIX "lib1")
set(PREFIX "lib1")
Purpose: Sets a local variable for easy reuse.
These following names will all be generated by PREFIX:
| Expression | After expansion |
|---|---|
${PREFIX}_SRC_LIST |
lib1_SRC_LIST |
${PREFIX}_src_lib |
lib1_src_lib |
If this file is copied to lib3, only the following needs to be changed:
set(PREFIX "lib3")
the target name becomes:
lib3_src_lib
Note: target names cannot be duplicated.lib1 and lib2 of the .cpp files can both be named eigen3_test.cpp, but CMake targets cannot both be named src_lib.
11.file(GLOB_RECURSE ...)
file(GLOB_RECURSE ${PREFIX}_SRC_LIST CONFIGURE_DEPENDS
"${CMAKE_CURRENT_LIST_DIR}/src/*.c"
"${CMAKE_CURRENT_LIST_DIR}/src/*.cpp"
)
Function: collect source code files.
Parameter explanation:
| Parameter | Function |
|---|---|
GLOB_RECURSE |
Recursively search subdirectories |
${PREFIX}_SRC_LIST |
Variable name to store results |
CONFIGURE_DEPENDS |
Trigger CMake reconfiguration when the list of source files changes |
"src/*.c" |
Match C files |
"src/*.cpp" |
Match C++ files |
What can be filled in:
file(GLOB_RECURSE MY_SOURCES CONFIGURE_DEPENDS
"${CMAKE_CURRENT_LIST_DIR}/src/*.cpp"
)
Alternatively, you can omit GLOB_RECURSE and manually list the files:
set(lib1_SRC_LIST
src/eigen3_test.cpp
)
Comparison of the two methods:
| Approach | Pros | Cons |
|---|---|---|
| Manually list files | Explicit and controllable | Adding new files requires manually editing CMake |
GLOB_RECURSE |
Templates are convenient; new files are collected automatically | The file list is less intuitive than handwriting |
For ease of use, this template uses GLOB_RECURSE CONFIGURE_DEPENDS.
12.Double-layer variable expansion
add_library(${PREFIX}_src_lib
${${PREFIX}_SRC_LIST}
)
Here is a potentially confusing notation:
${${PREFIX}_SRC_LIST}
If:
PREFIX = lib1
Then:
${PREFIX}_SRC_LIST
First becomes:
lib1_SRC_LIST
Retrieve the variable again:
${lib1_SRC_LIST}
Finally, the list of source code files is obtained.
13.add_library
add_library(${PREFIX}_src_lib
${${PREFIX}_SRC_LIST}
)
作用:创建库 target。本模板不显式填写类型,使用 BUILD_SHARED_LIBS 的设置:Linux 预设为动态库,Windows 预设为静态库。
Common types:
| 类型 | 产物 | 说明 |
|---|---|---|
SHARED |
Linux 下为 .so |
动态库 |
STATIC |
.a |
静态库 |
MODULE |
.so |
插件模块,不用于普通链接 |
OBJECT |
.o |
对象库 |
| 不写 | 由 BUILD_SHARED_LIBS 决定 |
本模板使用 |
If you want to change it to a static library:
add_library(${PREFIX}_src_lib STATIC
${${PREFIX}_SRC_LIST}
)
本模板已经采用由 preset 控制静态或动态的写法:
add_library(${PREFIX}_src_lib
${${PREFIX}_SRC_LIST}
)
Then set in the preset:
"BUILD_SHARED_LIBS": "ON"
14.target_include_directories
target_include_directories(${PREFIX}_src_lib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/inc>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
Purpose: Tells the compiler where the header files are located.
Here, PUBLIC is used because:
lib1When compiling on your own, you need to locate your own header files.- Link
lib1_src_libThe main program also needs to findlib1/eigen3_test.hpp。
14.1.BUILD_INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/inc>
Indicates that during the source code build phase, the include path is:
src/lib1/inc
So in the source code, you can write:
#include "lib1/eigen3_test.hpp"
Corresponds to actual file:
src/lib1/inc/lib1/eigen3_test.hpp
14.2.INSTALL_INTERFACE
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
Indicates that the include path after installation is:
include
Header file path after installation:
install/linux-debug/include/lib1/eigen3_test.hpp
Downstream can still write:
#include "lib1/eigen3_test.hpp"
14.3.Why put the header file in inc/lib1/
If both libraries place the header files as:
inc/eigen3_test.hpp
Main program writes:
#include "eigen3_test.hpp"
it becomes impossible to distinguish whether it comes from lib1 or lib2.
So the template uses:
lib1/inc/lib1/eigen3_test.hpp
lib2/inc/lib2/eigen3_test.hpp
Main program writes:
#include "lib1/eigen3_test.hpp"
#include "lib2/eigen3_test.hpp"
This is a very common engineering practice.
15.Link common options and warning
target_link_libraries(${PREFIX}_src_lib
PUBLIC
project_options
PRIVATE
project_warnings
)
Meaning:
project_optionsare library usage requirements, usePUBLIC.project_warningsare only used when compiling this library, usePRIVATE.
为什么 warning 不用 PUBLIC?
If the library is used by other projects in the future,PUBLIC warning will force downstream projects to use your warning settings as well. As a template,PRIVATE is gentler.
16.Third-party dependencies
# ========================
# Third-party dependencies
# ========================
find_package(Eigen3 REQUIRED)
target_link_libraries(${PREFIX}_src_lib
PUBLIC
Eigen3::Eigen
)
This block focuses on the third-party libraries used by the current module.
Principle:
- Whoever uses a third-party library, whoever
find_package. - Do not stuff all third-party libraries at the top level.
- When deleting a dependency, you can delete it cleanly just by looking at this block.
If lib1 uses Eigen while lib2 does not, then only in lib1/CMakeLists.txt write Eigen.
17.Install Library
install(TARGETS ${PREFIX}_src_lib
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
Function: Install library files.
Different platforms and different library types use different classifications:
| Classification | Common Linux artifacts | Directory |
|---|---|---|
LIBRARY |
.so Dynamic library |
${CMAKE_INSTALL_LIBDIR} |
ARCHIVE |
.a Static library |
${CMAKE_INSTALL_LIBDIR} |
RUNTIME |
Executable file | ${CMAKE_INSTALL_BINDIR} |
Although this template only supports Linux, keeping the three categories of syntax does no harm and also makes it easier to switch to a static library later.
18.Install header files
install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/inc/"
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
Function: Install all header files in the entire inc/ directory.
Note the trailing /:
"${CMAKE_CURRENT_LIST_DIR}/inc/"
Indicates the contents inside the installation inc directory, not the installation inc directory itself.
Source file:
src/lib1/inc/lib1/eigen3_test.hpp
After installation:
install/linux-debug/include/lib1/eigen3_test.hpp
If there is no trailing / at the end, the path structure may have one extra level, inc becoming an undesired form.
19.Relationship between lib2 and lib1
lib2/CMakeLists.txt 和 lib1/CMakeLists.txt 的源码收集、依赖管理与安装结构基本相同,模块前缀改为:
set(PREFIX "lib2")
当前 lib1 依赖同目录的 lib2,因此额外设置了 INSTALL_RPATH "$ORIGIN";lib2 使用的 Eigen 是头文件库,本例无需为它添加同样的动态库搜索路径。
So the target becomes:
lib2_src_lib
The header file path becomes:
lib2/eigen3_test.hpp
C++ Namespaces should also be separated:
namespace lib1 {
void run_eigen_vector_example();
}
namespace lib2 {
void run_eigen_matrix_example();
}
Avoid both libraries defining global functions:
void main_test();
Otherwise, symbol conflicts may occur during linking.
20.Add new library lib3
After copying src/lib1 as src/lib3 , the following places need to be changed:
src/lib3/CMakeLists.txt
set(PREFIX "lib3")
- Header file directory:
src/lib3/inc/lib3/xxx.hpp
- C++ Namespace:
namespace lib3 {
}
src/CMakeLists.txtAdd:
add_subdirectory(lib3)
target_link_libraries(${PROJECT_NAME}
PRIVATE
lib3_src_lib
)
In actual projects, it is recommended to put all add_subdirectory together, and also put all main program link libraries together, so the file will be clearer.
20.1.Windows 安装后的目录
Windows 主程序安装到 bin/,两个项目静态库安装到 lib/,公开头文件安装到 include/。源文件和函数声明仍与 Linux 共用。
lib1 和 lib2 的依赖关系继续通过 target_link_libraries 表达。日常练习选择 Windows 预设后直接构建、运行即可。