本文尝试以最简要和方式阐述 CMake 的核心观念和核心问题。

TODO and NOT TODO

  • 不要使用 PROJECT_SOURCE_DIR 以支持 subproject 不要使用 CMAKE_SYSTEM 检测系统,而是使用 CMAKE_HOST_SYSTEM_NAME 以支持交叉编译

声明式语言

CMake 是一个声明式的语言,而不是一个命令式的编程语言。所谓声明式编程,就是告诉计算机“做什么”而不是“怎么做”。另外一个与传统声明式语言不同的:CMake 中的 所有变量类型都是字符串类型 。尽管 CMake 有布尔值的概念,但是其可以看作是“具有布尔含义的字符串” 。

以 string 的 REPLACE 为例,让我们看看它的 API:

string(REPLACE <模式> <替换串> <输出变量> <串>)

这里看到,我们告诉 CMake: 按照模式去替换串中的内容并将替换后的串写入到输出变量中。

其余 API 与此类似。

事实上,在条件语句和 生成表达式 中,CMake 还残留着一些命令式编程的痕迹。

变量和变量值

在编写 CMakeLists.txt 的时候,需要注意变量和变量值的区别:

我们使用 set() 去创建一个变量,使用 string()list() 等去操纵变量,以 list() 为例:

cmake list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH})

这里 CMAKE_MODULE_PATH 是一个 变量 ,而 ${ECM_MODULE_PATH} 是一个变量值。

除了使用 $\{} 取变量值以外,也可以使用 $%% 取变量值,但是 $ 只适用于不引起歧义的情况下使用, %% 一般是在 *.in 文件中使用

${var} 将会导致变量在原地进行展开,由于路径代表的变量展开后可能含有空格,因此,取路径变量的值时应当总是使用双引号括起来

条件语句会先于变量展开,因此条件语句中的变量无需取值即可使用。自 3.1+ if 条件语句也支持取值手法。[1]

命令和子命令

命令大小写随意,子命令必须大写。一般约定命令使用全小写

presets

例如:

{
"version": 1,
"cmakeMinimumRequired": {
   "major": 3,
   "minor": 1,
   "patch": 5
},
"configurePresets": [
   {
      "name": "dev",
      "displayName": "Debug",
      "description": "",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/cmake-build-debug",
      "cacheVariables": {
      "CMAKE_BUILD_TYPE": "Debug",
      "CMAKE_PREFIX_PATH": "C:/CraftRoot"
      },
      "environment": {
      "PATH": "$penv{PATH};C:/CraftRoot/bin;C:/CraftRoot/dev-utils/bin;C:/CraftRoot/msys/mingw64/bin"
      }
   }
]
}

使用 cmake --preset=dev 去构建

对于 CLion 而言,可以在 CMakePresets.json 文件编辑器的右下角设置 schema 以改善补全效果

基于目标的构建

目标 (Target)生成表达式 (Generator Expressions) 是 CMake 最核心的概念和最有效的工具,通过这两者可以方便地描述一个工程的生成方式。

目标由三部分构成: target_include_directoriestarget_link_libraries 、target_compile_options 、target_compile_features

target_include_directories

target_include_directories 用于导入或导出头文件。

target_include_directories(目标 [SYSTEM] [AFTER|BEFORE]
[<INTERFACE|PUBLIC|PRIVATE> [itemss]]
)
  • 如果不指定 INTERFACE 而只指定 PUBLIC/PRIVATE ,则后续列出的头文件被视为 导入头文件

target_include_directories(main PRIVATE fmt::fmt)
  • 如果指定了 INTERFACEPUBLIC ,则后续的头文件被视为 导出头文件

target_include_directories(main INTERFACE PUBLIC ./include)

导入的头文件被填充到 INCLUDE_DIRECTORIES 属性,而到处的头文件被导出到 INTERFACE_INCLUDE_DIRECTORIES 属性。

一般来说,导出的头文件在被包含和被安装时一般不同,这时可以使用 生成表达式 来描述:

target_include_directories(mylib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/mylib>
$<INSTALL_INTERFACE:include/mylib>  # <prefix>/include/mylib
)

target_compile_options

target_compile_features

构建需求和使用需求

target_include_directories(), target_compile_definitions() 和 target_compile_options() 三个命令指明了代码的构建需求和使用需求。其中构建需求是通过更改 target 的 INCLUDE_DIRECTORIES, COMPILE_DEFINITIONSCOMPILE_OPTIONS 属性,而使用需求是通过更改 target 的 INTERFACE_INCLUDE_DIRECTORIES, INTERFACE_COMPILE_DEFINITIONSINTERFACE_COMPILE_OPTIONS 属性。

