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
将库链接到目标:
# 基本链接
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 宏
| 特性 | function | macro |
|---|---|---|
| 变量作用域 | 独立作用域 | 调用者作用域 |
| 参数处理 | ${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()
小结
本章我们学习了:
- CMake 语言基础:注释、字符串、列表
- 项目配置:cmake_minimum_required、project
- 构建目标:add_executable、add_library
- 编译配置:include 路径、定义、选项
- 条件判断:if、elseif、else
- 循环:foreach、while
- 函数和宏:自定义命令
练习
- 创建一个包含多个源文件的项目
- 使用条件判断设置不同平台的编译选项
- 编写一个函数来简化常用的目标配置
- 使用 configure_file 生成配置头文件
- 创建一个静态库和一个使用该库的可执行文件