第 22.3 節

Detailed CMake Explanation for src and lib Modules

0瀏覽次數0訪問次數--跳出率--平均停留

This section covers three levels:

  1. src/CMakeLists.txt How to create the main program.
  2. How to create a library.
  3. src/lib2/CMakeLists.txt and lib1 appear similar, but their target names must remain distinct.

src/CMakeLists.txt

File content:

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
)

set_target_properties(${PROJECT_NAME} PROPERTIES
  INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}"
)

install(TARGETS ${PROJECT_NAME}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

This file is responsible for the final executable.

add_executable

add_executable(${PROJECT_NAME}
  ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
)

Function: Create executable file target.

common practice

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 typically the name of the generated executable file.

This template is used for:

${PROJECT_NAME}

And PROJECT_NAME from the top level:

project(cmake_template VERSION 1.0.0 LANGUAGES C CXX)

So the executable file is called:

cmake_template

CMAKE_CURRENT_SOURCE_DIR

${CMAKE_CURRENT_SOURCE_DIR}/main.cpp

Indicates the source code directory where the current CMakeLists.txt is located.

In src/CMakeLists.txt:

CMAKE_CURRENT_SOURCE_DIR = 项目根目录/src

So:

${CMAKE_CURRENT_SOURCE_DIR}/main.cpp

that is:

项目根目录/src/main.cpp
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 via target_link_libraries.

Here we use PRIVATE, to indicate:

  1. The main program itself uses C++17 compilation requirements.
  2. The main program automatically enables warnings.
  3. The main program doesn't need to pass these requirements to others, because executable files are generally not linked by other targets.

Add subdirectory

add_subdirectory(lib1)
add_subdirectory(lib2)

Purpose: Access src/lib1 and src/lib2, and read their respective CMakeLists.txt.

After executing these two lines, the following two targets become available:

lib1_src_lib
lib2_src_lib

So usually first:

add_subdirectory(lib1)
add_subdirectory(lib2)

Then set the dependencies between the libraries and the libraries to be linked by the main program:

target_link_libraries(lib1_src_lib
  PRIVATE
    lib2_src_lib
)

target_link_libraries(${PROJECT_NAME}
  PRIVATE
    lib1_src_lib
    lib2_src_lib
)

Establish one-way dependency where lib1 calls lib2

This template now uses lib1 calling lib2 to create 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;
}

运行结果(完整工程链接 lib1lib2 后):

[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 then 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 explicitly link lib2_src_lib:

target_link_libraries(lib1_src_lib
  PRIVATE
    lib2_src_lib
)

This sentence is recommended to be placed in:

add_subdirectory(lib1)
add_subdirectory(lib2)

After that. Because only after executing add_subdirectory(lib1) and add_subdirectory(lib2) do the targets lib1_src_lib and lib2_src_lib already exist.

Here, PRIVATE is used to indicate that lib2 is just a library needed during lib1's own implementation. CMake will allow lib1 to find lib2/eigen3_test.hpp during compilation and also find the function implementations in lib2 during linking, but it won't force all targets linking lib1 to automatically use lib2's header files.

If in the future the public header of lib1 directly includes the header of lib2, or if 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, this is a one-way dependency:

lib1 -> lib2

Do not let lib2 link back to lib1. If two libraries really need to use the same piece of code, the shared components should typically be extracted into a new library such as common.

target_link_libraries(${PROJECT_NAME}
  PRIVATE
    lib1_src_lib
    lib2_src_lib
)

Function: Allows the main program to link the two example libraries in the template.

Currently main.cpp only directly calls:

lib1::run_eigen_vector_example();

while lib1::run_eigen_vector_example() in turn calls internally:

lib2::run_eigen_matrix_example();

So the actual calling 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 compiled into:

lib1_src_lib
lib2_src_lib

So the main program must at least link to lib1_src_lib, while lib1_src_lib in turn requires a link to lib2_src_lib.

This template still has the main program link both lib1_src_lib and lib2_src_lib here to preserve the intuitive structure of "the main program mounting two example libraries." In practical projects, if main.cpp does not directly call lib2 at all, you can also have the main program link only lib1_src_lib, allowing the dependency lib1 -> lib2 to bring in lib2.

INSTALL_RPATH

set_target_properties(${PROJECT_NAME} PROPERTIES
  INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}"
)

