CMake初探篇2-语法与准则

CMake初探篇一


千年暗室,一灯即明.

33

1. CMake 行为准则(Do's and Don'ts)

接下来的两个列表很大程度上基于优秀的 gist Effective Modern CMake. 那个列表更长且更详细,也非常欢迎你去仔细阅读它。

  • 不要使用具有全局作用域的函数:这包含 link_directoriesinclude_libraries 等相似的函数。使用target_link_directoriesstarget_include_libraries等具有target的限定。

  • 不要添加非必要的 PUBLIC 要求:你应该避免把一些不必要的东西强加给用户(-Wall)。相比于 PUBLIC,更应该把他们声明为 PRIVATE

  • 将库直接链接到需要构建的目标上:如果可以的话,总是显式地将库链接到目标上。

  • 当链接库文件时,不要省略 PUBLIC 或 PRIVATE 关键字:这将会导致后续所有的链接都是缺省的。

  • 把 CMake 程序视作代码:它是代码。它应该和其他的代码一样,是整洁并且可读的。

  • 建立目标的观念:你的目标应该代表一系列的概念。为任何需要保持一致的东西指定一个 (导入型)INTERFACE 目标,然后每次都链接到该目标。

  • 导出你的接口:你的 CMake 项目应该可以直接构建或者安装。

  • 为库书写一个 Config.cmake 文件:这是库作者为支持客户的体验而应该做的。

  • 声明一个 ALIAS 目标以保持使用的一致性:使用 add_subdirectoryfind_package 应该提供相同的目标和命名空间。

  • 将常见的功能合并到有详细文档的函数或宏中:函数往往是更好的选择。

  • 使用小写的函数名: CMake 的函数和宏的名字可以定义为大写或小写,但是一般都使用小写,变量名用大写。

  • 使用 cmake_policy 和/或 限定版本号范围: 每次改变版本特性 (policy) 都要有据可依。应该只有不得不使用旧特性时才降低特性 (policy) 版本。

2. 简单案例与CMake语法分析

下面是最简单生成一个可执行程序的CMakeLists.txt的构成,包含指定CMake指定运行最低版本,项目名称和可执行程序的生成。

cmake_minimum_required(VERSION 3.12)
project(HakuApp)
add_executable(HakuApp maim.cpp)

这是每个 CMakeLists.txt 都必须包含的第一行

cmake_minimum_required(VERSION 3.12)

顺便提一下关于 CMake 的语法。命令 cmake_minimum_required 是不区分大小写的,所以常用的做法是使用小写和下划线的连接来表示函数,使用大写表示变量。

project(xxx) 语法

image-20251206084110071

从CMake给出project的定义可知,该函数作用是给项目设置个名字。项目的名称被保存到PROJECT_NAME

变量中,如果当前构建的顶层CMakeLists.txt,名字也会保存到CMAKE_PROJECT_NAME中。

语法为:<>表示必须要填入的参数,[]表示可以选填的参数,[<>]表示,如果进行了选填,那就必须完成<>中指定的内容。

换句话说,必须指定<PROJECT_NAME>字段,其他均可以不用填写。如果选填了那么必须要填写主版本<major>。如果使用了次版本.,那就必须填写<minor>

project(HakuApp
    VERSION     1.0.0
    LANGUAGES   CXX
    DESCRIPTION "a simple test dll"
    HOMEPAGE_URL ""
)
    
# 输出所有项目相关变量, 上面填的与下面的一一对应可以输出。
message("=== 项目信息 ===   ")
message("项目名称: ${PROJECT_NAME}")
message("完整版本: ${PROJECT_VERSION}")
message("主版本: ${PROJECT_VERSION_MAJOR}")
message("次版本: ${PROJECT_VERSION_MINOR}")
message("补丁版本: ${PROJECT_VERSION_PATCH}")
message("微调版本: ${PROJECT_VERSION_TWEAK}")
message("描述: ${PROJECT_DESCRIPTION}")
message("主页: ${PROJECT_HOMEPAGE_URL}")

生成一个可执行文件

add_executable(HakuApp maim.cpp)

HakuApp 既是生成的可执行文件的名称,也是创建的 CMake 目标(target)的名称(我保证,你很快会听到更多关于目标的内容)。紧接着的是源文件的列表,你想列多少个都可以。CMake 很聪明 ,它根据拓展名只编译源文件。在大多数情况下,头文件将会被忽略;列出他们的唯一原因是为了让他们在 IDE 中被展示出来,目标文件在许多 IDE 中被显示为文件夹

有了上面的三行语法,我们可以生成一个简单的可执行程序了。

3. 变量与缓存

使用下面命令可以查看具体的函数用法

cmake --help-command set

3.1 本地变量

我们首先讨论变量。你可以这样声明一个本地 ( local ) 变量:

set( ... [PARENT_SCOPE])

set(MY_VARIABLE "value")
set(MY_LIST "one" "two")

变量名通常全部用大写,变量值跟在其后。你可以通过 ${} 来解析一个变量,例如 ${MY_VARIABLE}。CMake 有作用域的概念,在声明一个变量后,你只可以在它的作用域内访问这个变量。如果你将一个函数或一个文件放到一个子目录中,这个变量将不再被定义。你可以通过在变量声明末尾添加 PARENT_SCOPE 来将它的作用域置定为当前的上一级作用域。这个一般用于函数定义中的返回值。

3.2 缓存变量

缓存变量(Cache Variable)存储在 CMakeCache.txt 文件中,在多次配置之间保持持久性。在子目录中仍可以访问缓存变量。

set( ... CACHE [FORCE])

: BOOL, FILEPATH, PATH, STRING, INTERNAL

# 方式1:set() 命令带 CACHE 选项
set(VARIABLE_NAME value CACHE STRING "Description")

