Wiki
src與lib模組CMake詳解
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
本節講三個層次:
src/CMakeLists.txt如何建立主程式。src/lib1/CMakeLists.txt如何建立庫。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_options 和 project_warnings 不是普通庫,但它們是 CMake target,所以也通過 target_link_libraries 傳遞。
這裡用 PRIVATE,表示:
- 主程式自己使用 C++17 編譯要求。
- 主程式自己開啟 warning。
- 主程式不需要把這些要求再傳給別人,因為執行檔一般不會被其他 target 連結。
4.新增子目錄
add_subdirectory(lib1)
add_subdirectory(lib2)
作用:進入 src/lib1 和 src/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;
}
執行結果(完整工程連結 lib1 和 lib2 後):
[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_lib、lib2_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_lib 和 lib2_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 名不能重名。lib1 和 lib2 的 .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,因為:
lib1自己編譯時需要找到自己的標頭檔案。- 連結
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
)
含義:
project_options是庫的使用要求,使用PUBLIC。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
)
這個區塊集中寫當前模組使用的第三方庫。
原則:
- 誰用第三方庫,誰
find_package。 - 不把所有第三方庫塞到頂層。
- 刪除某個依賴時,只看這個區塊就能刪乾淨。
如果 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.txt 和 lib1/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/lib1 為 src/lib3 後,需要改這些地方:
src/lib3/CMakeLists.txt
set(PREFIX "lib3")
- 標頭檔案目錄:
src/lib3/inc/lib3/xxx.hpp
- C++ 名稱空間:
namespace lib3 {
}
src/CMakeLists.txt新增:
add_subdirectory(lib3)
target_link_libraries(${PROJECT_NAME}
PRIVATE
lib3_src_lib
)
實際專案中,建議把所有 add_subdirectory 放在一起,所有主程式連結庫也放在一起,檔案會更清楚。