第 22.3 節

src與lib模塊CMake詳解

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

本節講三個層次:

  1. src/CMakeLists.txt 如何創建主程序。
  2. src/lib1/CMakeLists.txt 如何創建庫。
  3. src/lib2/CMakeLists.txt 為什麼和 lib1 類似但 target 不能重名。

src/CMakeLists.txt

文件內容:

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}
)

這個文件負責最終可執行文件。

add_executable

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

作用:創建可執行文件 target。

常見寫法:

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

第一個參數是 target 名,也通常是生成的可執行文件名。

本模板使用:

${PROJECT_NAME}

PROJECT_NAME 來自頂層:

project(cmake_template VERSION 1.0.0 LANGUAGES C CXX)

所以可執行文件叫:

cmake_template

CMAKE_CURRENT_SOURCE_DIR

${CMAKE_CURRENT_SOURCE_DIR}/main.cpp

表示當前 CMakeLists.txt 所在源碼目錄。

src/CMakeLists.txt 中:

CMAKE_CURRENT_SOURCE_DIR = 项目根目录/src

所以:

${CMAKE_CURRENT_SOURCE_DIR}/main.cpp

就是:

项目根目录/src/main.cpp

給主程序鏈接公共選項

target_link_libraries(${PROJECT_NAME}
  PRIVATE
    project_options
    project_warnings
)

雖然 project_optionsproject_warnings 不是普通庫,但它們是 CMake target,所以也通過 target_link_libraries 傳遞。

這裏用 PRIVATE,表示:

  1. 主程序自己使用 C++17 編譯要求。
  2. 主程序自己開啓 warning。
  3. 主程序不需要把這些要求再傳給別人,因為可執行文件一般不會被其他 target 鏈接。

添加子目錄

add_subdirectory(lib1)
add_subdirectory(lib2)

作用:進入 src/lib1src/lib2,讀取它們各自的 CMakeLists.txt

執行完這兩行之後,下面兩個 target 才存在:

lib1_src_lib
lib2_src_lib

所以通常先:

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
)

讓 lib1 單向調用 lib2

本模板現在用 lib1 調用 lib2 做一個單向依賴示例。src/main.cpp 只直接調用 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

然後在 src/lib1/src/eigen3_test.cpp 中,lib1 再調用 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

因此 lib1_src_lib 必須顯式鏈接 lib2_src_lib

target_link_libraries(lib1_src_lib
  PRIVATE
    lib2_src_lib
)

這句話建議放在:

add_subdirectory(lib1)
add_subdirectory(lib2)

之後。因為執行完 add_subdirectory(lib1)add_subdirectory(lib2) 後,lib1_src_liblib2_src_lib 這兩個 target 才都已經存在。

這裏用 PRIVATE,表示 lib2 只是 lib1 自己實現時需要用到的庫。CMake 會讓 lib1 在編譯時找到 lib2/eigen3_test.hpp,也會在鏈接時找到 lib2 裏的函數實現,但不會強迫所有鏈接 lib1 的目標都自動使用 lib2 的頭文件。

如果以後 lib1 的公開頭文件裏直接包含了 lib2 的頭文件,或者公開接口裏使用了 lib2 的類型,就應該改成 PUBLIC

target_link_libraries(lib1_src_lib
  PUBLIC
    lib2_src_lib
)

注意,這裏是單向依賴:

lib1 -> lib2

不要再反過來讓 lib2 鏈接 lib1。如果兩個庫真的需要使用同一段代碼,通常應該把公共部分抽到 common 之類的新庫中。

鏈接 lib1 和 lib2

target_link_libraries(${PROJECT_NAME}
  PRIVATE
    lib1_src_lib
    lib2_src_lib
)

作用:讓主程序鏈接模板中的兩個示例庫。

當前 main.cpp 只直接調用:

lib1::run_eigen_vector_example();

lib1::run_eigen_vector_example() 內部又會調用:

lib2::run_eigen_matrix_example();

所以這個模板的實際調用路徑是:

main.cpp -> lib1 -> lib2

函數實現位於:

src/lib1/src/eigen3_test.cpp
src/lib2/src/eigen3_test.cpp

它們分別被編譯進:

lib1_src_lib
lib2_src_lib