# 方式2:option() 命令(布尔型缓存变量)
option(<variable> "<help_text>" [initial_value])
option(OPTION_NAME "Description" defaultValue)

我们可以通过下面命令,查看当前工程中所有的或者特定的缓存变量,也可以在CMakeCache.txt找到。

# 查看所有缓存变量
cmake -B build -L

# 查看 CMAKE_ 前缀的变量, window下
cmake -B build -L | findstr CMAKE_

# 查看特定前缀的变量, Linux下
cmake -B build -L | grep CMAKE_

3.3 环境变量

你也可以通过 set(ENV{variable_name} value)$ENV{variable_name} 来设置和获取环境变量,不过一般来说,我们最好避免这么用。

4. 实际案例(库 + 可执行程序)

aux_source_directory 命令可以用来获取指定目录下的所有源文件列表

aux_source_directory(<dir> <variable>)

# 常见用法
# 将当前目录下的所有源文件(如 .cpp, .c)添加到变量 SRC_LIST 中
aux_source_directory(. SRC_LIST)

# 或者,指定一个子目录,如 `src`
aux_source_directory(src SRC_LIST)

# 然后使用这个文件列表来创建可执行文件或库
add_executable(my_app ${SRC_LIST})

aux_source_directory 命令主要用于收集 源文件,如 .cpp.c 文件。它不会自动包含头文件(如 .h, .hpp)。

file(GLOB),如果项目结构包含子目录或者需要更灵活地指定文件类型(例如同时收集头文件),可以使用 file 命令配合 GLOBGLOB_RECURSE 选项

# 收集当前目录下所有的 .cpp 文件
file(GLOB SRC_LIST "*.cpp")

# 递归收集当前目录及所有子目录下的 .cpp 和 .h 文件
file(GLOB_RECURSE ALL_FILES "*.cpp" "*.h")
特性 aux_source_directory file(GLOB/GLOB_RECURSE)
主要用途 收集指定目录下的源文件 通过通配符模式匹配文件
文件类型 主要针对.c, .cpp等源文件 可指定任意扩展名(如*.cpp, *.h
递归搜索 不支持,仅搜索指定目录本身 GLOB_RECURSE 选项支持递归搜索子目录
灵活性 较低,自动识别源文件 高,可自定义匹配模式
潜在问题 可能遗漏在头文件中实现的代码 可能包含编译生成的临时文件(需注意构建目录分离)

实际场景,现在我们有一个库和一个可执行程序,目录结构如下:

window下, tree /F
D:.
│   CMakeLists.txt
│   main.cpp
└───TestLib1
        CMakeLists.txt
        testClassA.cpp
        testClassA.h

4.1 简单可执行程序调用

CMakeLists.txt部分

# CMakeLists.txt
# CMake 最低版本号要求
cmake_minimum_required (VERSION 3.5)

# 项目信息
project (DemoHakuon)

# 添加 math 子目录
add_executable(lesson1 main.cpp)

# 添加子模块, TestLib1库
add_subdirectory(TestLib1)

# 可执行程序包含库部分.
target_link_libraries(lesson1 PRIVATE TestLib1)

## ------------ 库部分 TestLib1/CMakeLists.txt
# CMake 最低版本号要求
cmake_minimum_required (VERSION 3.5)
add_library(TestLib1 testClassA.cpp)

具体实现部分可任意编写,这个是简单的库调用,不包含库的导出和导入操作。

4.2 库的安装和导出

下方为一个通用的安装库的函数,许需要传入目标的名称即可完成库的安装操作。

# 安装库
function(install_project_library TARGET_NAME)
    if(NOT TARGET ${TARGET_NAME})
        message(FATAL_ERROR "Target '${TARGET_NAME}' not found. Check your target name.")
    endif()

    include(GNUInstallDirs)
    include(CMakePackageConfigHelpers)

    # 安装目标文件
    install(TARGETS ${TARGET_NAME}
        EXPORT ${TARGET_NAME}Targets
        PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${TARGET_NAME}
        RUNTIME       DESTINATION ${CMAKE_INSTALL_BINDIR}
        LIBRARY       DESTINATION ${CMAKE_INSTALL_LIBDIR}
        ARCHIVE       DESTINATION ${CMAKE_INSTALL_LIBDIR}
    )

    # 导出目标信息
    install(EXPORT ${TARGET_NAME}Targets
        FILE ${TARGET_NAME}Targets.cmake
        NAMESPACE ${TARGET_NAME}::   
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${TARGET_NAME}
    )

    # 生成配置文件
    configure_package_config_file(
        "${CMAKE_CURRENT_SOURCE_DIR}/${TARGET_NAME}Config.cmake.in"
        "${CMAKE_CURRENT_BINARY_DIR}/${TARGET_NAME}Config.cmake"
        INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${TARGET_NAME}
    )

    write_basic_package_version_file(
        "${CMAKE_CURRENT_BINARY_DIR}/${TARGET_NAME}ConfigVersion.cmake"
        VERSION ${PROJECT_VERSION}
        COMPATIBILITY SameMajorVersion
    )

    # 安装 config 文件
    install(FILES
        "${CMAKE_CURRENT_BINARY_DIR}/${TARGET_NAME}Config.cmake"
        "${CMAKE_CURRENT_BINARY_DIR}/${TARGET_NAME}ConfigVersion.cmake"
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${TARGET_NAME}
    )
endfunction()

4.3 打包

简单的打包指令如下面:

# 最小安装配置
install(TARGETS lesson1 RUNTIME DESTINATION bin)

# 最小打包配置
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_GENERATOR "ZIP")  # 只生成ZIP包
include(CPack)

参考链接

Modern-Cmake

posted on 2025-12-06 11:45  Hakuon  阅读(22)  评论(0)    收藏  举报