最近在負(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_DIR
、PROJECT_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ù)可能是以下幾種情況:- 在此次編譯的工程里添加的目標(biāo),給出目標(biāo)名蚓土;
- 外部庫(kù)宏侍,給出路徑和庫(kù)文件全名;
- 外部庫(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_DIR
和XXX_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),同樣有INTERFACE和PRIVATE的區(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::boost
和RapidJSON::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::boost
和Boost::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:
- 設(shè)置內(nèi)部模塊依賴
- 搜索內(nèi)部依賴模塊的頭文件和庫(kù)文件
- 設(shè)置項(xiàng)目?jī)?nèi)第三方模塊依賴
- 搜索項(xiàng)目?jī)?nèi)第三方模塊依賴庫(kù)的頭文件和庫(kù)文件
- 設(shè)置和搜索本地的外部依賴庫(kù)
- 添加編譯目標(biāo)
- 包含頭文件目錄持灰、鏈接庫(kù)文件
- 設(shè)置安裝規(guī)則(比如一些配置文件)
- 設(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
如何調(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牛欢。