一文精通CMake
CMake
CMake 是个一个开源的跨平台自动化建构系统,用来管理软件建置的程序,并不依赖于某特定编译器,并可支持多层目录、多个应用程序与多个函数库。
CMake 通过使用简单的配置文件 CMakeLists.txt,自动生成不同平台的构建文件(如 Makefile、Ninja 构建文件、Visual Studio 工程文件等),简化了项目的编译和构建过程。
CMake 本身不是构建工具,而是生成构建系统的工具,它生成的构建系统可以使用不同的编译器和工具链。
- 跨平台支持: CMake 支持多种操作系统和编译器,使得同一份构建配置可以在不同的环境中使用。
- 简化配置: 通过 CMakeLists.txt 文件,用户可以定义项目结构、依赖项、编译选项等,无需手动编写复杂的构建脚本。
- 自动化构建: CMake 能够自动检测系统上的库和工具,减少手动配置的工作量。
- 灵活性: 支持多种构建类型和配置(如 Debug、Release),并允许用户自定义构建选项和模块。
基本工作流程:
- 编写 CMakeLists.txt 文件: 定义项目的构建规则和依赖关系。
- 生成构建文件: 使用 CMake 生成适合当前平台的构建系统文件(例如 Makefile、Visual Studio 工程文件)。
- 执行构建: 使用生成的构建系统文件(如
make
、ninja
、msbuild
)来编译项目。
安装
各架构安装方法如下:
sudo apt-get install cmake
sudo dnf install cmake
sudo pacman -S cmake
brew install cmake
从源码安装的方法这里略,可以看参考文献 1
基础使用
CMakeLists.txt 是 CMake 的配置文件,用于定义项目的构建规则、依赖关系、编译选项等。
一个工程里可以有多个 CMakeLists. txt 文件,例如 LLVM,可能每个 Cpp 文件夹都有一个。
最简单的实例:
cmake_minimum_required(VERSION 3.10)
project(MyProject CXX)
# 添加源文件(多个)
add_executable(MyExecutable main.cpp other.cpp)
# 链接目标文件与其他库:
target_link_libraries(MyExecutable MyLibrary)
# 添加其他路径(头文件路径)
include_directories(${PROJECT_SOURCE_DIR}/include)
# 设置目标属性
target_include_directories(MyExecutable PRIVATE ${PROJECT_SOURCE_DIR}/include)
# 安装规则
install(TARGETS MyExecutable RUNTIME DESTINATION bin)
#条件语句 elseif, else(),
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message("Debug build")
endif()
# 自定义命令
add_custom_command(
TARGET MyExecutable POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Build completed."
)
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 11)
CMake 使用变量来存储和传递信息,这些变量可以在 CMakeLists.txt 文件中定义和使用。变量可以分为普通变量和缓存变量。
缓存变量存储在 CMake 的缓存文件中,用户可以在 CMake 配置时修改这些值。缓存变量通常用于用户输入的设置,==例如编译选项和路径==。
- 普通变量:定义:
set(MY_VAR "Hello World")
使用 :message(STATUS "Variable MY_VAR is ${MY_VAR}")
- 缓存变量:定义:
set(MY_CACHE_VAR "DefaultValue" CACHE STRING "A cache variable")
CMake 可以通过 find_package() 指令自动检测和配置外部库和包。常用于查找系统安装的库或第三方库。
find_package(Boost 1.70 REQUIRED)
:指定库版本find_package(OpenCV REQUIRED PATHS /path/to/opencv)
: 查找库并制定路径- 使用查找到的库:
target_link_libraries(MyExecutable Boost::Boost)
- 设置包含目录和链接目录:
include_directories(${Boost_INCLUDE_DIRS})
link_directories(${Boost_LIBRARY_DIRS})
构建方法
- make 或 ninja 均可,例如先cmake -G “Ninja” .. 然后 ninja
- 可以使用 ninja XXX 指定编译目标
- make clean/ninja clean
特殊语法
容器 List
- 列表在 Cmake 的存储形式:
set(MYLIST foo bar baz)
等价于set(MYLIST "foo;bar;baz")
例如:cmake ../llvm -DLLVM_ENABLE_PROJECTS="clang;lld;bolt"
- List 的具体语法:
list(APPEND \ …) | 在列表末尾追加元素 |
list(PREPEND \ …) | 在列表开头插入元素 |
list(INSERT \ i …) | 在索引 i 处插入 |
list(LENGTH \ \ |
计算元素个数,结果放 |
list(GET \ i out) | 取第 i 个元素,放入 out |
==list(FIND \ val idx)== | 在列表中查找 val,返回索引 idx |
list(REMOVE_ITEM \ …) | 删除所有值等于给定项的元素 |
list(REMOVE_AT \ …) | 删除指定索引的元素 |
list(REMOVE_DUPLICATES \) | 去重 |
list(REVERSE \) | 反转列表 |
list(JOIN \ sep out) | 用 sep 把列表拼回字符串,结果放 out |
- 遍历方式:
foreach(proj IN LISTS LLVM_ENABLE_PROJECTS) message(STATUS "Will build subproject: ${proj}") endforeach()
- 查找元素是否存在:
list(FIND LLVM_ENABLE_PROJECTS "clang" clang_idx) if(clang_idx GREATER -1) message(STATUS "Include Clang frontend") endif()
实例:LLVM 项目里如何解析 LLVM_ENABLE_PROJECTS
# 顶层 CMakeLists.txt 中 set(LLVM_ENABLE_PROJECTS "clang;lld;bolt" CACHE STRING "List of LLVM sub-projects to build") # 然后在合适的位置做: foreach(subproj IN LISTS LLVM_ENABLE_PROJECTS) # 确保目录存在 if(EXISTS "${CMAKE_SOURCE_DIR}/${subproj}/CMakeLists.txt") add_subdirectory(${subproj}) else() message(FATAL_ERROR "Requested project '${subproj}' not found") endif() endforeach()
字符串 STRING
- 在 CMake 里,所有变量都是字符串,即便你觉得它是数值或列表,==底层依然以字符串形式存储。==
- 访问与展开:
set(MSG "Build type: ${CMAKE_BUILD_TYPE}") message(STATUS "${MSG}")
- STRING 命令全解:
使用方法:string(LENGTH <input> <out_var>) # 计算长度 string(SUBSTRING <input> <start> <len> <out_var>) # 截取子串 string(FIND <input> <substr> <out_var> [<start_index>]) # 查找位置 string(REPLACE <match> <replace> <input> <out_var>) # 直接替换 string(REGEX REPLACE <regex> <replace> <input> <out_var>) # 正则替换 string(REGEX MATCH <regex> <input> <out_var>) # 匹配并提取 string(TOUPPER <input> <out_var>) # 转大写 string(TOLOWER <input> <out_var>) # 转小写 string(STRIP <input> <out_var>) # 去除首尾空白 string(JOIN <sep> <out_var> <list>...) # 拼接列表(注意参数差异) string(RANDOM <length> <out_var>) # 随机字符串 string(TIMESTAMP <out_var> [format] [UTC])# 当前时间戳,支持格式化
- 截取与查找:
set(FULL "clang-format-12.0.1") string(FIND "${FULL}" "-" dash_pos) # dash_pos = 11 math(EXPR version_start "${dash_pos} + 1") string(SUBSTRING "${FULL}" ${version_start} -1 ver) # ver = "12.0.1"
- 正则与替换
set(TEXT "foo123bar456") # 将所有数字替换成 “#” string(REGEX REPLACE "[0-9]+" "#" result "${TEXT}") # result => foo#bar#
if else
基本语法:
判断条件:if(<condition>) # 条件为真时执行 elseif(<condition2>) # 前面的条件都不满足且 condition2 为真时执行 else() # 上述条件都不满足时执行 endif()
条件类型 | 语法 / 示例 | 说明 |
---|---|---|
布尔常量 | if(TRUE)if(ON)if(FALSE) | 等价于布尔值,区分大小写 |
字符串非空 | if(MY_VAR) | 只要变量非空或非 “FALSE”/“OFF” 就为真 |
变量已定义 | if(DEFINED VAR) | 检查变量是否存在(即是否用过 set()) |
文件存在 | if(EXISTS “path/to/file”) | 可是绝对路径或相对路径 |
目录存在 | if(IS_DIRECTORY “dir”) | 是否为目录 |
文件是否常规文件 | if(IS_SYMLINK “file”)if(IS_ABSOLUTE “path”) | 软链接、是否绝对路径等判断 |
目标是否存在 | if(TARGET target_name) | 判断某个 CMake target 是否已定义 |
命令是否存在 | if(COMMAND my_macro) | 判断函数/宏/命令是否存在 |
测试是否定义 | if(TEST my_test) | 是否存在某个 add_test() 测试名 |
策略是否定义 | if(POLICY CMP0077) | CMake 策略编号是否存在 |
环境变量是否定义 | if(ENV{HOME}) | 读取环境变量;空字符串为假 |
版本比较 | if(VERSION_LESS “3.20”)if(A VERSION_GREATER B) | 支持 VERSION_EQUAL, VERSION_NOT_EQUAL, VERSION_LESS, 等 |
字符串比较 | if(A STREQUAL B)if(A STRLESS B) | 字符串比较,大小写敏感 |
数值比较 | if(A EQUAL B)if(A LESS_EQUAL B) | 比较整数值;不能用于浮点 |
正则匹配 | if(A MATCHES “regex”) | A 匹配某个正则表达式 |
逻辑运算符 | if(A AND B)if(NOT A)if(A OR B)if((A OR B) AND C) | 支持嵌套括号与组合运算 |
列表展开 | if(A;B;C) | 相当于 if(A AND B AND C) |
表达式常量比较 | if(“1” LESS “2”)if(“abc” STRGREATER “aaa”) | 字符串/数值混合判断也支持 |
FUNCTION
最基础的定义和调用:
# 定义函数
function(<name> [ARG1 [ARG2 ...]])
# 函数体:可以访问 ${ARG1}, ${ARG2}, ${ARGN}, ${ARGV}, ${ARGC} 等
message(STATUS "In function ${name}: ARGC=${ARGC}, ARGV=${ARGV}")
endfunction()
# 调用函数
<name>(value1 value2)
内置参数变量:
变量名 | 含义 |
---|---|
ARGV | 列表,==函数所有实参(一个字符串,分号分隔)== |
ARGN | 列表,所有未被形参捕获的尾部实参 |
ARGC | 整数,实际传入参数的个数 |
ARGV0 | 字符串,第 1 个实参 |
ARGV1… | 字符串,第 2、3…各个实参 |
CMAKE_CURRENT_FUNCTION | 字符串,当前调用的函数名 |
举例:
function(print_all)
message("ARGS(${ARGC}): ${ARGV}")
message("REST: ${ARGN}")
endfunction()
# 调用
print_all(a b c d)
# ARGS(4): a;b;c;d
# REST: (空,因为无溢出参数)
支持return() 语法。
macro
定义与调用:
# 语法
macro(<name> [ARG1 [ARG2 ...]])
# 函数体:可以使用 ${ARG1}, ${ARG2}, ${ARGV}, ${ARGC}, ${ARGN}
message(STATUS "In macro ${name}: ARGV=${ARGV}, ARGC=${ARGC}")
endmacro()
# 调用
<name>(foo bar baz)
和函数的对比:
特性 | macro() | function() |
---|---|---|
作用域 | 共享调用处作用域 | 私有局部作用域 |
变量修改 | 改动的变量直接影响外层 | 需 PARENT_SCOPE 才影响外层 |
参数捕获 | 不捕获形参,所有实参都在 ARGV | 捕获到形参,剩余在 ARGN |
推荐场景 | 需要在调用点展开或注入作用域时用 | 一般推荐使用函数以避免污染作用域 |
例如如下就是一个共享作用域的场景:
macro(set_default var default)
if(NOT DEFINED ${var})
set(${var} ${default}) # 直接在调用处定义
endif()
endmacro()
# 使用
set_default(FOO "bar")
message(STATUS "FOO=${FOO}") # FOO=bar
foreach
foreach() 是 CMake 中的核心迭代命令,支持多种迭代模式。
基本语法:
foreach(loop_var IN ITEMS item1 item2 ...)
# body: 使用 ${loop_var}
endforeach()
遍历:
set(MY_LIST a b c d)
foreach(item IN LISTS MY_LIST)
message(STATUS "Item=${item}")
endforeach()
foreach 支持 break () 语法!
Generator Expressions
下面深入讲解 CMake 的 Generator Expressions(生成器表达式),它们是在生成阶段(Generate)而非配置阶段(Configure)被解析,用来根据上下文动态生成属性值。
1. 概念与使用场景
- 定义:Generator Expression(简称 GE)以 $<…> 形式书写,内部可以包含条件、查询或属性引用。
- 解析时机:GE 在 CMake 生成 Ninja/Makefile 等构建脚本时解析,而不会在第一次 cmake 配置时展开。
- 用途:按配置(Debug/Release)、目标类型、平台、编译特性等按需生成编译器/链接器选项、安装路径、依赖列表等。
基本语法:
$<GENEXPR:arg1,arg2,…>
2. 常用 Generator Expressions 一览
表达式 | 含义与参数 | 示例 | |
---|---|---|---|
配置相关 | |||
\$<CONFIG:cfg> |
如果当前配置是 cfg(大小写不敏感)返回 1,否则空。 | "$<CONFIG:Debug>" 在 Debug 下为 1,Release 下空。 |
|
\$<CONFIG:Debug,Release> |
多值匹配,只要在列表里就返回 1。 | "$<CONFIG:Debug,RelWithDebInfo>" |
|
条件判断 | |||
\$<AND:\$<...>,\$<...>> |
逻辑且。两个子表达式都非空时返回 1。 | $<AND:$<CONFIG:Debug>,$<BOOL:USE_FOO>> |
|
\$<OR:…> |
逻辑或。任一非空即 1。 | $<OR:$<CONFIG:Release>,$<BOOL:USE_BAR>> |
|
\$<NOT:…> |
逻辑非。内部为空或 “0” 则 1,否则空。 | $<NOT:$<CONFIG:Debug>> |
|
\$<IF:cond,true,false> |
三元表达式:cond 非空时取 true,否则取 false。 | $<IF:$<CONFIG:Debug>,-DDEBUG=1,> |
|
布尔与字符串 | |||
\$<BOOL:var> |
将 var 当布尔值解析,真返回 1,否则空。 | $<BOOL:${BUILD_TESTS}> |
|
\$<STREQUAL:a,b> |
字符串相等返回 1,否则空。 | $<STREQUAL:${CMAKE_SYSTEM_NAME},Linux> |
|
目标与属性 | |||
\$<TARGET_FILE:tgt> |
返回目标 tgt 的最终可执行/库文件路径。 | $<TARGET_FILE:MyLib> |
|
\$<TARGET_LINKER_FILE:tgt> |
返回链接器可见的文件(DLL/SO)路径。 | $<TARGET_LINKER_FILE:MyLib> |
|
\$<TARGET_PROPERTY:tgt,prop> |
读取已定义目标的属性值(通常是列表拼成字符串)。 | $<TARGET_PROPERTY:MyLib,INTERFACE_INCLUDE_DIRECTORIES> |
|
列表与字符串处理 | |||
\$<JOIN:list;sep> |
把 list(分号分隔)用 sep 拼接成字符串。 | $<JOIN:WARN;ERROR,;> → WARN;ERROR |
|
\$<SEMICOLON> |
字面输出分号,用于在属性字符串中插入 ; 。 |
"-DVAL1=1$<SEMICOLON>-DVAL2=1" |
|
编译器/平台信息 | |||
\$<C_COMPILER_ID> |
C 编译器标识(GNU/Clang/MSVC 等)。 | "$<C_COMPILER_ID>" |
|
\$<CXX_COMPILER_ID> |
C++ 编译器标识。 | ||
\$<PLATFORM_ID> |
平台标识(Windows, Linux, Darwin, …)。 | ||
\$<TARGET_PROPERTY>… |
上表所示 | ||
安装/导出 | |||
\$<INSTALL_INTERFACE:…> |
安装时展开;构建时为空。 | ||
\$<BUILD_INTERFACE:…> |
构建时展开;安装时为空。 |
完整请参考> 官方文档($\
)
目标属性(Target Properties)
1. 常见目标属性
- ARCHIVE_OUTPUT_DIRECTORY、LIBRARY_OUTPUT_DIRECTORY、RUNTIME_OUTPUT_DIRECTORY
set_target_properties(MyLib PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin )
- INCLUDE_DIRECTORIES(已被
target_include_directories
取代) - COMPILE_OPTIONS / COMPILE_DEFINITIONS
- LINK_OPTIONS
- INTERFACE_INCLUDE_DIRECTORIES / INTERFACE_LINK_LIBRARIES / INTERFACE_COMPILE_FEATURES
2. 查询与继承
- get_target_property
get_target_property(srcs MyLib SOURCES) message(STATUS "MyLib sources: ${srcs}")
- 目标间自动继承(Keyword:
INTERFACE_*
)add_library(Base INTERFACE) target_include_directories(Base INTERFACE ${CMAKE_SOURCE_DIR}/include) add_library(Impl STATIC impl.cpp) target_link_libraries(Impl PUBLIC Base) # Impl 会继承 Base 的 include 目录
3. Generator Expressions 与属性
target_compile_definitions(MyLib PUBLIC $<$<CONFIG:Debug>:DEBUG_MODE> )
自定义构建步骤(Custom Build Steps)
1. add_custom_command
- 直接为目标添加命令(POST_BUILD)
add_executable(App main.cpp) add_custom_command(TARGET App POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:App> ${CMAKE_BINARY_DIR}/deploy/ COMMENT "Deploying App..." )
- 生成文件(OUTPUT / COMMAND)
add_custom_command( OUTPUT generated.cpp COMMAND python3 ${CMAKE_SOURCE_DIR}/tools/gen.py -o generated.cpp DEPENDS ${CMAKE_SOURCE_DIR}/tools/gen.py COMMENT "Generating C++ source" ) add_executable(GenApp generated.cpp)
2. add_custom_target
- 虚拟目标,不产生文件
add_custom_target(format COMMAND clang-format -i ${SOURCES} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMENT "Formatting sources" )
- 联合依赖
add_custom_target(all_gen DEPENDS generated.cpp) add_custom_target(build_all ALL DEPENDS all_gen App)
3. 强制依赖(add_dependencies)
add_dependencies(App generate_code) # 确保 generate_code 先执行
测试与打包(Testing & Packaging)
1. 测试(CTest)
- 启用测试
enable_testing() include(CTest)
添加测试
add_executable(TestFoo test_foo.cpp) add_test(NAME FooTest COMMAND TestFoo --gtest_output=xml)
使用
add_llvm_unittest
/add_test
- 并行测试:
ctest -j ${N}
2. 安装规则(install)
- 库与可执行文件
install(TARGETS MyLib MyApp EXPORT MyProjectTargets ARCHIVE DESTINATION lib LIBRARY DESTINATION lib RUNTIME DESTINATION bin )
- 头文件与目录
install(DIRECTORY ${CMAKE_SOURCE_DIR}/include/ DESTINATION include)
- 导出 CMake 配置
install(EXPORT MyProjectTargets FILE MyProjectConfig.cmake NAMESPACE MyProj:: DESTINATION lib/cmake/MyProject )
3. CPack 打包
执行:include(CPack) set(CPACK_PACKAGE_NAME "MyProject") set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") set(CPACK_GENERATOR "TGZ;ZIP;DEB") # 可选设置:CPACK_DEBIAN_PACKAGE_MAINTAINER, CPACK_DEBIAN_PACKAGE_DEPENDS
make package # 生成 .tar.gz/.zip cpack -G DEB # 生成 .deb
LLVM 实战
cmake_minimum_required(VERSION 3.13.4) # 1 project(LLVM LANGUAGES C CXX) # 2 # 3. 启用选项与外部项目列表 option(LLVM_ENABLE_ASSERTIONS "Enable LLVM assertions." OFF) # Bool选项 set(LLVM_ENABLE_PROJECTS "clang;lld;compiler-rt" CACHE STRING "") # 4. 包含通用宏与功能函数 list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") include(LLVMConfig) # 定义 find_llvm_*, add_llvm_* 系列宏 include(AddLLVM) # 提供 add_llvm_library, llvm_output, etc. # 5. 全局设置——编译特性、警告、优化 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_compile_options(-Wall -Wextra -Werror) # LLVM 常用警告级别 # 6. 分发子项目 add_subdirectory(lib) # LLVM core libraries add_subdirectory(clang) # Clang 前端 # … 其它 projects based on LLVM_ENABLE_PROJECTS 列表 # 7. 测试与安装 enable_testing() add_subdirectory(test) # LLVM 自身测试 include(InstallLLVM) # 自动生成 install 规则、CPack 打包脚本
参考文献
评论