######################################## CMake 核心 ######################################## 对于一个构建工具而言,主要负责的就是构建、测试和打包。这其中的痛点不是对自己代码的管理,而是对外部代码的管理。依赖的跨平台问题更是让人头痛无比,在我看来,CMake 的核心问题就是 ``依赖管理`` 。CMake 应当围绕依赖建立项目。 本文尝试以最简要和方式阐述 CMake 的核心观念和核心问题。 TODO and NOT TODO **************************************** - 不要使用 `PROJECT_SOURCE_DIR` 以支持 `Subproject `_ - 不要使用 **CMAKE_SYSTEM** 检测系统,而是使用 **CMAKE_HOST_SYSTEM_NAME** 以支持交叉编译 声明式语言 **************************************** CMake 是一个声明式的语言,而不是一个命令式的编程语言。所谓声明式编程,就是告诉计算机“做什么”而不是“怎么做”。另外一个与传统声明式语言不同的:CMake 中的 **所有变量类型都是字符串类型** 。尽管 CMake 有布尔值的概念,但是其可以看作是“具有布尔含义的字符串” 。 以 string 的 REPLACE 为例,让我们看看它的 API: .. code-block:: cmake 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 文件中使用 .. important:: ${变量} 将会导致变量在原地进行展开,由于路径代表的变量展开后可能含有空格,因此 **取路径变量的值时应当总是使用双引号括起来** 条件语句会先于变量展开,因此条件语句中的变量无需取值即可使用。自 3.1+ if 条件语句也支持取值手法。 [#]_ 命令和子命令 **************************************** 命令大小写随意,子命令必须大写。一般约定命令使用全小写 presets **************************************** .. seealso:: - https://cmake.org/cmake/help/git-stage/manual/cmake-presets.7.html - https://cmake.org/cmake/help/git-stage/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json 例如: .. code-block:: json { "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 以改善补全效果 基于目标的构建 **************************************** :abbr:`目标 (Target)` 和 :abbr:`生成表达式 (Generator Expressions)` 是 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_ 用于导入或导出头文件。 .. code-block:: cmake target_include_directories(目标 [SYSTEM] [AFTER|BEFORE] [ [itemss]] ) - 如果不指定 *INTERFACE* 而只指定 *PUBLIC/PRIVATE* ,则后续列出的头文件被视为 **导入头文件** 。 .. code-block:: cmake target_include_directories(main PRIVATE fmt::fmt) - 如果指定了 *INTERFACE* 和 *PUBLIC* ,则后续的头文件被视为 **导出头文件** : .. code-block:: cmake target_include_directories(main INTERFACE PUBLIC ./include) 导入的头文件被填充到 **INCLUDE_DIRECTORIES** 属性,而到处的头文件被导出到 **INTERFACE_INCLUDE_DIRECTORIES** 属性。 一般来说,导出的头文件在被包含和被安装时一般不同,这时可以使用 :doc:`生成表达式` 来描述: .. code-block:: cmake target_include_directories(mylib PUBLIC $ $ # /include/mylib ) target_link_libraries ======================================== target_compile_options ======================================== target_compile_features ======================================== 导出库 **************************************** .. important:: 尽管库的作者可以手动写一个 *Find\*.cmake* 模块,但是请注意: **Find\*.cmake** 是写给那些没有为 CMake 做适配的库的。如果你使用 CMake,那么应该使用 *\*Config.cmake* 而不是前者。 #. 更改 *include_diectories* .. code-block:: cmake target_include_directories(FlowLayout PUBLIC INTERFACE $ $ ) #. 导出 *\*Targets.cmake* 文件 .. code-block:: cmake install(TARGETS FlowLayout DESTINATION lib EXPORT FlowLayoutTargets ) #. 导出 *\*Config.cmake* 文件 #. 添加一个 *Config.cmake.in* 文件到项目的跟路径下,写入以下内容: .. code-block:: none @PACKAGE_INIT@ include ("${CMAKE_CURRENT_LIST_DIR}/FlowLayoutTargets.cmake") #. 生成 *\*Config.cmake* 文件和 *\*Version.cmake* 文件: .. code-block:: 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 ) #. 安装头文件、库文件和配置文件 .. code-block:: cmake # 安装头文件 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 ) .. note:: - 库文件在导出 \*.Target.cmake 时就已经指定了安装路径 - 在使用 install 指令时,不要指定绝对路径(尤其是需要带特权的路径),否则生成的二进制压缩包内不会包含二进制。如果你发现你执行 cpack 指令需要特权时,就很可能是写了绝对路径 .. seealso:: - `CMake官方文档翻译(1) CMake教程 Step by Step `_ - `Exporting and Installing · Modern CMake `_ - `cmake的install指令 `_ 打包 **************************************** .. code-block:: cmake # 安装必要的库 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}) .. important:: 变量 **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 [#]_ ======================================== FetchContent 的使用步骤为: .. code-block::cmake include(FetchContent) FetchContent_Declare() # 声明项目 FetchContent_MakeAvailable() # 下载项目 # 后续包含项目的头文件及库文件 声明有以下几种形式: .. code-block::cmake 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()* 的时候被克隆,声明的下载项目会被解压。 .. note:: 根据 ExternaProject 来看,URL 可以声明多个。当前面的 URL 速度太慢时会回退到后面的 URL Find\*.cmake ======================================== Find\*.cmake 用于不支持 CMake 的项目。其项目命令一般遵守 find_package 的模式: - *\*_INCLUDE_DIRS* - *\*_LIBRARIES* pkg-config ======================================== 对于 Linux 而言, *\*.pc* 往往是普遍提供的文件。CMake 通过 *PkgConfig* 模块提供与其的交互。该模块仅在类 Unix 环境下可用(比如 Linux 或者 Mingw)。 .. code-block:: find_package(PkgConfig REQUIRED) pkg_search_module(gtkmm-3.0 REQUIRED gtkmm-3.0) 检测到的库后,命名方式为 *\*_INCLUDE_DIRS* 、 *\*_LIBRARY_DIRS* 和 *\*_LIBRARIES* 或者是使用 *PkgConfig::\** 的方式 .. seealso:: - `ExternalProject `_ 平台独立的命令 **************************************** CMake 提供了一部分跨平台的、经常使用的 `命令 `_ .. [#] `Modern CMake#Control flow `_ .. [#] `CMake - FetchContent `_