跳到正文

Wiki

src與lib模組CMake詳解

約 9 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

本節講三個層次:

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

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

這個檔案負責最終執行檔。

2.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

2.1.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

3.給主程式連結公共選項

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 連結。

4.新增子目錄

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
)

5.讓 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 之類的新庫中。

6.連結 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

7.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 環境變數。

8.安裝主程式

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

作用:安裝執行檔。

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

型別 對應產物 常用安裝目錄
RUNTIME 執行檔,Windows DLL ${CMAKE_INSTALL_BINDIR}
LIBRARY Linux 動態庫 .so ${CMAKE_INSTALL_LIBDIR}
ARCHIVE 靜態庫 .a ${CMAKE_INSTALL_LIBDIR}

主程式是執行檔,所以使用:

RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}

安裝後路徑通常是:

install/linux-debug/bin/cmake_template

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

10.set(PREFIX "lib1")

set(PREFIX "lib1")

作用:設定一個區域性變數,方便複用。

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

表示式 展開後
${PREFIX}_SRC_LIST lib1_SRC_LIST
${PREFIX}_src_lib lib1_src_lib

如果複製這個檔案給 lib3,只需要改:

set(PREFIX "lib3")

target 名就會變成:

lib3_src_lib

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

作用:收集原始碼檔案。

引數解釋:

引數 作用
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

12.雙層變數展開

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

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

${${PREFIX}_SRC_LIST}

如果:

PREFIX = lib1

那麼:

${PREFIX}_SRC_LIST

先變成:

lib1_SRC_LIST

再取變數:

${lib1_SRC_LIST}

最終得到原始碼檔案列表。

13.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"

14.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

14.1.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

14.2.INSTALL_INTERFACE

$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>

表示安裝後的 include 路徑是:

include

安裝後頭檔案路徑:

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

下游仍然可以寫:

#include "lib1/eigen3_test.hpp"

14.3.為什麼標頭檔案放 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"

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

15.連結公共選項和 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 更溫和。

16.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 即可。

17.安裝庫

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,但保留三類寫法沒有壞處,也方便以後改靜態庫。

18.安裝標頭檔案

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,變成不想要的形式。

19.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();

否則連結時可能發生符號衝突。

20.新增新庫 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 放在一起,所有主程式連結庫也放在一起,檔案會更清楚。