CMake学习笔记

1,140 阅读7分钟

总述

通过一段时间C/C++项目的开发工作,了解到C/C++项目也有类似于Android Gradle一样的构建工具--CMake。一般来说C++工程都是通过MakeFile来管理的,但是,MakeFile也有明显的缺点:

  • make在不同平台有不同的分支,如:gmake、nmake、dmake
  • MakeFile文件是面向规则的,不是面向工程的,造成了文件本身的可读性和维护性比较差。 因此,就有诞生了CMake,CMake是在MakeFile之上抽象出的一层定义,针对不同平台,翻译成对应的MakeFile(有点像java)这样做到了跨平台和隐藏规则。目前CMake对各种主流编译器具有优秀的支持,大家可放心使用。

核心概念

  • Target:执行单元,可以是生成库或者执行文件,也可以是执行一段代码,甚至可以是空内容
  • Generator:生成器.
  • cmake-commands:cmake 命令,通常写在 CMakeLists.txt / *.cmake 文件中调用的内置语法和函数都称之为 cmake 命令
  • cmake-generator-expressions:生成器表达式,一种特殊的表达式,编译过程才生效
  • Command-Line:CMake 控制台命令,即在终端控制台使用的命令,可以用于触发配置和编译之外,还可以用于文件操作以及解压缩等

Generator生成器:CMake属于meta-build(源编译)系统,有自己的交互语法,使用时需要先将自身的语法翻译成其他编译系统,这个翻译过程称之为configure,在执行配置命令时可以通过-G XXX来指定翻译的目标编译系统,在未指定目标编译时 CMake 会默认指定一个Generator:Linux/MacOs都使用的是MakeFile,Window:.sln/.vcxproj

工作流程及配置

CMake的工作流程大致分为两到三步:

1.配置:输入源文件目录,指定目标编译系统,添加编译选项,生成目标编译系统
# 新建编译缓存路径
mkdir out && cd out
mkdir debug && cd debug
# 执行配置
cmake ../.. -G Ninja -DCMAKE_BUILD_TYPE=Debug

2. 输入目标编译系统,执行编译
# 执行编译
cmake --build .

3.安装(可选):将编译产物安装到指定位置(需要 CMakeLists.txt 中支持安装规则)
# 安装(高版本cmake支持)
cmake --install .
# 低版本cmake可用
cmake --build . --target install

Ninja 属于目标编译系统,也是CMake支持的除上述Generator的一种。且效率很高,默认会根据系统处理器内核数来分配编译线程数。

配置阶段的参数主要为以下几种:

  • -G Generator:用于指定目标编译系统,未指定时取 cmake默认编译系统
  • -DCMAKE_BUILD_TYPE=Debug/Release:用于指定编译类型
  • -DCMAKE_<LANG>_COMPILER=clang:用于指定语言编译器,默认由 cmake 搜索指定,如:-DCMAKE_CXX_COMPILER=clang++
  • -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake:用于指定交叉编译工具链,一般用于非本地平台编译,如 Android,ARM 平台编译等
  • -DKey=Value:用于配置CMakeLists.txt或者工具链中的option选项等

CMake 执行配置时从指定路径下的CMakeLists.txt开始加载,遇到第一个project(...)时开始检查编译环境中的编译器,执行完所有代码后将全局变量保存至CMakeCache.txt文件,再次执行配置时不会再修改全局变量,所以遇到一些非预期错误时,可先删除缓存路径下的CMakeCache.txt文件。

简单使用

以最简单的样例开始:

# 指定最低cmake版本要求,每个CMakeLists.txt的首行都应该加上最低版本限制,避免出现运行的 CMake 版本过低导致不明错误,Android Studio目前可用最高版本3.10.23.18(目前不可用)
cmake_minimum_required(VERSION 3.10.2)

# 创建项目名称
project(myproject)

# 添加名为mylib的目标,类型为动态库
add_library(myproject SHARED ***.cc ***.h ...)

