CMake - 常用配置
我从来没见过有一个工具比 CMake 还要难用,我完全不知道它的语法有什么规律,我不知道它究竟有多少内置的参数,我不知道 cmake generator expressions 该怎么写。但又不得不用它,所以这里记录一下日常开发中积累的经验。
cmake 是什么?
在开发 C/C++ 项目时,我们可借助 make 来构建项目,当项目逐渐扩大时,涉及到的源文件、第三方库越来越多,它们之间的依赖关系也越来越复杂,此时编写 Makefile 会比较困难。CMake 是一个跨平台的用于实现自动化构建的工具,使用 cmake 的配置文件,可以生成适合当前平台的 Makefile。而后用户可基于生成的 Makefile 来完成项目的构建。
因为 makefile 编写起来比较麻烦,因此人们实现了 cmake 并定义了 cmake 的一些规则,基于 cmake 的规则,生成 makefile。在初学阶段,可以将 cmake 理解为一个用来生成 makefile 的工具。
简单的示例
下面是一个简单的 C++ 程序,这里我们使用 cmake 将其构建为可执行文件。
// hello.cpp
#include <iostream>
int main(int argc, char **argv) {
std::cout << "hello world\n";
return 0;
}
新建一个命名为 CMakeLists.txt 的文件,其内容如下:
# 指定 cmake 最低版本
cmake_minimum_required(VERSION 3.5)
# 指定当前项目的名称
project(hello)
# 添加一个可执行文件,并提供用于构建可执行文件的 .cpp 文件
add_executable(hello hello.cpp)
然后使用如下命令执行构建:
➜ ~/code/cmake-101/01 git:(main) ✗ mkdir build
➜ ~/code/cmake-101/01 git:(main) ✗ cd build
➜ ~/code/cmake-101/01/build git:(main) ✗ cmake ..
-- The C compiler identification is AppleClang 16.0.0.16000026
-- The CXX compiler identification is AppleClang 16.0.0.16000026
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.7s)
-- Generating done (0.0s)
-- Build files have been written to: /Users/wangyu/code/cmake-101/01/build
执行了 cmake .. 后,cmake 会在 .. 目录中寻找 CMakeLists.txt 文件,并基于此文件生成一个 makefile:
➜ ~/code/cmake-101/01/build git:(main) ✗ ls
CMakeCache.txt CMakeFiles Makefile cmake_install.cmake
执行 make 后即可完成构建:
➜ ~/code/cmake-101/01/build git:(main) ✗ make
[ 50%] Building CXX object CMakeFiles/hello.dir/hello.cpp.o
[100%] Linking CXX executable hello
[100%] Built target hello
➜ ~/code/cmake-101/01/build git:(main) ✗ ./hello
hello world
为什么要创建一个 build 目录?
执行 cmake .. 后会生成很多文件,如下所示:
$ tree
.
├── CMakeCache.txt
├── CMakeFiles
│ ├── 3.28.1
│ │ ├── CMakeCCompiler.cmake
│ │ ├── CMakeCXXCompiler.cmake
│ │ ├── CMakeDetermineCompilerABI_C.bin
│ │ ├── CMakeDetermineCompilerABI_CXX.bin
│ │ ├── CMakeSystem.cmake
│ │ ├── CompilerIdC
│ │ │ ├── CMakeCCompilerId.c
│ │ │ ├── CMakeCCompilerId.o
│ │ │ └── tmp
│ │ └── CompilerIdCXX
│ │ ├── CMakeCXXCompilerId.cpp
│ │ ├── CMakeCXXCompilerId.o
│ │ └── tmp
│ ├── CMakeConfigureLog.yaml
│ ├── CMakeDirectoryInformation.cmake
│ ├── CMakeScratch
│ ├── Makefile.cmake
│ ├── Makefile2
│ ├── TargetDirectories.txt
│ ├── cmake.check_cache
│ ├── hello.dir
│ │ ├── DependInfo.cmake
│ │ ├── build.make
│ │ ├── cmake_clean.cmake
│ │ ├── compiler_depend.make
│ │ ├── compiler_depend.ts
│ │ ├── depend.make
│ │ ├── flags.make
│ │ ├── hello.cpp.o
│ │ ├── hello.cpp.o.d
│ │ ├── link.txt
│ │ └── progress.make
│ ├── pkgRedirects
│ └── progress.marks
├── Makefile
├── cmake_install.cmake
└── hello
为了避免 cmake 产生的文件和目录和项目中源文件混在一起,因此新建了一个 build 目录,然后在此目录中执行 cmake ..。这里目录命名不一定非得是 build。
cmake 提供了一些命令行选择,可以更快地完成与上面相同的操作:
# -H. 表示在 . 目录中搜索 CMakeLists.txt
# -Bbuild 表示在 build 命令中构建
$ cmake -H. -Bbuild
指定 C++ 标准
在创建 C++ 项目时,需要确定当前项目使用的 C++ 标准。下面的代码中,我使用了 C++17 的标准,如果不明确指定使用 C++17,编译时会失败。
#include <iostream>
int main(int argc, char **argv) {
if constexpr (sizeof(int) == 4) {
std::cout << "sizeof(int) == 4\n";
}
return 0;
}
可以在 CMakeLists.txt 中指定所使用的 C++ 标准:
# 指定默认的 C++ 标准
set(CMAKE_CXX_STANDARD 17)
# 如果当前编译器不支持 C++17,则构建失败
set(CMAKE_CXX_STANDARD_REQUIRED True)
设置编译选项
可以使用 add_compile_options 添加全局的编译选项,以这种方式添加的编译选项会用于当前目录下的所有的编译中。
add_compile_options(-Wall -Wextra -Wpedantic)
如果存在多个目标文件,希望仅针对某个目标添加编译选择,可以使用 target_compile_options,它的语法如下:
target_compile_options(<target> [BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
下面是一个例子:
target_compile_options(foo PRIVATE
${flags}
)
可见性的含义如下:
- PRIVATE,编译选项会应用于给定的目标,不会传递给与目标相关的目标。即使 app 将链接到 foo 库,app 也不会继承 foo 目标上设置的编译器选项。
- INTERFACE,给定的编译选项将只应用于指定目标,并传递给与目标相关的目标。
- PUBLIC,编译选项将应用于指定目标和使用它的目标。
CMAKE_CXX_FLAGS is used to add flags for all C++ targets. That’s handy to pass general arguments like warning levels or to selected required C++ standards. It has no effect on C or Fortran targets and the user might pass additional flags.
add_compile_options adds the options to all targets within the directory and its sub-directories. This is handy if you have a library in a directory and you want to add options to all the targets related to the library, but unrelated to all other targets. Additionally, add_compile_options can handle arguments with generator expressions. The documentation explicitly states, that
判断编译器类型
cmake_minimum_required (VERSION 3.12.2)
project (list_cmd_test)
message("${CMAKE_CXX_COMPILER_ID}")
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang")
message("Clang")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
message("GNU")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Intel")
message("Intel")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
message("MSVC")
endif()
指定编译器
你的机器上可能安装了多个版本的编译器,或者编译器的路径不在 PATH 中,此时你需要告诉 CMake 你想要使用的编译器的路径,具体的方法有多种:
通过环境变量指定
在执行 CMake 之前设置 CC 和 CXX 环境变量,将其指向 C 和 C++ 编译器。
$ export CC="<path>/gcc"
$ export CXX="<path>/g++"
$ cmake ..
通过 CMake 命令行参数执行
$ cmake .. -DCMAKE_CXX_COMPILER=<path>/g++ -DCMAKE_C_COMPILER=<path>/gcc
指定编译链接选项
设置 LDFLAGS 环境变量,可以指定连接器的选项。
设置 CXXFLAGS 和 CFLAGS 会初始化 CMAKE_CXX_FLAGS 和 CMAKE_C_FLAGS 两个 Cache 变量,并进而设置 C 和 C++ 编译器的编译选项。
设置构建类型
在项目开发阶段,我们希望构建 Debug 版本,以包含完备的调试信息和较快的编译速度。而软件发布时候,我们希望构建 Release 版本,得到更高性能的可执行程序。这些可以通过配置构建类型,并基于不同构建类型对编译和链接做细致的配置。
通过设置 CMAKE_BUILD_TYPE 可以控制构建类型。设置构建类型为 Release 后,变量 CMAKE_CXX_FLAGS_RELEASE 中的编译选项就会编译器。因此在 CMakeLists.txt 中可以判断 CMAKE_BUILD_TYPE 的取值,并给 CMAKE_CXX_FLAGS_<CMAKE_BUILD_TYPE> 设置不同的编译选项。
$ cd cmake-build-debug
$ cmake .. -DCMAKE_BUILD_TYPE=Debug
$ cd cmake-build-release
$ cmake .. -DCMAKE_BUILD_TYPE=Release
可选的构建类型有以下几种:
- Debug:包含基本的调试标志。
- Release:启动优化。
- MinSizeRel:生成的目标文件最小,但不一定最快。
- RelWithDebInfo:启动优化,同时包含调试信息。
使用 ExternalProject 引入外部依赖
C++ 项目中往往需要引入一些外部依赖,比如日志库、网络库等,这里以引入 libuv 这个网络库为例。
首先去 GitHub 上下载 libuv 的源码,将其解压到 third-party 目录中。libuv 的根目录中有 CMakeLists.txt 文件,因此执行 cmake 和 make 完成构建。
$ cd third-party/libuv-1.x
$ mkdir build && cmake .. && make
构建完成后,可以在 third-party/libuv-1.x/build 目录中看到静态库 libuv_a.a,在 CMakeLists.txt 中引入,就可以使用 libuv 了。
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_SOURCE_DIR}/third-party/libuv-1.x/incluce) # libuv 的头文件
target_link_libraries(${PROJECT_NAME} PRIVATE
${CMAKE_SOURCE_DIR}/third-party/libuv-1.x/build/libuv_a.a) # 静态库
上述下载源码、解压缩、编译等等操作都是我手动执行的,若项目需要发布出去,这些步骤自然是需要自动化完成的。你当然可以使用脚本完成,但将其集成到 CMake 中,用户就可以按照习惯执行 cmake .. && make 完成构建了。
此时你可以使用 CMake 提供的 ExternalProject 相关命令实现你的目标。
ExternalProject
CMake 的 ExternalProject 功能提供了多个命令,其文档在这里:ExternalProject。
这里主要使用的 ExternalProject_Add 命令,此命令用来实现对外部项目的下载、构建、安装、测试等步骤。这个命令接收的参数很多,但可以分为几类,这里只描述比较重要的一些:
- Directory Options: 基本的一些目录配置
PREFIX: 外部项目的构建目录,CMake 在构建此项目时产生的临时文件会存储在这里SOURCE_DIR: 外部项目的源码地址BINARY_DIR: 执行 CMake 的目录,编译产物会存储在此目录下
- Download Step Options: 明确如何下载外部项目,可以通过执行命令、URL、Git 等多种方法下载外部项目
DOWNLOAD_COMMAND: 下载命令URL: 下载链接
- Update Step Options: 明确 CMake 重新构建的时候需要执行的动作
UPDATE_COMMAND: CMake 重新构建前执行的命令,比如执行 git pull 拉取最新源码等等
- Configure Step Options: 配置构建的细节
CONFIGURE_COMMAND: 如果不是 CMake 项目,则一定要配置此命令,比如执行./configure --prefix=${INSTALL_PREFIX}这样的命令,如果是 CMake 项目则可以不用配置CMAKE_ARGS: 如果是 CMake 项目,可以通过此配置项传递一些参数,如-DCMAKE_BUILD_TYPE=Debug
- Build Step Options: 指出构建的细节
BUILD_COMMAND: 执行构建的命令,比如make -j 8
- Install Step Options: 安装的配置项
- Target Options:
DEPENDS: 依赖的目标
ExternalProject_Add 的参数很多,使用的时候可以在仔细阅读文档。其中目录部分的配置需要重视,因为这决定了你构建产物存放的地址。如果你仅仅配置了 PREFIX 这一个参数,其他目录的默认值如下:
TMP_DIR = <prefix>/tmp
STAMP_DIR = <prefix>/src/<name>-stamp
DOWNLOAD_DIR = <prefix>/src
SOURCE_DIR = <prefix>/src/<name>
BINARY_DIR = <prefix>/src/<name>-build
INSTALL_DIR = <prefix>
LOG_DIR = <STAMP_DIR>
例子
我的项目中使用了 libuv,我不想将 libuv 的全部源码包含在自己的代码库中,由于项目常常在内网机器上开发,我也不能使用 Git、HTTP 等方式下载 libuv 的源码,因此我选择将 libuv 的 zip 包放到代码库中,在构建的时候自动解压,完成构建。
新建一个 cmake/libuv.cmake 文件,在此文件中配置 libuv 的细节,然后在项目的 CMakeLists.txt 中包含它:
# CMakeLists.txt
include(${PROJECT_SOURCE_DIR}/cmake/libuv.cmake)
cmake/libuv.cmake 内容如下:
# cmake/libuv.cmake
include(ExternalProject) # 引入 ExternalProject 模块
set(THIRD_PARTY_DIR ${CMAKE_SOURCE_DIR}/third-party)
set(LIBUV_PREFIX ${THIRD_PARTY_DIR}/libuv-1.x-prefix)
set(LIBUV_ARCHIVE_DIR ${THIRD_PARTY_DIR}/libuv-1.x.zip) # 压缩包
ExternalProject_Add(libuv # 项目名称
PREFIX ${LIBUV_PREFIX} # 指定目录
SOURCE_DIR ${LIBUV_PREFIX}/src/libuv-1.x
BINARY_DIR ${LIBUV_PREFIX}/build
CMAKE_ARGS # 传入 CMake 的参数
-DCMAKE_BUILD_TYPE=Release
-DBUILD_TESTING=OFF
DOWNLOAD_COMMAND ${CMAKE_COMMAND} -E tar xzf ${LIBUV_ARCHIVE_DIR} # 下载命令
INSTALL_COMMAND ""
)
set(LIBUV_INCLUDE_DIRS ${LIBUV_SOURCE_DIR}/include)
set(LIBUV_LIBRARIES ${LIBUV_PREFIX}/build/libuv_a.a)
上面的配置中我使用了如下下载命令:
${CMAKE_COMMAND} -E tar xzf ${LIBUV_ARCHIVE_DIR}
这行命令的功能是对 libuv-1.x.zip 进行解压缩,其中 ${CMAKE_COMMAND} 就是 cmake 可执行程序, -E 选择是给 cmake 传入一组命令,让 cmake 执行。cmake 提供了一些常见 shell 命令的封装,保证它们在各个平台上表现一致,你可以在 Run a Command-Line Tool 中找到更多命令。
这行命令会压缩包解压到 ${DOWNLOAD_DIR} 目录下,最终源码会存放在 <prefix>/src/libuv-1.x 下,因此我配置 SOURCE_DIR 为 ${LIBUV_PREFIX}/src/libuv-1.x。
因为在 ${LIBUV_PREFIX}/src/libuv-1.x 存在 CMakeLists.txt 文件,因此不需要指定配置和构建命令了,CMake 默认会执行 cmake 完成配置,并基于生成的 Makefile 执行 make 命令完成构建。
为了将构建产物导出,我定义了两个变量,存放 inlucde 目录和静态库文件。
更新项目的 CMakeLists.txt 内容如下,即可将 libuv 加入到本项目中:
# CMakeLists.txt
include(${PROJECT_SOURCE_DIR}/cmake/libuv.cmake)
target_include_directories(${PROJECT_NAME} PRIVATE ${LIBUV_INCLUDE_DIRS}) # 将 libuv 的 include 目录加入到搜索路径中
target_link_libraries(${PROJECT_NAME} PRIVATE ${LIBUV_LIBRARIES}) # 将 libuv 的静态库加到依赖库列表中
add_dependencies(${PROJECT_NAME} libuv) # 声明 libuv 是一个依赖性,这样在 libuv 外部项目构建完成后,才会开始本项目的构建