Skip to content

Wiki

Detailed Explanation of CMake for src and lib Modules

About 19 min read

Existing English translations were reused. 19 current source blocks remain in Simplified Chinese, without triggering paid translation.

This section covers three levels:

  1. src/CMakeLists.txt How to create the main program.
  2. src/lib1/CMakeLists.txt How to create a library.
  3. src/lib2/CMakeLists.txt Why it is similar to lib1 but 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:

  1. 主程序自己使用模板默认的 C++20 编译要求。
  2. 主程序自己开启 warning。
  3. 主程序不需要把这些要求再传给别人,因为可执行文件一般不会被其他 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:

  1. lib1When compiling on your own, you need to locate your own header files.
  2. Link lib1_src_lib The main program also needs to find lib1/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:

  1. project_options are library usage requirements, use PUBLIC.
  2. project_warnings are only used when compiling this library, use PRIVATE.

为什么 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:

  1. Whoever uses a third-party library, whoever find_package.
  2. Do not stuff all third-party libraries at the top level.
  3. 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:

  1. src/lib3/CMakeLists.txt
set(PREFIX "lib3")
  1. Header file directory:
src/lib3/inc/lib3/xxx.hpp
  1. C++ Namespace:
namespace lib3 {
}
  1. src/CMakeLists.txt Add:
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 预设后直接构建、运行即可。