现代 CMake 模块化项目管理指南
現代 CMake 模塊化項目管理指南
參考小彭老師的視頻教程整理筆記,學習同時方便快速查閱,視頻鏈接如下
【公開課】現代 CMake 模塊化項目管理指南【C/C++】
對應課程 PPT 和源碼見
https://github.com/parallel101/course
文件/目錄組織規范
完整案例參考源碼倉庫
https://github.com/parallel101/course/tree/master/16/00
推薦的目錄組織方式
.
├── biology
│?? ├── CMakeLists.txt
│?? ├── include
│?? │?? └── biology
│?? │?? └── Animal.h
│?? └── src
│?? └── Animal.cpp
├── CMakeLists.txt
└── pybmain
├── CMakeLists.txt
├── include
│?? └── pybmain
│?? └── myutils.h
└── src
└── main.cpp
第一點,劃分了 biology 和 pybmain 兩個子項目,每個子項目在各自目錄下各有自己的 CMakeLists.txt 文件。
第二點,所有子項目中都被劃分為了 include 和 src 兩個子目錄,分別用來放頭文件和源碼文件,而其中 include 目錄又套了一層項目名,這樣可以避免頭文件名稱沖突。子項目的 CMakeLists.txt 文件中需要使用
target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
源碼文件中這樣寫,
#include <biology/Animal.h>
#include <pybmain/myutils.h>
第三點,推薦每個模塊都有自己的命名空間,頭文件中需要
#pragma once
namespace biology {
class Animal {
//...
};
}
源碼文件中需要
#include <biology/Animal.h>
namespace biology {
//...
};
根項目的 CMakeLists.txt 配置
cmake_minimum_required(VERSION 3.15)
if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
set(CMake_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
project(CppCMakeDemo LANGUAGES CXX)
add_subdirectory(biology)
add_subdirectory(pybmain)
最后兩行是關鍵,用來添加子項目,會調用子項目的 CMakeLists.txt 文件。
子項目的 CMakeLists.txt 配置
庫文件的配置
# biology/CMakeLists.txt
file(GLOB_RECURSE srcs CONFIGURE_DEPENDS src/*.cpp include/*.h)
add_library(biology STATIC ${srcs})
target_include_directories(biology PUBLIC include)
首先使用 GLOB_RECURSE 命令獲取所有源碼文件,然后使用 add_library 命令添加靜態庫,最后使用 target_include_directories 命令添加頭文件搜索路徑。
-
PUBLIC修飾符表示這個頭文件搜索路徑會被暴露給其他依賴這個庫的項目,鏈接了 biology 庫的 pybmain 項目也可以共享這個路徑。 - 注意到我們將 .h 文件也一并添加到了
add_library命令中,這樣可以確保 .h 文件也會被添加到 IDE 中,方便查看。 -
GLOB_RECURSE相比GLOB允許匹配嵌套的目錄。 -
CONFIGURE_DEPENDS選項表示如果源碼文件發生變化,cmake --build build會檢測目錄是否更新,有新文件則自動重新運行cmake -B build重新配置項目。
可執行文件的配置
# pybmain/CMakeLists.txt
file(GLOB_RECURSE srcs CONFIGURE_DEPENDS src/*.cpp include/*.h)
add_executable(pybmain ${srcs})
target_include_directories(pybmain PUBLIC include)
target_link_libraries(pybmain PUBLIC biology)
基本和庫文件的配置一致,只是使用 add_executable 命令添加可執行文件,使用 target_link_libraries 命令鏈接庫文件。
CMake 的 include 功能
和 C/C++ 的 #include 一樣,CMake 也有一個 include 命令,CMake 會在 CMAKE_MODULE_PATH 中搜索相應的 XXX.cmake 文件。
# ./CMakeLists.txt
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
project(CppCMakeDemo LANGUAGES CXX)
include(MyUsefulFuncs)
# cmake/MyUsefulFuncs.cmake
macro (my_add_target name type)
# 用法:my_add_target(pybmain EXECUTABLE)
file(GLOB_RECURSE srcs CONFIGURE_DEPENDS src/*.cpp src/*.h)
if ("${type}" MATCHES "EXECUTABLE")
add_executable(${name} ${srcs})
else()
add_library(${name} ${type} ${srcs})
endif()
target_include_directories(${name} PUBLIC include)
endmacro()
set(SOME_USEFUL_GLOBAL_VAR ON)
set(ANOTHER_USEFUL_GLOBAL_VAR OFF)
macro 和 function 的區別
- macro 相當于直接把代碼粘貼過去,直接訪問調用者的作用域。這里寫的相對路徑 include 和 src,是基于調用者所在路徑。
- function 則是會創建一個閉包,優先訪問定義者的作用域。這里寫的相對路徑 include 和 src,則是基于定義者所在路徑。
include 和 add_subdirectory 的區別
- include 相當于直接把代碼粘貼過去,直接訪問調用者的作用域。這里創建的變量和外面共享,直接 set(key val) 則調用者也有 ${key} 這個變量了。
- function 中則是基于定義者所在路徑,優先訪問定義者的作用域。這里需要 set(key val PARENT_SCOPE) 才能修改到外面的變量。
第三方庫/依賴項配置
主要講解 find_package 命令,其官方文檔為
https://cmake.org/cmake/help/latest/command/find_package.html
用法舉例
# 查找名為 OpenCV 的包,找不到不報錯,事后可以通過 ${OpenCV_FOUND} 查詢是否找到。
find_package(OpenCV)
# 查找名為 OpenCV 的包,找不到不報錯,也不打印任何信息。
find_package(OpenCV QUIET)
# 查找名為 OpenCV 的包,找不到就報錯(并終止 cmake 進程,不再繼續往下執行)。
find_package(OpenCV REQUIRED) # 最常見用法
# 查找名為 OpenCV 的包,找不到就報錯,且必須具有 OpenCV::core 和 OpenCV::videoio 這兩個組件,如果沒有這兩個組件也會報錯。
find_package(OpenCV REQUIRED COMPONENTS core videoio)
# 查找名為 OpenCV 的包,找不到就報錯,可具有 OpenCV::core 和 OpenCV::videoio 這兩個組件,沒有這兩組件不會報錯,通過 ${OpenCV_core_FOUND} 查詢是否找到 core 組件。
find_package(OpenCV REQUIRED OPTIONAL_COMPONENTS core videoio)
find_package 原理
實際上 find_package(OpenCV) 是在找一個叫做 OpenCVConfig.cmake 的文件,這個文件是 OpenCV 項目提供的,用來告訴 cmake OpenCV 的安裝路徑和組件信息。
這個包配置文件由第三方庫作者提供,在安裝這個包時一并安裝到系統中的,一般的約定是將其安裝到 /usr/lib/cmake/XXX/ 目錄下,其中 XXX 為包名。
Windows 系統下的搜索路徑
<prefix>/
<prefix>/cmake/
<prefix>/<name>*/
<prefix>/<name>*/cmake/
<prefix>/<name>*/(lib/<arch>|lib*|share)/cmake/<name>*/
<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/
<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/cmake/
其中 <prefix> 是變量 ${CMAKE_PREFIX_PATH},Windows 平臺默認為 C:/Program Files,<name> 是在 find_package(<name> REQUIRED) 命令中指定的包名,<arch> 是系統的架構名。
Unix 系統下的搜索路徑
<prefix>/(lib/<arch>|lib*|share)/cmake/<name>*/
<prefix>/(lib/<arch>|lib*|share)/<name>*/
<prefix>/(lib/<arch>|lib*|share)/<name>*/cmake/
<prefix>/<name>*/(lib/<arch>|lib*|share)/cmake/<name>*/
<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/
<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/cmake/
其中 <prefix> 是變量 ${CMAKE_PREFIX_PATH},Unix 平臺默認為 /usr,<name> 是你在 find_package(<name> REQUIRED) 命令中指定的包名,<arch> 是系統的架構,例如 x86_64-linux-gnu 或 i386-linux-gnu(用于伺候 Ubuntu 喜歡把庫文件套娃在 /usr/lib/x86_64-linux-gnu 目錄下)。
另外 <name> 可以有額外后綴,例如 find_package(Qt5) 可以在 Qt5.12.1/cmake 或者 Qt5xxx/cmake 目錄下找到 Qt5Config.cmake 文件。
非標準路徑安裝的庫
想讓 CMake 找到非標準路徑安裝的庫,本質上是定義好 NAME_DIR 變量,告訴 CMake 庫文件的路徑,然后再調用 find_package 命令。
例如可以直接在 CMakeLists.txt 中定義該變量,
# pybmain/CMakeLists.txt
set(OpenCV_DIR "/home/pyb/opencv/build")
find_package(OpenCV REQUIRED)
這種方式對該項目有效,但是不便于開源分發,因其路徑直接寫死在 CMakeLists.txt 中,其他人要用的時候就會找不到該路徑下的 OpenCVConfig.cmake 文件。
也可以設置環境變量
export OpenCV_DIR="/home/pyb/opencv/build"
這種方式全局有效,但是不便于多版本共存,因為環境變量是全局的,如果要切換到其他版本的 OpenCV,就需要重新設置環境變量。
最好的方式是在調用 cmake 命令時定義該變量,例如
cmake -B build -DOpenCV_DIR="/home/pyb/opencv/build"
雖然每次需要在命令行中輸入,但是這種方式既不會污染全局環境,也不會污染項目的 CMakeLists.txt,而且可以方便的切換版本。并且 CMake 本身有緩存功能,只要沒有刪除 build 目錄下的 CMakeCache.txt 文件,下次再運行 cmake -B build 時不輸入該變量 CMake 也會自動讀取緩存中的值。
未提供 Config 文件的第三方庫
有一些庫非常熱門,但是并未提供 Config 文件,例如 Python,CUDA,Jemalloc 等,這時候就需要我們自己寫 FindXXX.cmake 文件來查找該庫,幸運的是 CMake 已經為我們提供了這些庫的 FindXXX.cmake 文件,可以在 CMake 安裝目錄下的 share/cmake/Modules/ 目錄下找到。
另外有一些庫沒有那么熱門,CMake 也沒有為我們提供 FindXXX.cmake 文件,這時候需要我們自己編寫相應的 Find 文件,但是往往網上已經有人寫過了,只需要搜索一下就可以找到,下面的鏈接是 Jemalloc 的 Find 文件
https://github.com/AcademySoftwareFoundation/openvdb/blob/master/cmake/FindJemalloc.cmake
這些文件有些使用的是古代 CMake 風格,有些是現代 CMake 風格,命名也不盡統一,但是一般都會有相應的說明文檔,可以參考著使用。
指定 find_package 模式
find_package 命令有兩種模式,一種是 MODULE 模式,一種是 CONFIG 模式。
-
MODULE模式,只會尋找FindTBB.cmake文件,而不會尋找TBBConfig.cmake文件,這種模式下,find_package命令會在CMAKE_MODULE_PATH(默認為/usr/share/cmake/Modules)中搜索FindTBB.cmake文件find_package(TBB MODULE REQUIRED) -
CONFIG模式,只會尋找TBBConfig.cmake文件,而不會尋找FindTBB.cmake文件,這種模式下,find_package命令會在${CMAKE_PREFIX_PATH}/lib/cmake/TBB(默認為/usr/lib/cmake/TBB)、${TBB_DIR}和$ENV{TBB_DIR}中搜索TBBConfig.cmake文件find_package(TBB CONFIG REQUIRED)
不指定則兩者都會嘗試,默認先查找 Find 文件,如果找不到再查找 Config 文件。
直接作為子模塊引用
有些庫并不是通過 find_package 命令來引用的,而是直接將其作為子模塊引入項目中,例如
add_subdirectory(spdlog)
target_link_libraries(myapp PUBLIC spdlog)
總結
以上是生活随笔為你收集整理的现代 CMake 模块化项目管理指南的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 原创:被社会边缘化的9个特征,你占了几个
- 下一篇: Python笔记三之闭包与装饰器