所以主程序至少需要鏈接 lib1_src_lib,而 lib1_src_lib 又需要鏈接 lib2_src_lib

本模板這裏仍然讓主程序同時鏈接 lib1_src_liblib2_src_lib,是為了保留"主程序掛載兩個示例庫"的直觀結構。實際項目中,如果 main.cpp 完全不直接調用 lib2,也可以只讓主程序鏈接 lib1_src_lib,由 lib1 -> lib2 這條依賴關係帶出 lib2

INSTALL_RPATH

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

作用:設置安裝後的可執行文件運行時如何找到動態庫。

本模板的庫安裝到:

install/linux-debug/lib64/

可執行文件安裝到:

install/linux-debug/bin/

運行:

./install/linux-debug/bin/cmake_template

程序需要找到:

install/linux-debug/lib64/liblib1_src_lib.so
install/linux-debug/lib64/liblib2_src_lib.so

$ORIGIN 表示可執行文件所在目錄,也就是:

install/linux-debug/bin

所以:

$ORIGIN/../lib64

就是:

install/linux-debug/lib64

其中 ${CMAKE_INSTALL_LIBDIR} 可能是 lib,也可能是 lib64

常見可填值:

寫法説明
"$ORIGIN/../${CMAKE_INSTALL_LIBDIR}"推薦,安裝後相對查找庫
"/usr/local/lib"寫死系統路徑,不適合模板
""不設置 RPATH,需要依賴系統庫路徑或 LD_LIBRARY_PATH

因為本模板已經設置 RPATH,所以不需要舊腳本去 source 環境變量。

安裝主程序

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

作用:安裝可執行文件。

install(TARGETS ...) 常見分類:

類型對應產物常用安裝目錄
RUNTIME可執行文件,Windows DLL${CMAKE_INSTALL_BINDIR}
LIBRARYLinux 動態庫 .so${CMAKE_INSTALL_LIBDIR}
ARCHIVE靜態庫 .a${CMAKE_INSTALL_LIBDIR}

主程序是可執行文件,所以使用:

RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}

安裝後路徑通常是:

install/linux-debug/bin/cmake_template

lib1 的 CMakeLists

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")

作用:設置一個局部變量,方便複用。

後面這些名字都會由 PREFIX 生成:

表達式展開後
${PREFIX}_SRC_LISTlib1_SRC_LIST
${PREFIX}_src_liblib1_src_lib

如果複製這個文件給 lib3,只需要改:

set(PREFIX "lib3")

target 名就會變成:

lib3_src_lib

注意:target 名不能重名。lib1lib2.cpp 文件可以都叫 eigen3_test.cpp,但 CMake target 不能都叫 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"
)

作用:收集源碼文件。

參數解釋:

參數作用
GLOB_RECURSE遞歸查找子目錄
${PREFIX}_SRC_LIST保存結果的變量名
CONFIGURE_DEPENDS源文件列表變化時,觸發 CMake 重新配置
"src/*.c"匹配 C 文件
"src/*.cpp"匹配 C++ 文件

可以填什麼:

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

也可以不用 GLOB_RECURSE,手動列文件:

set(lib1_SRC_LIST
  src/eigen3_test.cpp
)

兩種方式對比:

寫法優點缺點
手動列文件明確、可控新增文件要手動改 CMake
GLOB_RECURSE模板方便,新增文件自動收集文件列表不如手寫直觀

本模板為了易用,使用 GLOB_RECURSE CONFIGURE_DEPENDS

雙層變量展開

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

這裏有一個容易困惑的寫法:

${${PREFIX}_SRC_LIST}

如果:

PREFIX = lib1

那麼:

${PREFIX}_SRC_LIST

先變成:

lib1_SRC_LIST

再取變量:

${lib1_SRC_LIST}

最終得到源碼文件列表。

add_library

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

作用:創建庫 target。

常見類型:

類型產物説明
SHARED.so動態庫,本模板使用
STATIC.a靜態庫
MODULE.so插件模塊,不用於普通鏈接
OBJECT.o對象庫
不寫BUILD_SHARED_LIBS 決定

如果想改成靜態庫:

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

如果希望用户用 preset 控制靜態或動態,可以寫:

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

然後在 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}>
)

