最近在負責一個大型工程的CMake編譯系統(tǒng)管理,整理一些工作過程中積累下來的知識片段和技巧。CMake是一個跨平臺的編譯工具。
基本操作
通過編寫CMakeLists.txt指揮cmake進行構建和編譯。
通常我們會在根目錄新建一個build文件夾,然后依次執(zhí)行:
cmake ..
make
make install
其中cmake命令主要任務是按照CMakeLists.txt編寫的規(guī)則生成MakeFile,而make會按照MakeFile進行編譯、匯編和鏈接,從而生成可執(zhí)行文件或者庫文件。make install則是將編譯好的文件安裝到指定的目錄。
CMake常用的命令或函數(shù)包括:
定義項目:
project(myProject C CXX):該命令會影響PROJECT_SOURCE_DIR、PROJECT_BINARY_DIR、PROJECT_NAME等變量。另外要注意的是,對于多個project嵌套的情況,CMAKE_PROJECT_NAME是當前CMakeLists.txt文件回溯至最頂層CMakeLists.txt文件中所在位置之前所定義的最后一個project的名字。
cmake_minimum_required(VERSION 3.0):指出進行編譯所需要的CMake最低版本,如果不指定的話系統(tǒng)會自己指定一個,但是也會扔出一個warning。搜索源文件:
file(<GLOB|GLOB_RECURSE> <variable> <pattern>):按照正則表達式搜索路徑下的文件,比如file(GLOB SRC_LIST "./src/*.cpp")。
aux_source_directory(<dir> <variable>):搜索文件內所有的源文件。添加編譯目標:
add_library(mylib [STATIC|SHARED] ${SRC_LIST})
add_executable(myexe ${SRC_LIST})添加頭文件目錄:
include_directories(<items>):為該位置之后的target鏈接頭文件目錄(不推薦)。
target_include_directories(<target> <PUBLIC|INTERFACE|PRIVATE]> <items>):為特定的目標鏈接頭文件目錄。-
添加依賴庫:
link_libraries(<items>):為該位置之后的target鏈接依賴庫。
target_link_libraries(<target> <items>):為特定的目標鏈接依賴庫。
這里,常見的依賴庫可能是以下幾種情況:- 在此次編譯的工程里添加的目標,給出目標名;
- 外部庫,給出路徑和庫文件全名;
- 外部庫,通過
find_package()等命令搜索到的。
對于
find_package(XXX),該命令本身并不直接去進行搜索,而是通過特定路徑下的FindXXX.cmake或XXXConfig.cmake文件來定位頭文件和庫文件的位置,分別被稱為Module模式和Config模式。該命令會定義一個XXX_FOUND變量,如果成功找到,該變量為真,同時會定義XXX_INCLUDE_DIR和XXX_LIBRARIES兩個變量,用于link和include。 添加子目錄:
add_subdirectories(<dir>):子目錄中要有CMakeLists.txt文件,否則會報錯。包含其他cmake文件:
include(./path/to/tool.cmake)
或set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ./path/to),隨后include(tool)。
該命令相當于將tool.cmake的內容直接包含進來。定義變量:
set(<variable> <value>... [PARENT_SCOPE])
set(<variable> <value>... CACHE <type> <docstring> [FORCE])
其中CACHE會將變量定義在緩存文件CMakeCache.txt里,可以在下次編譯的時候讀取。作用域:
add_subdirectories(<dir>)會創(chuàng)建一個子作用域,里面可以使用父作用域里定義的變量,但里面定義的變量在父作用域不可見,同樣,在子作用域修改父作用域里的變量不會影響父作用域。function()同樣會產生一個子作用域。若想讓子作用域里的定義或者修改在父作用域可見,需要使用PARENT_SCOPE標記。
相對地,macro()和include()不會產生子作用域。選項:
add_option(MY_OPTION <ON|OFF>):會定義一個選項。在使用cmake命令時,可以通過-D改變選項的值。比如cmake .. -DMY_OPTION=ON。編譯選項:
add_compile_options(-std=c++11)
如果想要指定具體的編譯器的選項,可以使用make_cxx_flags()或cmake_c_flags()。與源文件的交互:
configure_file(XXX.in XXX.XX)會讀入一個文件,處理后輸入到新的位置。一方面,會替換掉#XXX或者@XXX@定義的內容。另一方面,會將文件里的#cmakedefine VAR …替換為#define VAR …或者/* #undef VAR */。字符串操作、循環(huán)、判斷、文件/變量存在判斷等
這些命令同樣有用,請參考網(wǎ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應該是基于Targets目標和Properties屬性的,應有面向對象的思想。
目標指的當然就是library和executable。目標的屬性則具有兩種不同的作用域:INTERFACE(接口)和PRIVATE(私有)。私有屬性適用于構建目標本身時內部使用,而接口屬性則是由目標的使用者在外部使用的。也就是說,接口屬性定義了使用要求,而私有屬性則定義了目標本身的構建要求。
此外,屬性也可以被定義為PUBLIC(公有),當且僅當其既是私有又是接口。
比如,假如一個工程里有如下文件:
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,這個文件僅在構建中使用,不想對外暴露。這兩個頭文件都應該在構建的時候被包含(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
)
對于目標的依賴項,同樣有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都應當被定義成接口類型的依賴,并被傳遞到目標的使用者那邊,因為用戶所導入的頭文件中調用了這兩個庫的工具。這意味著JSONUtils的用戶不僅需要JSONUtils的接口屬性,同時也需要其接口類型的依賴的接口屬性(在我們的情況下,定義了boost和rapidjson的公共頭文件),甚至接口類型的依賴的接口類型的依賴的接口屬性,等等。
對于CMake而言,它會將Boost::boost和RapidJSON::RapidJson的所有接口屬性添加到JSONUtils的接口屬性中。這意味著JSONUtils的用戶會傳遞獲取依賴鏈條上所有的接口屬性。
另一方面Boost::regex則僅在我們目標的內部使用,并且可以作為私有依賴。這種情況下,Boost::regex的接口屬性會被添加到JSONUtils的私有屬性中,而不會傳遞到用戶那里。
導入目標
當我們執(zhí)行find_package(Boost 1.55 REQUIRED COMPONENTS regex)的時候,CMake實際執(zhí)行了FindBoost.cmake腳本,并由此導入了目標Boost::boost和Boost::regex,這是為什么我們能通過target_link_libraries()來依賴這些目標。
然而部分第三方庫并不那么守規(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}")
它實際上并沒有定義目標,只是定義了RAPIDJSON_INCLUDE_DIRS一個變量。
這種情況,我們可以自己編寫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()
導出自己的庫
如果想讓自己的工程能夠被別人通過簡單的命令使用:
find_package(JSONUtils 1.0 REQUIRED)
target_link_libraries(example JSONUtils::JSONUtils)
我們需要做兩件事:首先,需要導出目標JSONUtils::JSONUtils;隨后,需要允許下游應用find_package(JSONUtils)的時候能夠導入這個目標。
首先我們要將目標導出到一個能夠導入目標的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
)
這樣,我們安裝了一個JSONUtilsTargets.cmake文件,這里面包含了導入JSONUtils的命令,只需要在別的文件中使用這個文件就可以導入。
下一步,我們制作一個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()
大型工程
在第一部分介紹的都是基本命令,對于大型工程來說,會用到一些不太常用的概念或者功能。
什么是Project?
對于大型工程來說,project的概念變得更為重要。通常來說,簡單的工程只需要有一個project,而對于復雜的工程,有可能會出現(xiàn)project的嵌套。
Project通常指的是一個邏輯上相對獨立、完整,能夠獨立編譯的集合。通常來說,如果某一個CMakeLists.txt文件中出現(xiàn)了project()命令,那你應該能以該文件所在的目錄為根目錄進行一次完整的編譯。
(https://stackoverflow.com/questions/26878379/in-cmake-what-is-a-project)該命令也會如上文所說的,影響CMAKE_PROJECT_NAME等變量的值。
文件組織
文件組織方式就見仁見智了。不過通常來說,為了方便cmake的管理,建議以modules的形式扁平地組織,并且在每個module中設置有限的文件層次。比如說我們有一個moduleA,其下面有src、include和test三個目錄,而在include目錄下面,再根據(jù)具體的功能分為不同的目錄,再下一級就只有頭文件。
這樣在添加頭文件目錄的時候,統(tǒng)一添加為*/moduleA/include,而在源文件或者其他頭文件包含的時候,可以從include下一級目錄開始:#include "abc/a.hpp"。
模塊下的CMakeLists.txt
在一個模塊下,可以遵循以下規(guī)律編寫CMakeLists.txt:
- 設置內部模塊依賴
- 搜索內部依賴模塊的頭文件和庫文件
- 設置項目內第三方模塊依賴
- 搜索項目內第三方模塊依賴庫的頭文件和庫文件
- 設置和搜索本地的外部依賴庫
- 添加編譯目標
- 包含頭文件目錄、鏈接庫文件
- 設置安裝規(guī)則(比如一些配置文件)
- 設置單元測試
頭文件暴露
有的時候,有些頭文件只供內部使用,不想暴露在install后的頭文件目錄里。那就將其放在src路徑下。
依賴順序管理
CMake中鏈接庫的順序是a依賴b,那么b放在a的后面。
例如目標test依賴a庫、b庫, a庫又依賴b庫,那么順序如下:
target_link_libraries(test a b)
另外,假如目標test依賴a庫, a庫又依賴b庫,但test不直接依賴b庫,那么test不用鏈接b庫。
如果在一個工程中有多個target,那么可以用add_dependencies(<target> [<target-dependency>]...)命令,來定義依賴關系。這樣CMake會首先編譯被依賴的目標,隨后再編譯依賴的目標。
INTERFACE|PUBLIC|PRIVATE

如何調試
nm -a <target>命令查看符號表。
如果出現(xiàn)
Undefined symbols for architecture x86_64:
"_main"
可能是在沒有main的cpp文件定義add_executable。
構造函數(shù)和析構函數(shù)聲明了就要定義,要么用default。