CMake 核心
对于一个构建工具而言,主要负责的就是构建、测试和打包。这其中的痛点不是对自己代码的管理,而是对外部代码的管理。依赖的跨平台问题更是让人头痛无比,在我看来,CMake 的核心问题就是 依赖管理 。CMake 应当围绕依赖建立项目。
本文尝试以最简要和方式阐述 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 文件中使用
重要
${变量} 将会导致变量在原地进行展开,由于路径代表的变量展开后可能含有空格,因此 取路径变量的值时应当总是使用双引号括起来
条件语句会先于变量展开,因此条件语句中的变量无需取值即可使用。自 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 以改善补全效果
基于目标的构建
目标 和 生成表达式 是 CMake 最核心的概念和最有效的工具,通过这两者可以方便地描述一个工程的生成方式。
目标由三部分构成: target_include_directories 、 target_link_libraries 、target_compile_options 、target_compile_features
目标别名
http://127.0.0.1:46221/cmake/cmake.org/cmake/help/v3.20/manual/cmake-buildsystem.7.html#id36
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)
如果指定了 INTERFACE 和 PUBLIC ,则后续的头文件被视为 导出头文件 :
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_link_libraries
target_compile_options
target_compile_features
导出库
重要
尽管库的作者可以手动写一个 Find*.cmake 模块,但是请注意: Find*.cmake 是写给那些没有为 CMake 做适配的库的。如果你使用 CMake,那么应该使用 *Config.cmake 而不是前者。
更改 include_diectories
target_include_directories(FlowLayout PUBLIC INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> $<INSTALL_INTERFACE:include> )
导出 *Targets.cmake 文件
install(TARGETS FlowLayout DESTINATION lib EXPORT FlowLayoutTargets )
导出 *Config.cmake 文件
添加一个 Config.cmake.in 文件到项目的跟路径下,写入以下内容:
@PACKAGE_INIT@ include ("${CMAKE_CURRENT_LIST_DIR}/FlowLayoutTargets.cmake")生成 *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 )
安装头文件、库文件和配置文件
# 安装头文件 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 )
备注
库文件在导出 *.Target.cmake 时就已经指定了安装路径
在使用 install 指令时,不要指定绝对路径(尤其是需要带特权的路径),否则生成的二进制压缩包内不会包含二进制。如果你发现你执行 cpack 指令需要特权时,就很可能是写了绝对路径
打包
# 安装必要的库
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 2
FetchContent 的使用步骤为:
声明有以下几种形式:
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 提供了一部分跨平台的、经常使用的 命令