Purpose: Configures how installed executables find dynamic libraries at runtime.

Install the template's library to:

install/linux-debug/lib64/

Executable files 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 represents the directory where the executable file is located:

install/linux-debug/bin

So:

$ORIGIN/../lib64

that is:

install/linux-debug/lib64

其中 ${CMAKE_INSTALL_LIBDIR} could be lib or lib64.

Common fillable values:

writing methodExplanation
"$ORIGIN/../${CMAKE_INSTALL_LIBDIR}"Recommended: finding libraries relative to installation
"/usr/local/lib"Hardcoded system paths are not suitable for templates.
""If not set, RPATH requires dependency on system library paths or LD_LIBRARY_PATH.

Since this template has already set the RPATH, there's no need for the old script to set the source environment variable.

Install the main program.

install(TARGETS ${PROJECT_NAME}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

Purpose: Install executable files.

install(TARGETS ...) Common categories:

typecorresponding productsCommon installation directories
RUNTIMEExecutable file, Windows Dynamic Link Library${CMAKE_INSTALL_BINDIR}
LIBRARYLinux dynamic library .so${CMAKE_INSTALL_LIBDIR}
ARCHIVEstatic library .a${CMAKE_INSTALL_LIBDIR}

The main program is an executable file, so it uses:

RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}

After installation, the path is typically:

install/linux-debug/bin/cmake_template

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 SHARED
  ${${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
# =======
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}
)

set(PREFIX "lib1")

set(PREFIX "lib1")

Function: Sets a local variable for easier reuse.

These subsequent names will be generated by PREFIX:

Expressionafter expanding
${PREFIX}_SRC_LISTlib1_SRC_LIST
${PREFIX}_src_liblib1_src_lib

If you are copying this file to lib3, you only need to change:

set(PREFIX "lib3")

the target name will become:

lib3_src_lib

Note: Target names must be unique. lib1 and lib2's .cpp files can both be named eigen3_test.cpp, but their CMake targets cannot both be named src_lib.

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:

parametereffect
GLOB_RECURSERecursively search subdirectories
${PREFIX}_SRC_LISTvariable name for storing the result
CONFIGURE_DEPENDSWhen source file list changes, trigger CMake reconfiguration
"src/*.c"Match C file
"src/*.cpp"Match C++ files

What can be filled in:

file(GLOB_RECURSE MY_SOURCES CONFIGURE_DEPENDS
  "${CMAKE_CURRENT_LIST_DIR}/src/*.cpp"
)

You can also skip GLOB_RECURSE and manually list files:

set(lib1_SRC_LIST
  src/eigen3_test.cpp
)

Comparison of two methods:

writing methodAdvantagesDisadvantages
manually list filesClear and controllableNew files need to be manually added to CMake.
GLOB_RECURSETemplates are convenient, new files are automatically collected.File lists aren't as intuitive as handwriting.

This template uses GLOB_RECURSE CONFIGURE_DEPENDS for ease of use.

Double-layer variable expansion

add_library(${PREFIX}_src_lib SHARED
  ${${PREFIX}_SRC_LIST}
)

Here is a commonly confusing writing format:

${${PREFIX}_SRC_LIST}

If:

PREFIX = lib1

So:

${PREFIX}_SRC_LIST

First, become:

lib1_SRC_LIST

Assign another variable:

${lib1_SRC_LIST}

Finally, obtain the list of source code files.

add_library

add_library(${PREFIX}_src_lib SHARED
  ${${PREFIX}_SRC_LIST}
)

Function: Creates the library target.

Common types:

typeproductExplanation
SHARED.soDynamic library, used by this template
STATIC.aStatic library
MODULE.soPlugin module, not used for regular links.
OBJECT.oobject library
Understood. I'll refrain from writing any further translations.determined by BUILD_SHARED_LIBS