作用:告訴編譯器頭文件在哪裏。

這裏使用 PUBLIC,因為:

  1. lib1 自己編譯時需要找到自己的頭文件。
  2. 鏈接 lib1_src_lib 的主程序也需要找到 lib1/eigen3_test.hpp

BUILD_INTERFACE

$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/inc>

表示在源碼構建階段,include 路徑是:

src/lib1/inc

所以源碼裏可以寫:

#include "lib1/eigen3_test.hpp"

對應真實文件:

src/lib1/inc/lib1/eigen3_test.hpp

INSTALL_INTERFACE

$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>

表示安裝後的 include 路徑是:

include

安裝後頭文件路徑:

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

下游仍然可以寫:

#include "lib1/eigen3_test.hpp"

為什麼頭文件放 inc/lib1/

如果兩個庫都把頭文件放成:

inc/eigen3_test.hpp

主程序寫:

#include "eigen3_test.hpp"

就會分不清到底來自 lib1 還是 lib2

所以模板使用:

lib1/inc/lib1/eigen3_test.hpp
lib2/inc/lib2/eigen3_test.hpp

主程序寫:

#include "lib1/eigen3_test.hpp"
#include "lib2/eigen3_test.hpp"

這是一種很常見的工程寫法。

鏈接公共選項和 warning

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

含義:

  1. project_options 是庫的使用要求,使用 PUBLIC
  2. project_warnings 只是編譯本庫時使用,使用 PRIVATE

為什麼 warning 不用 PUBLIC

如果庫未來被其他項目使用,PUBLIC warning 會強迫下游也使用你的 warning 設置。作為模板,PRIVATE 更温和。

Third-party dependencies

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

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

這個區塊集中寫當前模塊使用的第三方庫。

原則:

  1. 誰用第三方庫,誰 find_package
  2. 不把所有第三方庫塞到頂層。
  3. 刪除某個依賴時,只看這個區塊就能刪乾淨。

如果 lib1 用 Eigen,而 lib2 不用,那麼只在 lib1/CMakeLists.txt 寫 Eigen 即可。

安裝庫

install(TARGETS ${PREFIX}_src_lib
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

作用:安裝庫文件。

不同平台、不同庫類型會用到不同分類:

分類Linux 常見產物目錄
LIBRARY.so 動態庫${CMAKE_INSTALL_LIBDIR}
ARCHIVE.a 靜態庫${CMAKE_INSTALL_LIBDIR}
RUNTIME可執行文件${CMAKE_INSTALL_BINDIR}

雖然本模板只支持 Linux,但保留三類寫法沒有壞處,也方便以後改靜態庫。

安裝頭文件

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

作用:安裝整個 inc/ 目錄裏的頭文件。

注意路徑末尾的 /

"${CMAKE_CURRENT_LIST_DIR}/inc/"

表示安裝 inc 目錄裏面的內容,而不是安裝 inc 目錄本身。

源文件:

src/lib1/inc/lib1/eigen3_test.hpp

安裝後:

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

如果沒有末尾 /,路徑結構可能會多一層 inc,變成不想要的形式。

lib2 和 lib1 的關係

lib2/CMakeLists.txtlib1/CMakeLists.txt 基本一樣,只是:

set(PREFIX "lib2")

所以 target 變成:

lib2_src_lib

頭文件路徑變成:

lib2/eigen3_test.hpp

C++ 命名空間也要分開:

namespace lib1 {
void run_eigen_vector_example();
}
namespace lib2 {
void run_eigen_matrix_example();
}

避免兩個庫都定義全局函數:

void main_test();

否則鏈接時可能發生符號衝突。

添加新庫 lib3

複製 src/lib1src/lib3 後,需要改這些地方:

  1. src/lib3/CMakeLists.txt
set(PREFIX "lib3")
  1. 頭文件目錄:
src/lib3/inc/lib3/xxx.hpp
  1. C++ 命名空間:
namespace lib3 {
}
  1. src/CMakeLists.txt 添加:
add_subdirectory(lib3)

target_link_libraries(${PROJECT_NAME}
  PRIVATE
    lib3_src_lib
)

實際項目中,建議把所有 add_subdirectory 放在一起,所有主程序鏈接庫也放在一起,文件會更清楚。

音乐页