CMake语法

关于CMake的基础概念和CMake项目构建流程

一、CMake 基础语法

1. 基础语法规则

  • 命令格式:不区分大小写(惯例用大写),格式为 COMMAND(参数1 参数2 ...),参数含空格需用双引号包裹。
    例:PROJECT(MyApp)project(myapp) 等效。

  • 变量

    • 定义:set(变量名 值),如 set(SRC main.cpp)
    • 引用:${变量名},如 add_executable(app ${SRC})
    • 类型:
      • 普通变量:当前作用域有效(函数/目录内)。
      • 缓存变量:set(变量名 值 CACHE 类型 "描述"),存入 CMakeCache.txt 跨构建保留(如 CMAKE_BUILD_TYPE)。
      • 环境变量:$ENV{变量名} 引用,set(ENV{变量名} 新值) 修改。
    • 预定义核心变量:
      变量名 含义
      PROJECT_SOURCE_DIR 项目根目录(根 CMakeLists.txt 路径)
      CMAKE_BINARY_DIR 构建目录(执行 cmake 命令的目录)
      CMAKE_CXX_STANDARD C++ 标准版本(如 17、20)
  • 列表(List):空格分隔的字符串集合,如 set(LIBS math utils)
    常用操作:list(APPEND 列表 元素)(追加)、list(LENGTH 列表 变量)(长度)。

  • 注释:单行用 #,无多行注释(需每行加 #)。

二、核心构建命令

1. 项目配置基础

  • cmake_minimum_required(VERSION 版本):指定最低 CMake 版本(必须在 project 前),如 cmake_minimum_required(VERSION 3.10)

  • project(项目名 [LANGUAGES C CXX ...]):定义项目名称及支持语言(默认 C/C++),自动生成 PROJECT_NAMEPROJECT_VERSION 等变量。

2. 目标(Target)构建

目标是 CMake 核心,代表可执行文件、库等产物。

  • 可执行文件add_executable(目标名 源文件...)
    例:add_executable(app main.cpp src/func.cpp)

  • 库文件add_library(目标名 [STATIC|SHARED|MODULE] 源文件...)

    • STATIC:静态库(.a/.lib,编译时链接)。
    • SHARED:动态库(.so/.dll,运行时加载)。
    • MODULE:模块库(非链接用,运行时动态加载)。
      例:add_library(math STATIC src/math.cpp)

3. 目标属性与依赖配置

现代 CMake(3.0+)推荐"目标导向"配置,替代全局命令(如 include_directories),明确作用域:

  • target_include_directories(目标 作用域 目录...):指定头文件目录

    • PUBLIC:当前目标及依赖它的目标可见。
    • PRIVATE:仅当前目标可见。
    • INTERFACE:仅依赖它的目标可见。
      例:target_include_directories(app PUBLIC include)
  • target_link_libraries(目标 依赖...):链接库(自定义库或系统库)
    例:target_link_libraries(app PUBLIC math pthread)(链接自定义库 math 和系统线程库)。

  • target_compile_features(目标 作用域 cxx_std_17):指定 C++ 标准(自动配置编译选项)。

  • target_compile_options(目标 作用域 -Wall -O2):添加编译选项(警告、优化等)。

三、项目组织与模块化(多目录工程)

1. 典型目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
project_root/
├── CMakeLists.txt # 根配置
├── include/ # 全局头文件
├── src/ # 源码目录
│ ├── math/ # 子模块(数学功能)
│ │ ├── CMakeLists.txt
│ │ ├── math.cpp
│ │ └── math.h
│ └── utils/ # 子模块(工具功能)
│ ├── CMakeLists.txt
│ └── utils.cpp
└── apps/ # 可执行程序
├── main.cpp
└── CMakeLists.txt

2. 父子目录协作

  • 根目录 CMakeLists.txt:定义全局配置,通过 add_subdirectory 加载子目录。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    cmake_minimum_required(VERSION 3.15)
    project(MyProject LANGUAGES CXX)

    # 全局路径变量
    set(PROJECT_ROOT ${CMAKE_CURRENT_SOURCE_DIR})

    # 加载子目录(自动解析子目录的 CMakeLists.txt)
    add_subdirectory(src/math)
    add_subdirectory(src/utils)
    add_subdirectory(apps)
  • 子目录 src/math/CMakeLists.txt:定义模块库,暴露头文件路径。

    1
    2
    3
    add_library(math STATIC math.cpp)
    # 头文件对依赖该库的目标可见(PUBLIC)
    target_include_directories(math PUBLIC ${PROJECT_ROOT}/include)
  • 应用目录 apps/CMakeLists.txt:链接子模块库。

    1
    2
    add_executable(app main.cpp)
    target_link_libraries(app PUBLIC math utils) # 依赖 math 和 utils 库

3. 关键技巧

  • 输出路径控制:通过 set_target_properties 统一输出目录,避免污染源码。

    1
    2
    set_target_properties(app PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_ROOT}/bin)
    set_target_properties(math PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_ROOT}/lib)
  • 变量作用域:子目录变量默认局部,修改父目录变量需加 PARENT_SCOPE

    1
    set(PARENT_VAR "value" PARENT_SCOPE)  # 子目录修改父目录变量