If you want to switch to a static library, you can use compiler flags like -static or -static-libgcc during compilation, or adjust the build system (such as CMake or Makefile) to generate a .a (Linux/macOS) or .lib (Windows) file.

add_library(${PREFIX}_src_lib STATIC
  ${${PREFIX}_SRC_LIST}
)

If you want users to control static or dynamic behavior with a preset, you can use:

add_library(${PREFIX}_src_lib
  ${${PREFIX}_SRC_LIST}
)

Then set in preset:

"BUILD_SHARED_LIBS": "ON"

target_include_directories

target_include_directories(${PREFIX}_src_lib
  PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/inc>
    $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

Function: Tells the compiler where to find header files.

We use PUBLIC here because:

  1. When compiling lib1, you need to locate your own header files.
  2. The main program linked at lib1_src_lib also needs to locate lib1/eigen3_test.hpp.

BUILD_INTERFACE

$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/inc>

It indicates that during the source code build stage, the include path is:

src/lib1/inc

So in the source code, you can write:

#include "lib1/eigen3_test.hpp"

Corresponding real file:

src/lib1/inc/lib1/eigen3_test.hpp

INSTALL_INTERFACE

$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>

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"

Why are header files placed in inc/lib1/

If both libraries place the header files as:

inc/eigen3_test.hpp

Main program writes:

#include "eigen3_test.hpp"

it would be difficult to tell whether it comes from lib1 or lib2.

So the template usage:

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.

target_link_libraries(${PREFIX}_src_lib
  PUBLIC
    project_options
  PRIVATE
    project_warnings
)

Meaning:

  1. project_options is the library's usage requirement; use PUBLIC.
  2. project_warnings is only used when compiling this library. Please use PRIVATE.

Why doesn't warning use PUBLIC?

If the library is used by other projects in the future, the PUBLIC warning would force downstream users to adopt your warning settings. As a template, PRIVATE is much softer.

Third-party dependencies

# ========================
# Third-party dependencies
# ========================
find_package(Eigen3 REQUIRED)

target_link_libraries(${PREFIX}_src_lib
  PUBLIC
    Eigen3::Eigen
)

This block focuses on consolidating the third-party libraries used in the current module.

Principle

  1. Whoever uses third-party libraries should find_package.
  2. Not dumping all third-party libraries at the top level.
  3. When deleting a specific dependency, you can ensure it's completely removed by looking only at this block.

If lib1 uses Eigen, but lib2 doesn't, then only write Eigen in lib1/CMakeLists.txt.

Install a 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 library types will use different classifications:

CategoryCommon Linux productsTable of Contents
LIBRARY.so dynamic library${CMAKE_INSTALL_LIBDIR}
ARCHIVE.a Static library${CMAKE_INSTALL_LIBDIR}
RUNTIMEexecutable file${CMAKE_INSTALL_BINDIR}

Although this template only supports Linux, it's still beneficial to retain the three types of writing, making it easier to switch to static libraries in the future.

install header files

install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/inc/"
  DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

Function: Install header files in the entire inc/ directory.

Note the path ending with /:

"${CMAKE_CURRENT_LIST_DIR}/inc/"

This indicates installing the contents of the inc directory, rather than installing the inc directory itself.

Source files:

src/lib1/inc/lib1/eigen3_test.hpp

After installation:

install/linux-debug/include/lib1/eigen3_test.hpp

If the final / is not present, the path structure might have an additional layer inc, resulting in an unwanted form.

The relationship between lib2 and lib1

lib2/CMakeLists.txt is basically the same as lib1/CMakeLists.txt, except:

set(PREFIX "lib2")

Therefore, 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.

Add new library lib3

Copy src/lib1 to src/lib3, then these 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 lib3 {
}
  1. src/CMakeLists.txt Add:
add_subdirectory(lib3)

target_link_libraries(${PROJECT_NAME}
  PRIVATE
    lib3_src_lib
)

In actual projects, it's recommended to put all add_subdirectory together, and all main program libraries together as well, so the files will be clearer.

音乐页