CMake入門和大型工程管理

最近在負(fù)責(zé)一個(gè)大型工程的CMake編譯系統(tǒng)管理,整理一些工作過程中積累下來的知識片段和技巧。CMake是一個(gè)跨平臺的編譯工具。

基本操作

通過編寫CMakeLists.txt指揮cmake進(jìn)行構(gòu)建和編譯。
通常我們會(huì)在根目錄新建一個(gè)build文件夾,然后依次執(zhí)行:

cmake ..
make
make install

其中cmake命令主要任務(wù)是按照CMakeLists.txt編寫的規(guī)則生成MakeFile,而make會(huì)按照MakeFile進(jìn)行編譯、匯編和鏈接,從而生成可執(zhí)行文件或者庫文件。make install則是將編譯好的文件安裝到指定的目錄。
CMake常用的命令或函數(shù)包括:

  • 定義項(xiàng)目:
    project(myProject C CXX):該命令會(huì)影響PROJECT_SOURCE_DIRPROJECT_BINARY_DIR、PROJECT_NAME等變量。另外要注意的是,對于多個(gè)project嵌套的情況,CMAKE_PROJECT_NAME是當(dāng)前CMakeLists.txt文件回溯至最頂層CMakeLists.txt文件中所在位置之前所定義的最后一個(gè)project的名字。
    cmake_minimum_required(VERSION 3.0):指出進(jìn)行編譯所需要的CMake最低版本,如果不指定的話系統(tǒng)會(huì)自己指定一個(gè),但是也會(huì)扔出一個(gè)warning。

  • 搜索源文件:
    file(<GLOB|GLOB_RECURSE> <variable> <pattern>):按照正則表達(dá)式搜索路徑下的文件,比如file(GLOB SRC_LIST "./src/*.cpp")。
    aux_source_directory(<dir> <variable>):搜索文件內(nèi)所有的源文件。

  • 添加編譯目標(biāo):
    add_library(mylib [STATIC|SHARED] ${SRC_LIST})
    add_executable(myexe ${SRC_LIST})

  • 添加頭文件目錄:
    include_directories(<items>):為該位置之后的target鏈接頭文件目錄(不推薦)。
    target_include_directories(<target> <PUBLIC|INTERFACE|PRIVATE]> <items>):為特定的目標(biāo)鏈接頭文件目錄。

  • 添加依賴庫:
    link_libraries(<items>):為該位置之后的target鏈接依賴庫。
    target_link_libraries(<target> <items>):為特定的目標(biāo)鏈接依賴庫。
    這里,常見的依賴庫可能是以下幾種情況:

    1. 在此次編譯的工程里添加的目標(biāo),給出目標(biāo)名;
    2. 外部庫,給出路徑和庫文件全名;
    3. 外部庫,通過find_package()等命令搜索到的。

    對于find_package(XXX),該命令本身并不直接去進(jìn)行搜索,而是通過特定路徑下的FindXXX.cmake或XXXConfig.cmake文件來定位頭文件和庫文件的位置,分別被稱為Module模式和Config模式。該命令會(huì)定義一個(gè)XXX_FOUND變量,如果成功找到,該變量為真,同時(shí)會(huì)定義XXX_INCLUDE_DIRXXX_LIBRARIES兩個(gè)變量,用于link和include。

  • 添加子目錄:
    add_subdirectories(<dir>):子目錄中要有CMakeLists.txt文件,否則會(huì)報(bào)錯(cuò)。

  • 包含其他cmake文件:
    include(./path/to/tool.cmake)
    set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ./path/to),隨后include(tool)
    該命令相當(dāng)于將tool.cmake的內(nèi)容直接包含進(jìn)來。

  • 定義變量:
    set(<variable> <value>... [PARENT_SCOPE])
    set(<variable> <value>... CACHE <type> <docstring> [FORCE])
    其中CACHE會(huì)將變量定義在緩存文件CMakeCache.txt里,可以在下次編譯的時(shí)候讀取。

  • 作用域:
    add_subdirectories(<dir>)會(huì)創(chuàng)建一個(gè)子作用域,里面可以使用父作用域里定義的變量,但里面定義的變量在父作用域不可見,同樣,在子作用域修改父作用域里的變量不會(huì)影響父作用域。function()同樣會(huì)產(chǎn)生一個(gè)子作用域。若想讓子作用域里的定義或者修改在父作用域可見,需要使用PARENT_SCOPE標(biāo)記。
    相對地,macro()include()不會(huì)產(chǎn)生子作用域。

  • 選項(xiàng):
    add_option(MY_OPTION <ON|OFF>):會(huì)定義一個(gè)選項(xiàng)。在使用cmake命令時(shí),可以通過-D改變選項(xiàng)的值。比如cmake .. -DMY_OPTION=ON。

  • 編譯選項(xiàng):
    add_compile_options(-std=c++11)
    如果想要指定具體的編譯器的選項(xiàng),可以使用make_cxx_flags()cmake_c_flags()。

  • 與源文件的交互:
    configure_file(XXX.in XXX.XX)會(huì)讀入一個(gè)文件,處理后輸入到新的位置。一方面,會(huì)替換掉#XXX或者@XXX@定義的內(nèi)容。另一方面,會(huì)將文件里的#cmakedefine VAR …替換為#define VAR …或者/* #undef VAR */

  • 字符串操作、循環(huán)、判斷、文件/變量存在判斷等
    這些命令同樣有用,請參考網(wǎng)絡(luò)資料。

