最近在负责一个大型工程的CMake编译系统管理,整理一些工作过程中积累下来的知识片段和技巧。CMake是一个跨平台的编译工具。
基本操作
通过编写CMakeLists.txt指挥cmake进行构建和编译。
通常我们会在根目录新建一个build文件夹,然后依次执行:
cmake ..
make
make install
其中cmake
命令主要任务是按照CMakeLists.txt编写的规则生成MakeFile,而make
会按照MakeFile进行编译、汇编和链接,从而生成可执行文件或者库文件。make install
则是将编译好的文件安装到指定的目录。
CMake常用的命令或函数包括:
定义项目:
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最低版本,如果不指定的话系统会自己指定一个,但是也会扔出一个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>)
会创建一个子作用域,里面可以使用父作用域里定义的变量,但里面定义的变量在父作用域不可见,同样,在子作用域修改父作用域里的变量不会影响父作用域。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 */
。字符串操作、循环、判断、文件/变量存在判断等
这些命令同样有用,请参考网络资料。
当代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的区分。
比如:
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的私有属性中,而不会传递到用户那里。
导入目标
当我们执行find_package(Boost 1.55 REQUIRED COMPONENTS regex)
的时候,CMake实际执行了FindBoost.cmake
脚本,并由此导入了目标Boost::boost
和Boost::regex
,这是为什么我们能通过target_link_libraries()
来依赖这些目标。
然而部分第三方库并不那么守规矩,比如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,而对于复杂的工程,有可能会出现project的嵌套。
Project通常指的是一个逻辑上相对独立、完整,能够独立编译的集合。通常来说,如果某一个CMakeLists.txt文件中出现了project()
命令,那你应该能以该文件所在的目录为根目录进行一次完整的编译。
(https://stackoverflow.com/questions/26878379/in-cmake-what-is-a-project)该命令也会如上文所说的,影响CMAKE_PROJECT_NAME
等变量的值。
文件组织
文件组织方式就见仁见智了。不过通常来说,为了方便cmake的管理,建议以modules的形式扁平地组织,并且在每个module中设置有限的文件层次。比如说我们有一个moduleA,其下面有src、include和test三个目录,而在include目录下面,再根据具体的功能分为不同的目录,再下一级就只有头文件。
这样在添加头文件目录的时候,统一添加为*/moduleA/include
,而在源文件或者其他头文件包含的时候,可以从include下一级目录开始:#include "abc/a.hpp"
。
模块下的CMakeLists.txt
在一个模块下,可以遵循以下规律编写CMakeLists.txt:
- 设置内部模块依赖
- 搜索内部依赖模块的头文件和库文件
- 设置项目内第三方模块依赖
- 搜索项目内第三方模块依赖库的头文件和库文件
- 设置和搜索本地的外部依赖库
- 添加编译目标
- 包含头文件目录、链接库文件
- 设置安装规则(比如一些配置文件)
- 设置单元测试
头文件暴露
有的时候,有些头文件只供内部使用,不想暴露在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>
命令查看符号表。
如果出现
Undefined symbols for architecture x86_64:
"_main"
可能是在没有main的cpp文件定义add_executable。
构造函数和析构函数声明了就要定义,要么用default。