笔者去年有篇文章介绍了将团队项目整改为 CMake 用到的一些命令,虽然尽力遵从 Modern CMake 的写法,但还是由于经验不足用到了一些非 Modern 的语法。今年三月初,公司层面开始推广 CMake 构建,项目群请了个专家带领某产品的各子系统进行 CMake 整改,我负责其中一个子系统,跟专家学到了很多规范和技巧。笔者结合最近的实践及网上的 CMake 资料写下本文,记录 CMake 的一些优秀实践。

CMake 不是构建系统

严格来讲,CMake 不是构建系统,而是一个跨平台的构建系统生成器。通过编写 CMakeLists.txt,告诉 CMake 生成相应的构建工程,如 Makefile,Ninja 或 MSBuild,从而增加了构建的可移植性。另外,CMake 拥有一个开发工具家族,包括 CMake、CTest、CPack 及 CDash。

Modern CMake?

CMake 自版本 2.8.12 引入 Modern CMake 的概念,3.12 之后的版本被称为 More Modern CMake,华为公司建议使用 3.14 及之后的版本。

语法

Modern CMake 围绕 TargetsProperties 展开,因此常用的命令如下:

创建 target

add_library()
add_exectutable()

target_sources()

为 target 指定参数

## 预处理头文件搜索路径 -I
target_include_directories()

## 编译宏 自动在指定的宏前添加 -D
target_compile_definitions()

## 编译选项
target_compile_options()

## 链接库搜索路径 -L
target_link_directories()

## 链接选项
target_link_options()

## 链接库
target_link_libraries()

让 CMake 帮你解析出应该添加哪些编译选项 cmake-compile-features

target_compile_features(mylib PRIVATE cxx_constexpr)

target_compile_features(Foo
  PUBLIC
    cxx_strong_enums
  PRIVATE
    cxx_lambdas
    cxx_range_for
)

推荐上面的用法而不是直接指定 c++ 标准

实践

Interface Library

Interface Library 是一种不需要构建的库(或 Header-Only 的库),由于具有传递性,可将其用于公共的依赖,如

add_library(project_options INTERFACE)
target_compile_features(project_options INTERFACE cxx_std_17)

静态库链接 Interface Library, 用 install 导出静态库会出现错误,可用如下方法解决

add_library(foo STATIC foo.cc)
target_link_libraries(foo PRIVATE $<BUILD_INTERFACE:project_options>)

BUILD_INTERFACE 和 INSTALL_INTERFACE 的解释见 Effective CMake 中相应的讲解。

Object Library

使用如下方法能同时编静态库和动态库,且源文件只需编一次。

add_library(foo_objs OBJECT)
target_sources(foo_objs foo.cc)

add_library(foo_static STATIC)
target_link_libraries(foo_static PRIVATE project_options foo_objs)
set_target_properties(foo_static PROPERTIES ARCHIVE_OUTPUT_NAME foo)    # 更改静态库名字

add_library(foo_shared SHARED)
target_link_libraries(foo_shared PRIVATE project_options foo_objs)
set_target_properties(foo_shared PROPERTIES LIBRARY_OUTPUT_NAME foo)    # 更改动态库名字

Generator expressions

生成表达式能根据不用的信息生成不同的 Properties。

add_executable(myapp main.cpp foo.c bar.cpp zot.cu)
target_compile_options(myapp
  PRIVATE $<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
)
target_compile_definitions(myapp
  PRIVATE $<$<COMPILE_LANGUAGE:CXX>:COMPILING_CXX>
          $<$<COMPILE_LANGUAGE:CUDA>:COMPILING_CUDA>
)

添加 Target

类似 Makefile,CMake 中可以添加定制的 target 以满足不同的构建目标。

add_custom_target(my_target)
add_dependencies(my_target foo bar)

更改目标的生成路径

set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 静态库
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 动态库
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # 可执行文件

find_package

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake/modules/")

find_package(GTest)

add_executable(foo foo.cc)

target_link_libraries(foo PRIVATE
    GTest::GTest GTest::Main
)

将静态库中的所有对象文件都链接到目标中,并循环查找依赖

## gcc -shared -o libfoo.so foo.o -Wl,--whole-archive libbar.a libfuzz.a -Wl,--no-whole-archive
target_link_libraries(foo PRIVATE
    -Wl,--whole-archive
    libbar.a
    libfuzz.a
    -Wl,--no-whole-archive
)

按需链接静态库中的符号,并循环查找依赖

## gcc -shared -o libfoo.so foo.o -Wl,--start-group libbar.a libfuzz.a -Wl,--end-group
target_link_libraries(foo PRIVATE
    -Wl,--start-group
    libbar.a
    libfuzz.a
    -Wl,--end-group
)

## gcc -shared -o libfoo.so foo.o -Xlinker "-(" libbar.a libfuzz.a -Xlinker "-)"
target_link_libraries(foo PRIVATE
    -Xlinker "-("
    libbar.a
    libfuzz.a
    -Xlinker "-)"
)

Tips

  • CMake is code,像对待代码仓里的其它代码一样对待 CMakeLists.txt,如相同的代码要抽象为函数
  • 运行完 cmake 命令生成构建工程后,可以去 build tree 中看一些生成的文件,如 flags.txt 和 link.txt
  • 使用 make VERBOSE=1 能查看具体的编译参数或链接参数
  • 使用正确的变量,如 CMAKE_CURRENT_SOURCE_DIR, CMAKE_PROJECT_NAME, PROJECT_NAME, CMAKE_SOURCE_DIR
  • 如果只更改了多个子模块中的一个,使用 make <target> -j 来编译,避免错误日志刷屏
  • 编译所有子系统,想查看编译日志,使用 make -j | tee /tmp/build_log.txt
  • 在每个源文件对应的目录中都应该有相应的 CMakeLists.txt
  • 不使用 file(GLOB)

TO-Learn