當(dāng)代CMake理念

參考1: https://kubasejdak.com/modern-cmake-is-like-inheritance
翻譯自: https://pabloariasal.github.io/2018/02/19/its-time-to-do-cmake-right/

一些人士指出,CMake應(yīng)該是基于Targets目標(biāo)和Properties屬性的,應(yīng)有面向?qū)ο?/strong>的思想。
目標(biāo)指的當(dāng)然就是library和executable。目標(biāo)的屬性則具有兩種不同的作用域:INTERFACE(接口)和PRIVATE(私有)。私有屬性適用于構(gòu)建目標(biāo)本身時(shí)內(nèi)部使用,而接口屬性則是由目標(biāo)的使用者在外部使用的。也就是說,接口屬性定義了使用要求,而私有屬性則定義了目標(biāo)本身的構(gòu)建要求。
此外,屬性也可以被定義為PUBLIC(公有),當(dāng)且僅當(dāng)其既是私有又是接口。
比如,假如一個(gè)工程里有如下文件:

libjsonutils
├── CMakeLists.txt
├── include
│   └── jsonutils
│       └── json_utils.h
├── src
│   ├── file_utils.h
│   └── json_utils.cpp
└── test
    ├── CMakeLists.txt
    └── src
        └── test_main.cpp

我們注意到,include/中有json_utils.h頭文件,這是我們想對外暴露的公共文件;而src/中有額外的頭文件file_utils.h,這個(gè)文件僅在構(gòu)建中使用,不想對外暴露。這兩個(gè)頭文件都應(yīng)該在構(gòu)建的時(shí)候被包含(include) ;另一方面,jsontuils的使用者又僅僅需要知道公開的頭文件,因此INTERFACE_INCLUDE_DIRS只需要包含include/,而沒有src/。
為此,可以在CMakeLists.txt使用如下代碼(這里使用了CMake的generator expression特性):

add_library(JSONUtils src/json_utils.cpp)
target_include_directories(JSONUtils
    PUBLIC 
        $<INSTALL_INTERFACE:include>    
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/src
)

對于目標(biāo)的依賴項(xiàng),同樣有INTERFACEPRIVATE的區(qū)分。
比如:

find_package(Boost 1.55 REQUIRED COMPONENTS regex)
find_package(RapidJSON 1.0 REQUIRED MODULE)

target_link_libraries(JSONUtils
    PUBLIC
        Boost::boost RapidJSON::RapidJSON
    PRIVATE
        Boost::regex
)

