本文尝试以最简要和方式阐述 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_directories 、 target_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)
如果指定了 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
构建需求和使用需求
target_include_directories(), target_compile_definitions() 和 target_compile_options() 三个命令指明了代码的构建需求和使用需求。其中构建需求是通过更改 target 的 INCLUDE_DIRECTORIES, COMPILE_DEFINITIONS 和 COMPILE_OPTIONS 属性,而使用需求是通过更改 target 的 INTERFACE_INCLUDE_DIRECTORIES, INTERFACE_COMPILE_DEFINITIONS 和 INTERFACE_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 而不是前者。 |
更改 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[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 对内核版本有要求,因此很可能依然没办法“一次编译,到处部署”
打包
要将一个二进制文件打包成一个可独立执行的包,需要下面几个步骤:
将需要的动态库拷贝到当前路径下:
ldd ccls | awk {'print $1'} | xargs -I {} cp -L -n '/usr/lib/{}' .
部分动态库可能会拷贝失败,需要手动干预。另外,一些自己安装的库可能不再
/usr/lib
将二进制文件中的绝对路径去除:
sed -i -e 's#/usr#././#g' ccls sed -i -e 's#/lib#././#g' ccls
这一步的主要原因是有些程序写死了动态库加载路径,需要将其改为当前路径
根据需要使用 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 而言有两种方式:
使用 Makefile 生成器,并使用
make src.i
即可。这种方式只支持 Makefile 生成器,CMake 配置后,进入含有 Makefile 文件的目录中,然后执行
make src.i
即可使用
-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)