WangYu::Space

Study, think, create, and grow. Teach yourself and teach others.

CMake - 常用配置

分类:工具标签: cmake创建时间:2022-12-05 00:00:00

我从来没见过有一个工具比 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}
)

可见性的含义如下:

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 之前设置 CCCXX 环境变量,将其指向 C 和 C++ 编译器。

$ export CC="<path>/gcc"
$ export CXX="<path>/g++"
$ cmake ..

通过 CMake 命令行参数执行

$ cmake .. -DCMAKE_CXX_COMPILER=<path>/g++ -DCMAKE_C_COMPILER=<path>/gcc

指定编译链接选项

设置 LDFLAGS 环境变量,可以指定连接器的选项。

设置 CXXFLAGSCFLAGS 会初始化 CMAKE_CXX_FLAGSCMAKE_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

可选的构建类型有以下几种:

使用 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 命令,此命令用来实现对外部项目的下载、构建、安装、测试等步骤。这个命令接收的参数很多,但可以分为几类,这里只描述比较重要的一些:

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 外部项目构建完成后,才会开始本项目的构建

评论 (评论内容仅博主可见,不会公开显示)