這種情況,rapidjson和Boost::boost都應(yīng)當(dāng)被定義成接口類型的依賴,并被傳遞到目標(biāo)的使用者那邊,因?yàn)橛脩羲鶎?dǎo)入的頭文件中調(diào)用了這兩個(gè)庫的工具。這意味著JSONUtils的用戶不僅需要JSONUtils的接口屬性,同時(shí)也需要其接口類型的依賴的接口屬性(在我們的情況下,定義了boost和rapidjson的公共頭文件),甚至接口類型的依賴的接口類型的依賴的接口屬性,等等。
對于CMake而言,它會(huì)將Boost::boostRapidJSON::RapidJson的所有接口屬性添加到JSONUtils的接口屬性中。這意味著JSONUtils的用戶會(huì)傳遞獲取依賴鏈條上所有的接口屬性。
另一方面Boost::regex則僅在我們目標(biāo)的內(nèi)部使用,并且可以作為私有依賴。這種情況下,Boost::regex的接口屬性會(huì)被添加到JSONUtils的私有屬性中,而不會(huì)傳遞到用戶那里。

導(dǎo)入目標(biāo)

當(dāng)我們執(zhí)行find_package(Boost 1.55 REQUIRED COMPONENTS regex)的時(shí)候,CMake實(shí)際執(zhí)行了FindBoost.cmake腳本,并由此導(dǎo)入了目標(biāo)Boost::boostBoost::regex,這是為什么我們能通過target_link_libraries()來依賴這些目標(biāo)。
然而部分第三方庫并不那么守規(guī)矩,比如RapidJSON的RapidJSONConfig.cmake:

get_filename_component(RAPIDJSON_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
set(RAPIDJSON_INCLUDE_DIRS "/usr/include")
message(STATUS "RapidJSON found. Headers: ${RAPIDJSON_INCLUDE_DIRS}")

它實(shí)際上并沒有定義目標(biāo),只是定義了RAPIDJSON_INCLUDE_DIRS一個(gè)變量。
這種情況,我們可以自己編寫FindRapidJSON.cmake文件:

# FindRapidJSON.cmake
#
# Finds the rapidjson library
#
# This will define the following variables
#
#    RapidJSON_FOUND
#    RapidJSON_INCLUDE_DIRS
#
# and the following imported targets
#
#     RapidJSON::RapidJSON
#
# Author: Pablo Arias - pabloariasal@gmail.com

find_package(PkgConfig)
pkg_check_modules(PC_RapidJSON QUIET RapidJSON)

find_path(RapidJSON_INCLUDE_DIR
    NAMES rapidjson.h
    PATHS ${PC_RapidJSON_INCLUDE_DIRS}
    PATH_SUFFIXES rapidjson
)

set(RapidJSON_VERSION ${PC_RapidJSON_VERSION})

mark_as_advanced(RapidJSON_FOUND RapidJSON_INCLUDE_DIR RapidJSON_VERSION)

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(RapidJSON
    REQUIRED_VARS RapidJSON_INCLUDE_DIR
    VERSION_VAR RapidJSON_VERSION
)

if(RapidJSON_FOUND)
    set(RapidJSON_INCLUDE_DIRS ${RapidJSON_INCLUDE_DIR})
endif()

if(RapidJSON_FOUND AND NOT TARGET RapidJSON::RapidJSON)
    add_library(RapidJSON::RapidJSON INTERFACE IMPORTED)
    set_target_properties(RapidJSON::RapidJSON PROPERTIES
        INTERFACE_INCLUDE_DIRECTORIES "${RapidJSON_INCLUDE_DIR}"
    )
endif()

導(dǎo)出自己的庫

如果想讓自己的工程能夠被別人通過簡單的命令使用:

find_package(JSONUtils 1.0 REQUIRED)
target_link_libraries(example JSONUtils::JSONUtils)

我們需要做兩件事:首先,需要導(dǎo)出目標(biāo)JSONUtils::JSONUtils;隨后,需要允許下游應(yīng)用find_package(JSONUtils)的時(shí)候能夠?qū)脒@個(gè)目標(biāo)。
首先我們要將目標(biāo)導(dǎo)出到一個(gè)能夠?qū)肽繕?biāo)的JSONUtilsTargets.cmake

