CMake技術總結

zq發表於2022-05-15

在做演算法部署的過程中,我們一般都是用C++開發,主要原因是C++的高效性,而構建維護一個大型C++工程的過程中,如何管理不同子模組之間的依賴、外部依賴庫、標頭檔案和原始檔如何隔離、編譯的時候又該如何相互依賴這些問題,直接用Makefile實現是比較麻煩的。這個時候,CMake的優勢就顯現出來了,簡潔的命令大大簡化了專案構建過程,而且其跨平臺特性也方便了不同部署平臺間的遷移。這裡我想把工作這一年來,在實踐過程中學到的CMake用法做個總結。這裡會參考一篇在知乎寫的非常不錯的文章,但這裡我只記錄我認為比較重要的部分,從來不會用到的功能不去深究,畢竟只是個工具,夠用就行。

一、CMake構建編譯原理概述

  • 單個cpp檔案可以通過gcc直接編譯生成可執行檔案,但當專案很大時,這種方式便不再適用,我們需要寫Makefile或者CMake。
  • CMake構建C++工程其實是充當一個生成Makefile的媒介,以往直接寫Makefile也是可以的,但是當工程越來越複雜的時候,Makefile就不那麼好寫了,目前也不要求自己學會寫Makefile了;
  • cpp工程一般由標頭檔案目錄、原始檔目錄和第三方庫目錄三大塊程式碼內容組成,CMake一般會在每個模組資料夾下都建立一個CMakelists.txt檔案,而在最頂層的原始檔目錄下,會建立一個總的CMakelists.txt用於控制整個cmake流程,然後通過add_subdirectory()命令遞迴的訪問每個模組目錄執行cmake,最後在build目錄下生成一個總的makefile用於編譯原始碼。標頭檔案目錄存放最終SDK提供出去需要的標頭檔案、以及一些需要原始檔目錄訪問的介面類定義標頭檔案,原始檔下的程式碼存放實現類,大致如此。CMake中需要配置每個模組編譯時標頭檔案需要從哪裡找、還有連結的時候庫檔案需要從哪裡找。
  • gcc編譯生成的目標檔案分為三類,可執行檔案、動態庫和靜態庫。其中可執行檔案在連結過程中會連結一些系統c執行時庫等,需保證可執行檔案對應的原始碼中main函式是存在的,不然會連結失敗。動態庫和靜態庫可以樸素的理解為就是一系列的cpp檔案打包而成的,cpp檔案中會定義一些類和函式可供呼叫,此外還有一些全域性變數。

二、CMake用法總結

2.1 使用與設定系統環境變數與系統資訊

$ENV{Name}      # 使用環境變數
set(ENV{Name} value)    # 寫入環境變數, 這裡沒有`$`符號
­UNIX                   # Linux平臺下該值為 TRUE
­WIN32                  # Windows平臺下該值為 TRUE

2.2 CMake預定義變數

PROJECT_SOURCE_DIR       # 工程的根目錄,即根CMakefiles.txt檔案所在目錄
PROJECT_BINARY_DIR       # 執行 cmake 命令的目錄,通常是 ${PROJECT_SOURCE_DIR}/build 
CMAKE_CURRENT_SOURCE_DIR # 當前處理的 CMakeLists.txt 所在的路徑
CMAKE_CURRENT_BINARY_DIR # target(包括可執行檔案與庫檔案) 編譯目錄
CMAKE_CURRENT_LIST_DIR   # CMakeLists.txt 的完整路徑
CMAKE_MODULE_PATH        # 自己的 cmake 模組所在的路徑,SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
EXECUTABLE_OUTPUT_PATH   # 目標二進位制可執行檔案的存放位置
LIBRARY_OUTPUT_PATH      # 目標連結庫檔案的存放位置

CMAKE_CXX_FLAGS          # 設定 C++ 編譯選項,如優化等級、c++版本等

2.3 常用命令

  • 基本命令
cmake_minimum_required(VERSION3.20.1)   #宣告最低cmake版本要求,當執行cmake命令啟動時檢測到版本不符合要求時會提醒
project(demo)     #設定專案名稱,在windows下camke會生成對應的VS sln檔案 
option(<variable> "<help_text>" [value])  #設定編譯選項,用於控制選擇編譯方案