每个命令都有 PRIVATE, PUBLIC 和 INTERFACE 三种模式:

  • PRIVATE 指明了 target 构建时的需求

  • PUBLIC 指明了 target 构建时和被使用时的需求

  • INTERFACE 指明了 target 被使用时的需求

例如:

target_include_directories(
    devlib
    INTERFACE "devlib/include"
)

表示使用 devlib 时需要导入 devlib/include 路径,其它包可以以这样的方式使用:

target_include_directories(
    lib2
    PRIVATE devlib
)

这表明 devlib 只是 lib2 构建时的依赖。

参见 [创建可重定位包](#创建可重定位包) 来学习如何正确地指明使用需求。例如:

target_include_directories(
    devlib
    INTERFACE
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:include/devlib>
)

表示当 devlib 作为一个子项目使用时需要包含的路径是 ${CMAKE_CURRENT_SOURCE_DIR}/include ,而被安装后使用 find_package 的方式使用时需要包含的路径为 ${PREFIX}/include/devlib

接口库

一个 接口 (INTERFACE) 库不会产生任何二进制文件。其作用只是为了指明需求。

接口库可能需要使用 INTERFACE 模式下的 target_include_directories(), target_compile_definitions(), target_compile_options(), target_sources(), and target_link_libraries() 命令来修改 target 的 INTERFACE_INCLUDE_DIRECTORIES, INTERFACE_COMPILE_DEFINITIONS, INTERFACE_COMPILE_OPTIONS, INTERFACE_LINK_LIBRARIES, INTERFACE_SOURCES, and INTERFACE_POSITION_INDEPENDENT_CODE 属性。

自 CMake 3.19 之后,接口库可以包含一些源文件,这些源文件可以作为其它 target 源文件的一部分,但是接口库本身依然不会编译这些源文件

接口库的一个主要目的是 header-only 库:

add_library(Eigen INTERFACE
  src/eigen.h
  src/vector.h
  src/matrix.h
  )
target_include_directories(Eigen INTERFACE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
  $<INSTALL_INTERFACE:include/Eigen>
)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 Eigen)

在此处,Eigen 实际上只在编译器有效,而在连接期无效

另一种使用情形是作为一个 target-focussed 设计系统的一部分:

add_library(pic_on INTERFACE)
set_property(TARGET pic_on PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON)
add_library(pic_off INTERFACE)
set_property(TARGET pic_off PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE OFF)

add_library(enable_rtti INTERFACE)
target_compile_options(enable_rtti INTERFACE
  $<$<OR:$<COMPILER_ID:GNU>,$<COMPILER_ID:Clang>>:-rtti>
)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 pic_on enable_rtti)

代码意图很明显,我就不翻译了

接口库也可以被安装或者导出,其引用的内容会被分别安装:

set(Eigen_headers
  src/eigen.h
  src/vector.h
  src/matrix.h
  )
add_library(Eigen INTERFACE ${Eigen_headers})
target_include_directories(Eigen INTERFACE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
  $<INSTALL_INTERFACE:include/Eigen>
)

install(TARGETS Eigen EXPORT eigenExport)
install(EXPORT eigenExport NAMESPACE Upstream::
  DESTINATION lib/cmake/Eigen
)
install(FILES ${Eigen_headers}
  DESTINATION include/Eigen
)

目标库

要使用目标库,请将 CMake 版本限定在 3.12 及以上

目标库只产生编译的目标文件而不执行链接操作,要使用目标库需要使用 生成表达式 将目标库 放到源码位置 。例如:

add_library(foo OBJECT foo1.c foo2.c)

add_library(Combined $<TARGET_OBJECTS:foo>)

和静态库或动态库一样,目标库可以将构建需求通过 target_include_directories 和 target_link_libraries 传递出去

目标库一般来说是不推荐的。但是在将一个动态库项目拆分时很有用,因为这时候静态库存在符号导出的问题

导出库

