CMake入門和大型工程管理

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

基本操作

通過編寫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í)行文件或者庫(kù)文件。make install則是將編譯好的文件安裝到指定的目錄车柠。
CMake常用的命令或函數(shù)包括:

  • 定義項(xiàng)目:
    project(myProject C CXX):該命令會(huì)影響PROJECT_SOURCE_DIR剔氏、PROJECT_BINARY_DIRPROJECT_NAME等變量竹祷。另外要注意的是谈跛,對(duì)于多個(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)鏈接頭文件目錄。

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

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

    對(duì)于find_package(XXX),該命令本身并不直接去進(jìn)行搜索嗜愈,而是通過特定路徑下的FindXXX.cmake或XXXConfig.cmake文件來(lái)定位頭文件和庫(kù)文件的位置旧蛾,分別被稱為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)來(lái)基公。

  • 定義變量:
    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)記渗饮。
    相對(duì)地,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)茂缚、判斷、文件/變量存在判斷等
    這些命令同樣有用屋谭,請(qǐng)參考網(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頭文件,這是我們想對(duì)外暴露的公共文件尺锚;而src/中有額外的頭文件file_utils.h,這個(gè)文件僅在構(gòu)建中使用惜浅,不想對(duì)外暴露瘫辩。這兩個(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
)

對(duì)于目標(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è)庫(kù)的工具晤锹。這意味著JSONUtils的用戶不僅需要JSONUtils的接口屬性摩幔,同時(shí)也需要其接口類型的依賴的接口屬性(在我們的情況下,定義了boost和rapidjson的公共頭文件)鞭铆,甚至接口類型的依賴的接口類型的依賴的接口屬性或衡,等等。
對(duì)于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()來(lái)依賴這些目標(biāo)。
然而部分第三方庫(kù)并不那么守規(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)出自己的庫(kù)

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

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()

大型工程

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

什么是Project举庶?

對(duì)于大型工程來(lái)說,project的概念變得更為重要揩抡。通常來(lái)說户侥,簡(jiǎn)單的工程只需要有一個(gè)project镀琉,而對(duì)于復(fù)雜的工程,有可能會(huì)出現(xiàn)project的嵌套添祸。
Project通常指的是一個(gè)邏輯上相對(duì)獨(dú)立滚粟、完整,能夠獨(dú)立編譯的集合刃泌。通常來(lái)說凡壤,如果某一個(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等變量的值。

文件組織

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

模塊下的CMakeLists.txt

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

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

頭文件暴露

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

依賴順序管理

CMake中鏈接庫(kù)的順序是a依賴b绽族,那么b放在a的后面。
例如目標(biāo)test依賴a庫(kù)衩藤、b庫(kù)吧慢, a庫(kù)又依賴b庫(kù),那么順序如下:
target_link_libraries(test a b)
另外赏表,假如目標(biāo)test依賴a庫(kù)检诗, a庫(kù)又依賴b庫(kù)匈仗,但test不直接依賴b庫(kù),那么test不用鏈接b庫(kù)逢慌。
如果在一個(gè)工程中有多個(gè)target悠轩,那么可以用add_dependencies(<target> [<target-dependency>]...)命令,來(lái)定義依賴關(guān)系攻泼。這樣CMake會(huì)首先編譯被依賴的目標(biāo)火架,隨后再編譯依賴的目標(biāo)。

INTERFACE|PUBLIC|PRIVATE

INTERFACE|PUBLIC|PRIVATE

如何調(diào)試

nm -a <target>命令查看符號(hào)表忙菠。
如果出現(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)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末骡男,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子傍睹,更是在濱河造成了極大的恐慌隔盛,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拾稳,死亡現(xiàn)場(chǎng)離奇詭異吮炕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)熊赖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門来屠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人震鹉,你說我怎么就攤上這事俱笛。” “怎么了传趾?”我有些...
    開封第一講書人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵迎膜,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我浆兰,道長(zhǎng)磕仅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任簸呈,我火速辦了婚禮榕订,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蜕便。我一直安慰自己劫恒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著两嘴,像睡著了一般丛楚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上憔辫,一...
    開封第一講書人閱讀 49,950評(píng)論 1 291
  • 那天趣些,我揣著相機(jī)與錄音,去河邊找鬼贰您。 笑死坏平,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的枉圃。 我是一名探鬼主播功茴,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼孽亲!你這毒婦竟也來(lái)了坎穿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤返劲,失蹤者是張志新(化名)和其女友劉穎玲昧,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體篮绿,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡孵延,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了亲配。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片尘应。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖吼虎,靈堂內(nèi)的尸體忽然破棺而出犬钢,到底是詐尸還是另有隱情,我是刑警寧澤思灰,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布玷犹,位于F島的核電站,受9級(jí)特大地震影響洒疚,放射性物質(zhì)發(fā)生泄漏歹颓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一油湖、第九天 我趴在偏房一處隱蔽的房頂上張望巍扛。 院中可真熱鬧,春花似錦乏德、人聲如沸撤奸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)寂呛。三九已至,卻和暖如春瘾晃,著一層夾襖步出監(jiān)牢的瞬間贷痪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工蹦误, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留劫拢,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓强胰,卻偏偏與公主長(zhǎng)得像舱沧,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子偶洋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容