src與lib模塊CMake詳解
本節講三個層次:
src/CMakeLists.txt如何創建主程序。src/lib1/CMakeLists.txt如何創建庫。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_options 和 project_warnings 不是普通庫,但它們是 CMake target,所以也通過 target_link_libraries 傳遞。
這裏用 PRIVATE,表示:
- 主程序自己使用 C++17 編譯要求。
- 主程序自己開啓 warning。
- 主程序不需要把這些要求再傳給別人,因為可執行文件一般不會被其他 target 鏈接。
添加子目錄
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
)
讓 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 之類的新庫中。
鏈接 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。
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} |
LIBRARY | Linux 動態庫 .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_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。
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,因為:
lib1自己編譯時需要找到自己的頭文件。- 鏈接
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
)
含義:
project_options是庫的使用要求,使用PUBLIC。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
)
這個區塊集中寫當前模塊使用的第三方庫。
原則:
- 誰用第三方庫,誰
find_package。 - 不把所有第三方庫塞到頂層。
- 刪除某個依賴時,只看這個區塊就能刪乾淨。
如果 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.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();
否則鏈接時可能發生符號衝突。
添加新庫 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 放在一起,所有主程序鏈接庫也放在一起,文件會更清楚。