四、流程控制(条件与循环)

1. 条件判断(if-else-endif

根据平台、配置等动态调整构建规则:

1
2
3
4
5
6
7
if(条件)
# 满足条件执行
elseif(另一个条件)
# 满足另一条件执行
else()
# 默认执行
endif()

常用条件

  • 平台:if(WIN32)if(UNIX)if(APPLE)(macOS)。

  • 变量:if(DEFINED VAR)(是否定义)、if(VAR STREQUAL "value")(字符串相等)。

  • 文件:if(EXISTS 路径)(路径存在)、if(IS_DIRECTORY 路径)(是否目录)。

示例:平台差异化编译

1
2
3
4
5
if(WIN32)
target_compile_definitions(app PRIVATE _WIN32) # Windows 宏定义
elseif(UNIX)
target_link_libraries(app pthread) # Linux 链接线程库
endif()

2. 循环(foreachwhile

  • foreach:遍历列表或范围

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 遍历列表
    set(SRCS a.cpp b.cpp c.cpp)
    foreach(src IN LISTS SRCS)
    message(STATUS "Source: ${src}")
    endforeach()

    # 遍历数值范围(0到10,步长2)
    foreach(i RANGE 0 10 2)
    message(STATUS "Index: ${i}")
    endforeach()
  • while:条件循环(适合动态计数场景)

    1
    2
    3
    4
    5
    set(COUNT 0)
    while(COUNT LESS 5)
    message(STATUS "Count: ${COUNT}")
    math(EXPR COUNT "${COUNT} + 1") # 计数+1
    endwhile()

五、依赖管理与外部库

1. 查找系统库(find_package

用于引入外部库(如 Boost、OpenCV):

1
find_package(库名 [版本] [REQUIRED])  # REQUIRED:找不到则报错

找到库后

  • 现代库会生成目标 库名::库名,直接链接:

    1
    2
    find_package(OpenCV 4 REQUIRED)
    target_link_libraries(app PUBLIC OpenCV::OpenCV) # 自动包含头文件和链接库
  • 传统库需手动处理变量:

    1
    2
    3
    find_package(Boost 1.70 REQUIRED COMPONENTS filesystem)
    target_include_directories(app PUBLIC ${Boost_INCLUDE_DIRS})
    target_link_libraries(app PUBLIC ${Boost_LIBRARIES})

2. 自定义模块

若外部库无默认查找模块,可自定义 FindXXX.cmake 并放入模块目录:

1
2
3
# 添加自定义模块目录
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_ROOT}/cmake")
find_package(MyLib REQUIRED) # 查找 cmake/FindMyLib.cmake

六、安装与打包

1. 安装规则(install

定义目标、文件的安装路径(默认前缀:Unix 为 /usr/local,Windows 为 C:/Program Files):

1
2
3
4
5
6
7
8
# 安装可执行文件到 ${CMAKE_INSTALL_PREFIX}/bin
install(TARGETS app DESTINATION bin)

# 安装静态库到 ${CMAKE_INSTALL_PREFIX}/lib
install(TARGETS math DESTINATION lib)

# 安装头文件到 ${CMAKE_INSTALL_PREFIX}/include
install(FILES include/math.h DESTINATION include)

修改安装前缀

1
cmake .. -DCMAKE_INSTALL_PREFIX=/path/to/install  # 生成时指定

2. 生成安装包(CPack)

在根 CMakeLists.txt 末尾添加:

1
2
3
4
5
include(CPack)
# 配置包信息
set(CPACK_PACKAGE_NAME "MyProject")
set(CPACK_PACKAGE_VERSION "1.0")
set(CPACK_GENERATOR "TGZ;DEB") # 生成 .tar.gz 和 .deb 包

生成包

1
cd build && cpack  # 在构建目录执行

七、最佳实践与避坑指南

  1. 优先目标导向命令:用 target_include_directories 替代 include_directories,用 target_link_libraries 替代 link_libraries,明确依赖范围。

  2. 分离构建目录:采用"out-of-source"构建,避免污染源码:

    1
    mkdir build && cd build && cmake ..  # 构建目录与源码分离
  3. 调试变量:用 message(STATUS "VAR: ${VAR}") 打印变量值,排查配置问题。

  4. 跨平台路径:用 PATH_EQUAL 比较路径(自动处理 /\ 差异):

    1
    if("/a/b" PATH_EQUAL "/a//b")  # 结果为真
  5. 动态库查找

    • Linux:设置 LD_LIBRARY_PATHCMAKE_INSTALL_RPATH
    • Windows:将 .dll 复制到可执行文件目录或添加到 PATH

八、完整构建流程

  1. 编写 CMakeLists.txt:按模块拆分,定义目标与依赖。

  2. 生成构建文件

    1
    2
    mkdir build && cd build
    cmake .. -DCMAKE_BUILD_TYPE=Release # Release 模式(默认 Debug)
  3. 编译

    1
    2
    cmake --build . -j8  # 8线程编译(通用)
    # 或针对 Makefile:make -j8
  4. 安装(可选)

    1
    cmake --install .  # 安装到指定前缀