# 添加名为myexe的目标,类型为可自行文件
add_executable(myexe main.cc)

# 为myexe添加对mylib的链接关联
target_link_libraries(myexe PUBLIC mylib)

上述CMakeLists.txt文件属于一种最基本的结构,下面以如下项目结构开始:

企业微信截图_e3a9abd2-3b46-4740-9e81-c968a39c36d5.png 项目结构较为复杂时,为了方便管理,建议使用多级CMakeLists.txt来管理。在父CMakeListd.txt中通过add_subdirectory()来添加:

cmake_minimum_required(VERSION 3.10.2)
project(mylib)

option(build_with_test "是否编译测试代码" ON)

add_subdirectory(third_party/xhook ${CMAKE_BINARY_DIR}/xhook)
add_subdirectory(mylib)

if (build_with_test)
  add_subdirectory(test)

mylib目录下的CMakeLists.txt可以简单编写:

add_library(mylib SHARED pupu.cc util.h)
target_link_libraries(mylib PUBLIC xhook)

或者采用如下写法,在父CMakeLists.txt的target_link_libraries()中添加mylib即可

aux_source_directory(. lib_file)
#include_directories(${rootpath}/common list)
add_library(mylib ${lib_file})

测试代码CMakeLists.txt:

add_executable(test tests.cc)
target_link_libraries(test PUBLIC mylib)

变量

CMake中的变量可划分为三种:

  1. 普通变量:仅在当前 CMakeLists.txt 及子项目(通过 add_subdirectory 添加的项目)中生效,可取消设置
  2. 缓存变量:缓存变量则会写到 CMakeCache.txt 缓存文件中全局可用
  3. 环境变量:CMake的环境变量
# 常规变量
# set(<variable> <value>... [PARENT_SCOPE])
set(NORMAL_VAR "normal variable")
unset(NORMAL_VAR)
# 缓存变量
# set(<variable> <value>... CACHE <type> <docstring> [FORCE])
set(CACHE_VAR "cache variable" CAHCE STRING "description")
# 环境变量
# set(ENV{<variable>} [<value>])
set(ENV{PATH} "$ENV{PATH}:${CMAKE_CURRENT_LIST_DIR}")
# 获取变量
message(STATUS "NORMAL_VAR = ${NORMAL_VAR}")
message(STATUS "CACHE_VAR = ${CACHE_VAR}")
message(STATUS "ENV_PATH = $ENV{PATH}")

日常开发中,可能还会用到的变量:

名称解释
CMAKE_PROJECT_NAME顶层项目名称,由project(xxx)指定
PROJECT_NAME多级项目时最后一个项目名称,由project(xxx)指定
CMAKE_SOURCE_DIR获取入口 cmake 文件所在路径,相对路径时建议使用 CMAKE_CURRENT_LIST_DIR
CMAKE_CURRENT_LIST_DIR获取当前 cmake 文件(可以是CMakeLists.txt,也可是xxx.cmake)所在路径,CMAKE_CURRENT_LIST_DIR 更为常用
CMAKE_BINARY_DIR顶层缓存路径,即执行 cmake 配置的路径
CMAKE_CURRENT_BINARY_DIR当前缓存路径,add_subdirectory(subproject subpath) 添加的 subpath
PROJECT_BINARY_DIR当前项目缓存路径,即最后一个project所在路径
CMAKE_BUILD_TYPE编译类型,常用有 Debug Release,RelWithDebInfo MinSizeRel不常用
CMAKE__COMPILER编译器信息
CMAKE_SYSTEM_NAME描述目标平台名称,如: WindowsDarwinLinuxAndroidiOS,交叉编译时由工具链指定
CMAKE_SYSTEM_PROCESSOR描述目标处理器类型,交叉编译时由工具链指定
CMAKE_HOST_SYSTEM_NAME描述目标处理器类型,交叉编译时由工具链指定
CMAKE_HOST_SYSTEM_PROCESSOR描述本地处理器类型

编译选项