add_executable(demo demo.cpp)       # 生成可執行檔案
add_library(common SHARED util.cpp) # 生成動態庫或共享庫
set_property(TARGET common PROPERTY POSITION_INDEPENDENT_CODE ON)  # 代表-fPIC,生成位置無關的動態庫檔案
  • set——設定變數
set(SRC_LIST main.cpp)    # 設定變數
list(APPEND SRC_LIST test.cpp)   #追加檔案到變數list
list(REMOVE_ITEM SRC_LIST main.cpp)   #從變數列表中移除檔案
  • if——條件判斷
if (expression)                  # expression 不為空(0,N,NO,OFF,FALSE,NOTFOUND)時為真

#數字比較:
if (variable LESS number)        # LESS 小於
if (string LESS number)
if (variable GREATER number)     # GREATER 大於
if (string GREATER number)
if (variable EQUAL number)       # EQUAL 等於
if (string EQUAL number)

#字母表順序比較:
if (variable STRLESS string)
if (string STRLESS string)
if (variable STRGREATER string)
if (string STRGREATER string)
if (variable STREQUAL string)
if (string STREQUAL string)
  • 迴圈
#while 迴圈
while(condition)
    ...
endwhile()

# for迴圈
# start 表示起始數,stop 表示終止數,step 表示步長
foreach(loop_var RANGE start stop [step])
    ...
endforeach(loop_var)
  • function——函式
# 定義一個簡單的列印函式
function(_foo)
    foreach(arg IN LISTS ARGN)
        message(STATUS "this in function is ${arg}")
    endforeach()
endfunction()

_foo(a b c)
# this in function is a
# this in function is b
# this in function is c
  • 指定當前編譯需要包含的原始檔
aux_source_directory(dir VAR)        將目錄下所有的原始碼檔案列表儲存在一個變數中
file(GLOB SRC_LIST "*.cpp" "protocol/*.cpp")   #按字串匹配的檔案設定變數
file(GLOB_RECURSE SRC_LIST "*.cpp")             # 遞迴搜尋匹配
add_library(demo SHARED ${SRC_LIST} ${SRC_PROTOCOL_LIST}) # 最後將所有原始檔編譯到一個動態庫檔案中,其中連結過程會對不同原始檔中的定義式進行整合。
  • 查詢指定的庫檔案或package
find_library(VAR name path)   #查詢指定名稱的庫檔案,並將路徑儲存到VAR中,其中path是庫檔案所在目錄。
find_package(<Name>)          # 通過尋找 Find<name>.cmake檔案引入其他包,具體搜尋路徑依次為:1. ${CMAKE_MODULE_PATH}中的所有目錄;2. 再檢視CMake自己的模組目錄 /share/cmake-x.y/Modules/,通過$CMAKE_ROOT可檢視;3. 在~/.cmake/packages/或/usr/local/share/中的各個包目錄中查詢<庫名字的大寫>Config.cmake 或者 <庫名字的小寫>-config.cmake。
找到上述.cmake檔案後,就會定義下述幾個變數:
<NAME>_FOUND                           #判斷查詢是否成功
<NAME>_INCLUDE_DIRS or <NAME>_INCLUDES #package的標頭檔案包含目錄
<NAME>_LIBRARIES or <NAME>_LIBRARIES or <NAME>_LIBS     # package的庫目錄
然後就可以使用該庫了。
  • 設定包含目錄
    只有將包含目錄設定了,在原始檔中的include才能正確索引到標頭檔案
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
  • 新增子目錄並用CMake構建子目錄
    CMake一個很好的功能就是,可以在個子目錄設定單獨的CMakelists.txt,然後再上一層的Cmakelists.txt中新增該子目錄即可,例如:
# 其中若設定EXCLUDE_FROM_ALL引數,則預設不編譯該目錄;binary_dir指定編譯target輸出目錄
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])  
  • 設定連結庫搜尋目錄
link_directories(${CMAKE_CURRENT_SOURCE_DIR}/libs)
  • 設定target需要連結的庫檔案
target_link_libraries(demo libface.a)
  • 列印log資訊
    有些時候我們需要在終端列印某個變數以確定是否符合預期