尽管库的作者可以手动写一个 Find*.cmake 模块,但是请注意: Find.cmake* 是写给那些没有为 CMake 做适配的库的。如果你使用 CMake,那么应该使用 *Config.cmake 而不是前者。
  1. 更改 include_diectories

    target_include_directories(FlowLayout PUBLIC INTERFACE
       $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
       $<INSTALL_INTERFACE:include>
    )
  2. 导出 *Targets.cmake 文件

    install(TARGETS FlowLayout
       DESTINATION lib
       EXPORT FlowLayoutTargets
    )
  3. 导出 *Config.cmake 文件

    1. 添加一个 Config.cmake.in 文件到项目的跟路径下,写入以下内容:

      @PACKAGE_INIT@
      
      include ("${CMAKE_CURRENT_LIST_DIR}/FlowLayoutTargets.cmake")
    2. 生成 *Config.cmake 文件和 *Version.cmake 文件:

      include(CMakePackageConfigHelpers)
      # 根据 Config.cmake.in 生成 FlowLayoutConfig.cmake 文件
      configure_package_config_file(
         "${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in"
         "${CMAKE_CURRENT_BINARY_DIR}/FlowLayoutConfig.cmake"
         INSTALL_DESTINATION "lib/cmake/FlowLayout"
         NO_SET_AND_CHECK_MACRO
         NO_CHECK_REQUIRED_COMPONENTS_MACRO
      )
      # 生成 FlowLayoutConfigVersion.cmake 文件
      write_basic_package_version_file(
         "${CMAKE_CURRENT_BINARY_DIR}/FlowLayoutConfigVersion.cmake"
         VERSION "${FlowLayout_VERSION_MAJOR}.${FlowLayout_VERSION_MINOR}"
         COMPATIBILITY AnyNewerVersion
      )
  4. 安装头文件、库文件和配置文件

    # 安装头文件
    install(FILES flowlayout.h DESTINATION include)
    # 安装 *Targets.cmake 文件
    install(EXPORT FlowLayoutTargets
       FILE FlowLayoutTargets.cmake
       DESTINATION lib/cmake/FlowLayout
    )
    # 安装 配置文件
    install(
          FILES ${CMAKE_CURRENT_BINARY_DIR}/FlowLayoutConfig.cmake
          FILES ${CMAKE_CURRENT_BINARY_DIR}/FlowLayoutConfigVersion.cmake
          DESTINATION lib/cmake/FlowLayout
    )

打包

# 安装必要的库
include(InstallRequiredSystemLibraries)
set(CPACK_PACKAGE_VENDOR Z)
set(CPACK_GENERATOR TGZ)
set(CPACK_PACKAGE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/pack)
include(CPack)
set(CPACK_PACKAGE_FILE_NAME ${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CPACK_SYSTEM_NAME})
变量 CPACK_PACKAGE_FILE_NAME 必须要在 include(CPack) 之后设置

获取项目

CMake 可以使用五种方式来包含项目: Config.cmake, Find.cmake, pkgconfig, FetchContent 和 Git SubModule

其中, Config.cmake 是由支持 CMake 的上游项目提供的,可以直接使用 find_package() 来使用; Find.cmake 是下游写给不支持 CMake 的项目用的,需要使用 include() 使用;pkgconfig 是给 Linux 上带有 *.pc 文件的项目使用的。而 FetchContent 和 Git SubModule 是用来在项目中集成源码的。

FetchContent[1]

FetchContent 的使用步骤为:

include(FetchContent)
FetchContent_Declare() # 声明项目
FetchContent_MakeAvailable() # 下载项目
# 后续包含项目的头文件及库文件

声明有以下几种形式:

FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        release-1.8.0
)

FetchContent_Declare(
  myCompanyIcons
  URL      https://intranet.mycompany.com/assets/iconset_1.12.tar.gz
  URL_HASH 5588a7b18261c20068beabfb4f530b87
)

FetchContent_Declare(
  myCompanyCertificates
  SVN_REPOSITORY svn+ssh://svn.mycompany.com/srv/svn/trunk/certs
  SVN_REVISION   -r12345
)

声明的 CVS 项目会在 FetchContent_MakeAvailable() 的时候被克隆,声明的下载项目会被解压。

根据 ExternaProject 来看,URL 可以声明多个。当前面的 URL 速度太慢时会回退到后面的 URL

Find*.cmake

Find*.cmake 用于不支持 CMake 的项目。其项目命令一般遵守 find_package 的模式:

  • *_INCLUDE_DIRS

  • *_LIBRARIES

pkg-config

对于 Linux 而言, *.pc 往往是普遍提供的文件。CMake 通过 PkgConfig 模块提供与其的交互。该模块仅在类 Unix 环境下可用(比如 Linux 或者 Mingw)。