CMake添加编译选项主要通过CMAKE_<LANG>_FLAGS来设置编译选项,例如:CMAKE_C_FLAGS/CMAKE_CXX_FLAGS分别指 C 和 C++编译选项。CMAKE_XXX_FLAGS为字符串类型,通常使用方式:

# 分别添加`C11`和`C++14`特征支持检查
target_compile_features(mylib PUBLIC c_std_11 cxx_std_14)
# 添加预编译头文件,通常用于编译提速
target_precompile_headers(mylib PRIVATE precompile.h)
# 相当于-DFoo=1
target_compile_definitions(mylib PUBLIC -DFoo=1)
# 表达式编译选项
target_compile_options(mylib PUBLIC -fno-exceptions
  PRIVATE $<$<COMPILE_LANGUAGE:C>:${__CFLAGS_C}>            # C编译选项
  PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${__CFLAGS_CXX}>        # C++编译选项
  PRIVATE $<$<CXX_COMPILER_ID:GNU>:${__CFLAGS_CXX_GNU}>     # GNU编译器生效
          $<$<CXX_COMPILER_ID:Clang>:${__CFLAGS_CXX_CLANG}> # Clang编译器生效
          $<$<CXX_COMPILER_ID:AppleClang>:${__CFLAGS_CXX_CLANG}>
)
# 添加头文件搜索路径,相当于 -Iinclude
target_include_directories(mylib PUBLIC include)
# 添加库文件查找路径,相当于 -Llib
target_link_directories(mylib PUBLIC lib)
# 添加库链接,相当于 -lfoo
target_link_libraries(mylib PUBLIC foo)
# 添加链接选项,启用lld链接器
target_link_options(mylib PUBLIC -fuse-ld=lld)

属性继承

CMake中一切皆为target,可以将其理解成JAVA中的Object,有对象想就有继承关系,在CMake中已有继承关系如下:

  • SYSTEM : 不常用,不做解释
  • AFTER|BEFORE:不常用,不做解释
  • PUBLIC:当前target生效,且依赖target也生效
  • PRIVATE:仅当前target生效,依赖target不生效
  • INTERFACE:当前target不生效,仅依赖target生效

CMAKE VS BAZEL

CMake 优缺点

CMake通过执行CMakeLists来生成makefile。从而构建C++项目。由于一直遵循的理念是:一切皆为target to target property,所以cmake通过每个target之前的依赖关系,实现target属性(头文件查找路劲、编译选项、依赖)的自动传递,非常方便的处理大型C++项目的复杂依赖关系,从而大大提高C/C++开发人员的生产力。

优点

  1. 开源社区成熟,相关博客解决思路较多
  2. 易于维护和修改
  3. 灵活性高(相比较makefile),开发者可以在CMakeLists中实现各种自定义功能 缺点
  4. 仅支持本地编译,不支持云编译,不支持多语言
  5. 缺少处理依赖的功能,比如下载依赖模块、指定依赖的版本信息以及获取依赖的版本信息等

Bazel优缺点

Bazel是Google开源的编译构建工具,同CMake一样都是优秀的项目构建工具,但是不同于CMake的是:Bazel采用了C/S运行模式,主要是为云编译而生,目前已经广泛用于云计算领域,例如k8s、kubevirt等都是采用Bazel构建。

优点

  1. 多语言编译
  2. C/S模式,云编译
  3. 非常方便处理依赖:用户可声明依赖的地址、版本、构建规则,Bazel自动下载对应版本的依赖,并根据规则构建对应依赖的target
  4. 优秀的缓存:增量编译速度非常快 缺点
  5. 使用难度较高,社区资源匮乏

总的来说:如果项目从0开始,且团队没有Bazel基础,推荐使用CMake;项目开发完成,处于维护阶段,建议CMake(迁移Bazel成本较高,没有足够目标不建议转Bazel);项目从0开始,或者项目处于初期,预期未来项目将会比较复杂,且团队有Bazel经验,或者愿景,推荐Bazel。

参考

cmake官方文档

bazel官方文档

Bazel入门