message(STATUS ${PROJECT_SOURCE_DIR}"this is warnning message")  # 狀態資訊,顯示變數
message(WARNING "this is warnning message") # 警告資訊
message(FATAL_ERROR"this is error message") # 錯誤資訊,終止生成
  • 檔案操作
#檔案拷貝
file({COPY | INSTALL} <file>... DESTINATION <dir> [...])
#資料夾建立
file(MAKE_DIRECTORY [<dir>...])

三、編譯示例

假如有如下結構的示例工程:

|-- build  # 編譯輸出目錄
|-- cmake  # 自定義命令目錄
|   |-- utils_function.cmake
|   `-- utils_macro.cmake
|-- CMakeLists.txt   # root cmake指令碼(1)
|-- include          # 公共標頭檔案目錄
|   |-- config.h
|   `-- public.h
|-- source          # 原始碼目錄
|   |-- CMakeLists.txt   # cmake指令碼(2)
|   |-- mod_1
|   |   |-- CMakeLists.txt   # cmake指令碼
|   |   |-- include
|   |   |   `-- mod_1.h
|   |   |-- src
|   |   |   `-- mod_1.cpp
|   `-- mod_2
|   |   |-- CMakeLists.txt   # cmake指令碼(3)
|   |   |-- include
|   |   |   `-- mod_3.h
|   |   `-- src
|   |       `-- mod_3.cpp
|   |- test               # 一般為單元測試程式碼
|       |-- CMakeLists.txt   # 可執行檔案cmake指令碼 (4)
|       `-- main_total.cpp
|       `-- test_module1.cpp
|-- build.sh          # 編譯指令碼
|-- libs              # 第三方依賴庫
  • 一般的CPP工程都按照上述結構組織,原始碼只存放在source和include資料夾中,其中include存放公共標頭檔案,一般是一些需要提供出去的虛介面類;source下也會按照模組分別有每個模組的.h標頭檔案和.cpp原始檔,分別存放class宣告和成員函式實現。此外還有單元測試程式碼,長期看單元測試是十分必要的。C++的STL我們可以直接使用,但第三方庫需要引入才能使用,較小的庫可以隨工程放入單獨的資料夾內,例如libs或者3rdparty資料夾下可以將opencv放進去,但像cuda這種很大的庫,一般還是會從系統安裝目錄動態連結過來。

  • 上述工程目錄中的CMake指令碼的工作邏輯是:先shell命令建立build目錄,然後在cd到build目錄後執行cmake ..,這樣就搜尋到了根目錄下的Cmakelists.txt,然後按順序執行其中的命令,這個cmake指令碼中需要做的工作包括:①專案名稱設定、option開關設定、CMAKE_CXX_FLAGS設定;②外部標頭檔案包含目錄設定;③第三方庫檔案引入(opencv和cuda);④新增需要編譯的子目錄。然後遞迴執行子目錄中的cmake流程。

  • cmake過程中一般比較容易出現的問題是:庫找不到。一般都是路徑不對或者相關庫未安裝。
    make過程中一般出現的問題是:標頭檔案找不到、重定義、連結失敗等。這些問題也需要返回到cmake指令碼中修復。

CMakelists.txt示例:
本來是要在windows的SWL上寫一個demo的,手欠先升級到了SWL2,結果之前的子系統登不進去了,又得重新配置ubuntu編譯環境。樣例這塊就先不實踐了。後續寫c++專案的時候,再對其中的CMake做一次解析。

四、小結

這次花了一天時間對cmake的相關內容回顧和總結了一下,但工具不用很快就忘記了,這類東西最好還是在工作實踐過程主動去嘗試去思考,平時一個工程如果架構構建完成後,cmakelist是很少去動的,所以從頭寫的機會比較少。那麼就要在看到別人的cmakelists.txt的時候,多想想為什麼他這樣寫,學習別人是如何寫出簡潔的cmakelists.txt檔案的。cmake的思路其實和編譯原理是相輔相成的,學會cmake對我們理解專案架構、解決庫依賴問題很有幫助。

參考:

  1. https://zhuanlan.zhihu.com/p/470681241

相關文章