find_package(PkgConfig REQUIRED)
pkg_search_module(gtkmm-3.0 REQUIRED gtkmm-3.0)

检测到的库后,命名方式为 *_INCLUDE_DIRS*_LIBRARY_DIRS*_LIBRARIES

或者是使用 PkgConfig::* 的方式

平台独立的命令

CMake 提供了一部分跨平台的、经常使用的 命令

静态链接

静态链接需要使用特殊的编译器标志。对于 GCC 而言,有三种种方式:

  • 添加 -static 标志使用全静态链接

  • 使用 -static-libstdc++-static-libgcc 的方式为单个库进行静态链接

  • 使用 -Bstatic 将后面的库进行静态链接

  • 直接指定库文件的绝对路径: -l:/usr/lib/libclangBasic.a

对于直接指定绝对路径的形式,在实践中我发现不加 -l: 也行,但是是不是通用的我不清楚

IMPORTANTL: 尽管 全静态链接 后程序对静态库没需求,但是某些库例如 glibc 对内核版本有要求,因此很可能依然没办法“一次编译,到处部署”

打包

要将一个二进制文件打包成一个可独立执行的包,需要下面几个步骤:

  1. 将需要的动态库拷贝到当前路径下:

    ldd ccls | awk {'print $1'} | xargs -I {} cp -L -n '/usr/lib/{}' .

    部分动态库可能会拷贝失败,需要手动干预。另外,一些自己安装的库可能不再 /usr/lib

  2. 将二进制文件中的绝对路径去除:

    sed -i -e 's#/usr#././#g' ccls
    sed -i -e 's#/lib#././#g' ccls

    这一步的主要原因是有些程序写死了动态库加载路径,需要将其改为当前路径

  3. 根据需要使用 linuxdeployqt 打包成 appimage

交叉编译

交叉编译的命令格式如下:

cmake -B build/Debug -GNinja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_SYSTEM_NAME=Windows -DCMAKE_C_COMPILER=/usr/bin/x86_64-w64-mingw32-gcc -DCMAKE_CXX_COMPILER=/usr/bin/x86_64-w64-mingw32-c++ -DCMAKE_RC_COMPILER=/usr/bin/x86_64-w64-mingw32-windres

其中 CMAKE_SYSTEM_NAME 在 /usr/share/cmake/Modules/Platform 中定义,其余的为指定编译器

创建预编译文件

在编译工程中,可能希望生成预编译文件而不是最终产物。对于 MSVC 而言很简单,只需要在 VS 中右键目标 → 编译成预编译文件即可,对于 Linux 而言有两种方式:

  1. 使用 Makefile 生成器,并使用 make src.i 即可。

    这种方式只支持 Makefile 生成器,CMake 配置后,进入含有 Makefile 文件的目录中,然后执行 make src.i 即可

  2. 使用 -save-temps=obj 编译参数:

    add_compile_options(-save-temps=obj)

禁用外部库中的警告

要禁用外部库中的警告,有两种办法:

  • 对于编译型库而言。在引入库的 CMakeLists 之前添加:

    add_definitions(-w)
  • 对于头文件类型的库而言。在引入头文件时添加 SYSTEM:

    target_include_directories(psutil SYSTEM PRIVATE Boost::algorithm)

    使用这种方式引入的库是使用 -isystem 而不是 -I。编译器会将其视为系统库,因而不会发出警告。

ExternalProject

ExternalProject 用于添加外部的项目。可以作为 add_subdirectory 的替代。但是 ExternalProject 有以下不同:

  • ExternalProject 自动运行 build 和 install 规则。

  • ExternalProject 在删除 CMakeCache 文件后不需要重新构建。

  • ExternalProject 不共享 CMakeCache 中的变量。

  • ExternalProject 添加的项目在构建时才执行构建。因此配置期间的 find_package 不生效。

使用方式如下:

ExternalProject_Add(NamedType
    SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/NamedType"
    CMAKE_ARGS -DENABLE_TEST=OFF -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/contrib
)

实用模块

CMakeDependentOption

这增加了命令 cmake_dependent_option ,它根据另外一组变量是否为真来(决定是否)开启一个选项。

宏原型为:

cmake_dependent_option(<option> "<help_text>" <value> <depends> <force>)

一个使用例子为:

cmake_dependent_option(WITH_TESTS "build with tests" ON
  "cmake_build_type STREQUAL debug" OFF)
Last moify: 2022-12-04 15:11:33
Build time:2025-07-18 09:41:42
Powered By asphinx