跳到主要内容

CMake 基础语法

本章将介绍 CMakeLists.txt 的基本语法和常用命令,帮助你理解 CMake 构建脚本的核心概念。CMake 是一个跨平台的构建系统生成器,它使用配置文件(CMakeLists.txt)生成标准的构建文件(如 Unix 的 Makefile 或 Visual Studio 的项目文件)。

CMake 语言基础

注释

CMake 支持单行注释和多行注释:

# 这是单行注释
# 注释内容会被 CMake 忽略,用于解释代码逻辑

#[[
这是
多行
注释
]]

在实际项目中,建议使用注释来说明:

  • 每个目标的用途
  • 依赖库的选择原因
  • 条件编译的逻辑

命令调用

CMake 命令不区分大小写,但社区约定使用小写形式:

# 推荐写法(小写)
project(MyProject)

# 也可以(但不推荐)
PROJECT(MyProject)
Project(MyProject)

命令的基本格式是:command(arg1 arg2 arg3),参数之间用空格分隔,整个调用用括号包围。

字符串

CMake 中的字符串是基本数据类型,有多种定义方式:

# 双引号字符串(支持变量替换)
set(name "World")
set(message "Hello, ${name}!")

# 引号内的特殊字符需要转义
set(path "C:\\Users\\Admin")

# 原始字符串(不进行转义)
set(raw_path [[C:\Users\Admin]])

# 带方括号的原始字符串(支持多行)
set(multi_line [[
第一行
第二行
第三行
]])

字符串操作常用命令:

# 字符串连接
set(result "${str1}${str2}")

# 字符串长度
string(LENGTH "${str}" length)

# 子字符串
string(SUBSTRING "${str}" 0 5 substr)

# 查找和替换
string(FIND "${str}" "pattern" index)
string(REPLACE "old" "new" result "${str}")

# 大小写转换
string(TOLOWER "${str}" lower)
string(TOUPPER "${str}" upper)

列表

列表是 CMake 中最常用的数据结构,本质上是用分号分隔的字符串:

# 使用分号分隔的字符串
set(SOURCES "a.cpp;b.cpp;c.cpp")

# 使用空格分隔的多个参数(CMake 会自动转换为分号分隔)
set(SOURCES a.cpp b.cpp c.cpp)

# 多行列表(推荐写法,便于维护)
set(SOURCES
src/main.cpp
src/utils.cpp
src/config.cpp
)

# 追加元素
list(APPEND SOURCES src/new.cpp)

# 列表操作
list(LENGTH SOURCES count) # 获取长度
list(GET SOURCES 0 first) # 获取第一个元素
list(REMOVE_ITEM SOURCES old) # 移除元素
list(INSERT SOURCES 0 new.cpp) # 在指定位置插入
list(REVERSE SOURCES) # 反转列表
list(SORT SOURCES) # 排序

项目配置

cmake_minimum_required

指定项目需要的最低 CMake 版本,建议放在 CMakeLists.txt 的最开头:

cmake_minimum_required(VERSION 3.15)

# 指定版本范围(CMake 3.12+)
cmake_minimum_required(VERSION 3.15...3.28)

# 设置策略版本
cmake_minimum_required(VERSION 3.15 FATAL_ERROR)

为什么需要指定版本?因为不同版本的 CMake 有不同的特性和策略行为。如果你使用了新版本的特性,需要明确要求最低版本,否则在旧版本上构建会失败。

project

定义项目名称和属性:

# 基本项目定义
project(MyProject)

# 完整项目定义
project(MyProject
VERSION 1.0.0
DESCRIPTION "A sample project"
LANGUAGES CXX C
)

# project 命令会自动设置以下变量:
# PROJECT_NAME = MyProject
# PROJECT_VERSION = 1.0.0
# PROJECT_VERSION_MAJOR = 1
# PROJECT_VERSION_MINOR = 0
# PROJECT_VERSION_PATCH = 0
# PROJECT_SOURCE_DIR = 项目根目录
# PROJECT_BINARY_DIR = 构建目录

常用的语言选项:

  • C:C 语言
  • CXX:C++ 语言
  • CUDA:CUDA 语言
  • NONE:不启用任何语言

构建目标

add_executable

创建可执行文件目标:

# 基本语法
add_executable(target_name source1.cpp source2.cpp ...)

# 示例
add_executable(myapp
src/main.cpp
src/utils.cpp
)

# 从列表创建
set(SOURCES src/main.cpp src/utils.cpp)
add_executable(myapp ${SOURCES})

# WIN32 应用程序(Windows 上不显示控制台窗口)
if(WIN32)
add_executable(myapp WIN32 ${SOURCES})
endif()

add_library

创建库目标,库分为多种类型:

# 静态库(.a / .lib)
add_library(mylib STATIC
src/lib.cpp
src/helper.cpp
)