include(GNUInstallDirs)
install(TARGETS JSONUtils
    EXPORT jsonutils-targets
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

install(EXPORT jsonutils-targets
  FILE
    JSONUtilsTargets.cmake
  NAMESPACE
    JSONUtils::
  DESTINATION
    ${CMAKE_INSTALL_LIBDIR}/cmake/JSONUtils
)

這樣,我們安裝了一個(gè)JSONUtilsTargets.cmake文件,這里面包含了導(dǎo)入JSONUtils的命令,只需要在別的文件中使用這個(gè)文件就可以導(dǎo)入。
下一步,我們制作一個(gè)JSONUtilsConfig.cmake

get_filename_component(JSONUtils_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
include(CMakeFindDependencyMacro)

find_dependency(Boost 1.55 REQUIRED COMPONENTS regex)
find_dependency(RapidJSON 1.0 REQUIRED MODULE)

if(NOT TARGET JSONUtils::JSONUtils)
    include("${JSONUtils_CMAKE_DIR}/JSONUtilsTargets.cmake")
endif()

大型工程

在第一部分介紹的都是基本命令,對于大型工程來說,會(huì)用到一些不太常用的概念或者功能。

什么是Project?

對于大型工程來說,project的概念變得更為重要。通常來說,簡單的工程只需要有一個(gè)project,而對于復(fù)雜的工程,有可能會(huì)出現(xiàn)project的嵌套。
Project通常指的是一個(gè)邏輯上相對獨(dú)立、完整,能夠獨(dú)立編譯的集合。通常來說,如果某一個(gè)CMakeLists.txt文件中出現(xiàn)了project()命令,那你應(yīng)該能以該文件所在的目錄為根目錄進(jìn)行一次完整的編譯。
https://stackoverflow.com/questions/26878379/in-cmake-what-is-a-project)該命令也會(huì)如上文所說的,影響CMAKE_PROJECT_NAME等變量的值。

文件組織

文件組織方式就見仁見智了。不過通常來說,為了方便cmake的管理,建議以modules的形式扁平地組織,并且在每個(gè)module中設(shè)置有限的文件層次。比如說我們有一個(gè)moduleA,其下面有src、include和test三個(gè)目錄,而在include目錄下面,再根據(jù)具體的功能分為不同的目錄,再下一級就只有頭文件。
這樣在添加頭文件目錄的時(shí)候,統(tǒng)一添加為*/moduleA/include,而在源文件或者其他頭文件包含的時(shí)候,可以從include下一級目錄開始:#include "abc/a.hpp"。

模塊下的CMakeLists.txt

在一個(gè)模塊下,可以遵循以下規(guī)律編寫CMakeLists.txt:

  1. 設(shè)置內(nèi)部模塊依賴
  2. 搜索內(nèi)部依賴模塊的頭文件和庫文件
  3. 設(shè)置項(xiàng)目內(nèi)第三方模塊依賴
  4. 搜索項(xiàng)目內(nèi)第三方模塊依賴庫的頭文件和庫文件
  5. 設(shè)置和搜索本地的外部依賴庫
  6. 添加編譯目標(biāo)
  7. 包含頭文件目錄、鏈接庫文件
  8. 設(shè)置安裝規(guī)則(比如一些配置文件)
  9. 設(shè)置單元測試

頭文件暴露

有的時(shí)候,有些頭文件只供內(nèi)部使用,不想暴露在install后的頭文件目錄里。那就將其放在src路徑下。

依賴順序管理

CMake中鏈接庫的順序是a依賴b,那么b放在a的后面。
例如目標(biāo)test依賴a庫、b庫, a庫又依賴b庫,那么順序如下:
target_link_libraries(test a b)
另外,假如目標(biāo)test依賴a庫, a庫又依賴b庫,但test不直接依賴b庫,那么test不用鏈接b庫。
如果在一個(gè)工程中有多個(gè)target,那么可以用add_dependencies(<target> [<target-dependency>]...)命令,來定義依賴關(guān)系。這樣CMake會(huì)首先編譯被依賴的目標(biāo),隨后再編譯依賴的目標(biāo)。

INTERFACE|PUBLIC|PRIVATE

INTERFACE|PUBLIC|PRIVATE

如何調(diào)試

nm -a <target>命令查看符號表。
如果出現(xiàn)

Undefined symbols for architecture x86_64:
  "_main"

可能是在沒有main的cpp文件定義add_executable。
構(gòu)造函數(shù)和析構(gòu)函數(shù)聲明了就要定義,要么用default。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容