# 动态库(.so / .dll)
add_library(mylib SHARED
src/lib.cpp
src/helper.cpp
)

# 模块库(用于运行时动态加载)
add_library(mylib MODULE
src/plugin.cpp
)

# 对象库(编译但不归档,用于在多个目标间共享编译结果)
add_library(mylib OBJECT
src/common.cpp
)

# 接口库(仅头文件,不编译源文件)
add_library(mylib INTERFACE)

# 为接口库设置属性
target_include_directories(mylib INTERFACE include/)

选择库类型的考虑:

  • 静态库:会被链接到可执行文件中,部署简单但体积大
  • 动态库:运行时加载,多个程序可以共享,节省磁盘和内存
  • 接口库:适合纯头文件库或仅用于组织编译选项

将库链接到目标:

# 基本链接
target_link_libraries(myapp
mylib # 项目内的库
pthread # 系统库
${OpenCV_LIBS} # 外部库
)

# 使用可见性关键字控制依赖传播
target_link_libraries(myapp
PUBLIC public_lib # 传播给依赖 myapp 的目标
PRIVATE private_lib # 仅当前目标使用
INTERFACE interface_lib # 仅对依赖者可见
)

可见性说明

可见性当前目标依赖者
PUBLIC可用可用
PRIVATE可用不可用
INTERFACE不可用可用

理解可见性很重要:当你写一个库给别人用时,PRIVATE 的依赖不会泄漏给使用者,而 PUBLIC 的依赖会自动传递。

编译配置

target_include_directories

设置头文件搜索路径:

target_include_directories(myapp
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include # 使用者也能访问
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src # 仅当前目标使用
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/api # 仅使用者访问
)

# 也可以使用生成器表达式
target_include_directories(myapp
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)

target_compile_definitions

添加预处理器定义:

target_compile_definitions(myapp
PRIVATE
DEBUG_MODE # 定义 DEBUG_MODE
VERSION="1.0" # 定义 VERSION="1.0"
PUBLIC
USE_FEATURE_X # 使用者也能看到这个定义
)

这等价于编译器命令行参数 -DDEBUG_MODE -DVERSION="1.0"

target_compile_options

添加编译选项:

target_compile_options(myapp
PRIVATE
-Wall # 所有警告
-Wextra # 额外警告
-O2 # 优化级别 2
)

# 针对不同编译器的选项
target_compile_options(myapp PRIVATE
$<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)

target_compile_features

指定需要的 C++ 标准和特性:

# 要求 C++17
target_compile_features(myapp PUBLIC cxx_std_17)

# 要求特定特性
target_compile_features(myapp PRIVATE
cxx_lambdas
cxx_auto_type
)

使用 compile_features 比直接设置标准更灵活,因为 CMake 会自动推断所需的编译器标志。

set_target_properties

设置目标的各种属性:

set_target_properties(myapp PROPERTIES
OUTPUT_NAME "my_application" # 输出文件名
CXX_STANDARD 17 # C++ 标准
CXX_STANDARD_REQUIRED ON # 强制要求标准
CXX_EXTENSIONS OFF # 禁用编译器扩展
POSITION_INDEPENDENT_CODE ON # 启用 PIC
)

构建类型

设置构建类型

CMake 支持多种构建类型,每种类型有不同的优化和调试配置:

# 默认构建类型(如果没有指定)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()

# 可选值:Debug, Release, RelWithDebInfo, MinSizeRel

构建类型对比

类型优化调试信息用途
Debug完整开发调试
Release完全生产发布
RelWithDebInfo部分完整性能分析
MinSizeRel体积优先最小体积

自定义编译标志

# 全局编译标志
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")

# 目标特定的编译标志(推荐方式)
target_compile_options(myapp PRIVATE
$<$<CONFIG:Debug>:-g -O0>
$<$<CONFIG:Release>:-O3>
)

文件操作

file 命令

file 命令提供了丰富的文件操作功能:

# 读取文件内容
file(READ "config.txt" CONFIG_CONTENT)

# 写入文件
file(WRITE "output.txt" "Hello, World!")

# 追加内容
file(APPEND "log.txt" "New log entry\n")

# 列出文件(GLOB 不递归,GLOB_RECURSE 递归)
file(GLOB SOURCES "src/*.cpp")
file(GLOB_RECURSE SOURCES "src/**/*.cpp")

# 复制文件
file(COPY "data" DESTINATION "${CMAKE_BINARY_DIR}")

# 创建目录
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/output")

# 下载文件
file(DOWNLOAD "https://example.com/file.zip" "file.zip")

# 计算文件哈希
file(MD5 "myfile.txt" hash_value)

注意:使用 GLOB 收集文件时,添加新文件后不会自动重新配置,需要手动重新运行 CMake。

configure_file

配置文件模板,替换其中的变量:

# 创建 config.h.in 模板文件
# 内容:
#cmakedefine VERSION "@PROJECT_VERSION@"
#cmakedefine HAVE_FEATURE_X

# 在 CMakeLists.txt 中配置
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/config.h.in"
"${CMAKE_CURRENT_BINARY_DIR}/config.h"
)

# 可选参数
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/config.h.in"
"${CMAKE_CURRENT_BINARY_DIR}/config.h"
@ONLY # 只替换 @VAR@ 格式的变量
NEWLINE_STYLE UNIX # 使用 Unix 换行符
)

条件判断

if 语句

# 基本条件判断
if(WIN32)
message("Building on Windows")
elseif(UNIX AND NOT APPLE)
message("Building on Linux")
elseif(APPLE)
message("Building on macOS")
else()
message("Unknown platform")
endif()

# 布尔判断
if(ENABLE_FEATURE)
message("Feature enabled")
endif()

# 字符串比较
if(${CMAKE_BUILD_TYPE} STREQUAL "Debug")
add_definitions(-DDEBUG)
endif()

# 版本比较
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.15")
# 使用新版本特性
endif()

# 文件存在判断
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/config.h")
add_definitions(-DHAVE_CONFIG)
endif()

逻辑运算

# AND - 两个条件都满足
if(WIN32 AND MSVC)
message("Windows with MSVC")
endif()

# OR - 任一条件满足
if(UNIX OR APPLE)
message("Unix-like system")
endif()

# NOT - 条件取反
if(NOT DEBUG_MODE)
add_definitions(-DNDEBUG)
endif()

循环

foreach 循环

# 遍历列表
foreach(file IN LISTS SOURCES)
message("Processing: ${file}")
endforeach()

# 遍历数字范围
foreach(i RANGE 0 9)
message("Number: ${i}")
endforeach()

# 遍历多个项目
foreach(arg IN ITEMS a b c)
message("Item: ${arg}")
endforeach()

# 同时遍历多个列表
foreach(file dep IN ZIP_LISTS SOURCES DEPENDS)
message("Source: ${file}, Depends: ${dep}")
endforeach()

while 循环

set(i 0)
while(i LESS 10)
message("i = ${i}")
math(EXPR i "${i} + 1")
endwhile()

函数和宏

function

函数有自己独立的作用域:

function(add_custom_target name)
message("Creating target: ${name}")
add_executable(${name} ${ARGN})
endfunction()

# 调用函数
add_custom_target(myapp main.cpp utils.cpp)

# 带返回值的函数
function(get_sources result)
set(${result} main.cpp utils.cpp PARENT_SCOPE)
endfunction()
get_sources(my_sources)

macro

宏在调用者的作用域中展开:

macro(print_list list_name)
message("List ${list_name}:")
foreach(item IN LISTS ${list_name})
message(" ${item}")
endforeach()
endmacro()

set(MY_LIST a b c)
print_list(MY_LIST)

函数 vs 宏

特性functionmacro
变量作用域独立作用域调用者作用域
参数处理${ARGN}${ARGN}
返回值set(结果 PARENT_SCOPE)直接设置变量
适用场景逻辑封装简单操作

消息输出

message 命令

用于在构建过程中输出信息:

# 普通消息
message("Configuring project...")

# 状态消息(带前缀)
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")

# 警告(继续处理)
message(WARNING "This feature is deprecated")

# 错误(继续处理,但最终会失败)
message(SEND_ERROR "Configuration error")

# 致命错误(立即停止)
message(FATAL_ERROR "Cannot continue")

完整示例

下面是一个完整的 CMakeLists.txt 示例,展示了各种语法的综合应用:

cmake_minimum_required(VERSION 3.15)

project(MyApp
VERSION 1.0.0
DESCRIPTION "A sample application"
LANGUAGES CXX
)

# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 收集源文件
set(SOURCES
src/main.cpp
src/utils.cpp
src/config.cpp
)

# 创建可执行文件
add_executable(myapp ${SOURCES})

# 设置头文件目录
target_include_directories(myapp
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)

# 链接库
target_link_libraries(myapp
PRIVATE
pthread
)

# 编译选项
target_compile_options(myapp
PRIVATE
-Wall -Wextra
)

# 条件配置
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(myapp PRIVATE DEBUG_MODE)
endif()

小结

本章我们学习了:

  1. CMake 语言基础:注释、字符串、列表
  2. 项目配置:cmake_minimum_required、project
  3. 构建目标:add_executable、add_library
  4. 编译配置:include 路径、定义、选项
  5. 条件判断:if、elseif、else
  6. 循环:foreach、while
  7. 函数和宏:自定义命令

练习

  1. 创建一个包含多个源文件的项目
  2. 使用条件判断设置不同平台的编译选项
  3. 编写一个函数来简化常用的目标配置
  4. 使用 configure_file 生成配置头文件
  5. 创建一个静态库和一个使用该库的可执行文件

参考资源