开头OK了书接上回在了解了基本的CMake语法后我们这一期要来学习下CMake多目录以及一些基本函数语法。我们直接开始。一.多目录嵌套CMake单文件CMakeLists.txt只适合微型 Demo工程规模一旦扩大所有配置塞在一个文件里会出现三个核心问题职责混乱可维护性差库代码、主程序、测试、示例的构建逻辑混在一起修改一处容易影响其他模块配置效率低修改测试代码也会触发整个项目重新配置大型项目配置耗时会非常明显无法按需构建测试、示例、基准测试属于可选内容无法做到默认不构建、按需单独编译工业级项目的标准解法是按模块拆分目录每个目录一个独立的CMakeLists.txt顶层通过add_subdirectory引入子模块实现模块解耦、职责分离、按需构建1. 基本机制——add_subdirectory()add_subdirectory(source_dir)的行为完全等价于「函数调用」暂停当前CMakeLists.txt的执行进入指定子目录完整执行其中的CMakeLists.txt执行完毕后返回父目录继续执行下一行代码核心结论目标Target是全局可见的普通变量不是。只要在前面的目录中用add_library/add_executable定义过目标后面所有目录都可以直接链接使用但普通变量有独立作用域子目录里定义的变量父目录默认看不见add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])参数作用典型使用场景source_dir子源目录路径必须包含CMakeLists.txt路径相对当前源目录引入本地模块目录binary_dir指定对应的构建目录默认和源目录同名第三方源码目录重命名构建目录避免命名冲突EXCLUDE_FROM_ALL该目录下的目标默认不加入默认构建只有显式指定目标名才会编译测试代码、示例代码、基准测试减少日常构建耗时SYSTEM该目录的头文件按系统头文件处理自动屏蔽其中的警告引入第三方源码、不想处理第三方代码的编译警告时2. 使用示例如上图现在我们的目录结构是这样的每一个子目录里都对应有个CMakeLists.txt注意app/CMakeLists.txt里没有add_subdirectory指向src但它能用math这个 target——因为add_subdirectory(src)已经在它之前执行过了。target 是全局的变量不是目标是全局的变量不是3.构建树会镜像add_subdirectory会自动在构建目录中创建和源目录同名的子目录形成「源树 - 构建树」一一对应的镜像结构也就是 CMake 「三棵树」模型中的源树和构建树的对应关系源目录my_lib/→ 构建目录build/my_lib/源目录app/→ 构建目录build/app/二.变量体系1.三大作用域与set基础用法CMake 中所有变量本质都是字符串类型但根据生命周期、作用范围、持久化能力可以分为三类分别对应set函数的不同写法1目录作用域普通全局变量这是最常用的普通变量由set(var value)直接定义属于「目录级」作用域也是我们日常写配置最常用的类型核心规则向下继承向上隔离父目录定义的普通变量子目录add_subdirectory引入可以直接读取子目录内修改同名变量只会修改自己的「本地副本」不会影响父目录的原始变量方向是单向的子能读父父不能读子。作用范围当前 CMakeLists.txt 所在目录及其所有子目录。生命周期仅在本次 CMake 配置过程中有效不会写入缓存文件重新配置会重置对应 set 语法通俗理解父目录把变量「抄了一份」给子目录子目录改自己的抄本原件不会变2函数作用域局部变量在function()自定义函数内部定义的变量默认是函数局部作用域和目录作用域相互独立。核心规则函数内部可以读取父作用域调用函数的目录 / 上层函数的变量函数内直接修改变量只在函数内部生效函数外部访问不到要把函数内的计算结果传回外层必须加PARENT_SCOPE关键字对应set语法PARENT_SCOPE向上一级传递变量作用把当前作用域的变量写入上一级父作用域专门用于「子目录→父目录」「函数→调用方」的结果回传。两个关键注意点只影响直接父级不会逐层穿透孙目录设置的PARENT_SCOPE变量只会传到直接父目录不会自动传到祖父目录要跨多层传递必须每一层都加PARENT_SCOPE逐层上传。当前作用域的值不变加了PARENT_SCOPE后变量只会写到父作用域当前子目录 / 函数里的该变量仍然是旧值甚至未定义这是最容易混淆的细节。典型场景子目录编译完库把库的路径、版本等信息传回父目录自定义函数返回计算结果、配置结果3Cache 缓存作用域持久化全局变量缓存变量是 CMake 最特殊的一类变量它会持久化写入CMakeCache.txt文件跨多次 CMake 配置会话保持有效且整个工程所有目录都能访问。核心规则全局可见整个工程的所有 CMakeLists.txt、所有子目录都可以读取持久化保存首次配置写入CMakeCache.txt之后重新运行 cmake 会先读取缓存默认不会被 CMakeLists 里的新默认值覆盖优先级命令行-D参数 缓存里的旧值 CMakeLists 里的默认值对应set语法set(VAR_NAME value CACHE TYPE 变量说明文字)其中TYPE是变量类型常见的有STRING普通字符串BOOL布尔值取值 ON/OFFPATH目录路径FILEPATH文件路径补充语法option()option本质就是一个简化的 BOOL 型缓存变量缓存变量的经典踩坑修改了 CMakeLists 里的option默认值重新构建发现没生效。原因第一次配置后值已经写入缓存后续配置会优先读取缓存里的旧值解决删除 build 目录重新配置或者用-D命令行参数强制覆盖FORCE强制覆盖缓存变量作用强制修改已经存在的缓存变量的值打破「缓存旧值优先」的默认规则。语法set(VAR_NAME newValue CACHE STRING 变量说明 FORCE)使用原则非常不推荐滥用缓存变量的设计初衷是让用户通过-D参数自由配置工程代码里强制覆盖会破坏用户的配置权仅用于极少数必须强制锁定配置的场景比如某些编译选项、特性开关必须强制开启 / 关闭2.内置变量1路径类变量这类变量全部由 CMake 自动维护用来定位源码和构建目录是编写所有路径的基础变量名含义核心区别CMAKE_SOURCE_DIR整个工程最顶层的源目录根 CMakeLists.txt 所在目录整个工程唯一永远指向工程根目录CMAKE_BINARY_DIR整个工程最顶层的构建目录对应根目录的构建路径PROJECT_SOURCE_DIR最近一次project()命令所在的源目录子项目调用project()后值会变化写库工程优先用这个兼容性更好PROJECT_BINARY_DIR最近一次project()对应的构建目录对应当前项目的构建路径CMAKE_CURRENT_SOURCE_DIR当前正在处理的 CMakeLists.txt所在的源目录进入子目录后会自动切换写相对路径最常用CMAKE_CURRENT_BINARY_DIR当前 CMakeLists.txt 对应的构建目录存放生成文件、中间产物的默认位置CMAKE_CURRENT_LIST_DIR当前正在执行的 cmake 文件所在目录和include()配合时与上面不同引入子脚本时CURRENT_SOURCE_DIR不变CURRENT_LIST_DIR会变成脚本所在目录2产物输出路径用来控制可执行文件、库文件的输出位置替代默认的「当前构建目录」变量名作用对应产物类型CMAKE_RUNTIME_OUTPUT_DIRECTORY可执行文件的统一输出目录可执行程序Windows 下的 dll 也在此目录CMAKE_LIBRARY_OUTPUT_DIRECTORY动态库的统一输出目录Linux/macOS 的 .so/.dylib 动态库CMAKE_ARCHIVE_OUTPUT_DIRECTORY静态库的统一输出目录.a/.lib 静态库注意点这三个是全局变量必须在add_executable/add_library之前设置才会对后续创也可以针对单个目标设置属性优先级高于全局变量set_target_properties(myapp PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)关于旧写法EXECUTABLE_OUTPUT_PATH/LIBRARY_OUTPUT_PATH这是 CMake 早期的遗留变量现在已经不推荐使用优先用上面带OUTPUT_DIRECTORY的标准变量语义更清晰支持更完善3项目信息类全部由project()命令自动生成用来获取项目的基本信息变量名含义PROJECT_NAME当前项目的名称PROJECT_VERSION完整版本号如 1.2.3PROJECT_VERSION_MAJOR主版本号对应 1.2.3 里的 1PROJECT_VERSION_MINOR次版本号对应 1.2.3 里的 2PROJECT_VERSION_PATCH修订号对应 1.2.3 里的 34构建配置类控制编译、链接的全局行为通常通过命令行-D参数设置变量名作用常见值CMAKE_BUILD_TYPE构建类型Debug / Release / RelWithDebInfo / MinSizeRelCMAKE_CXX_STANDARDC 语言标准11 / 14 / 17 / 20CMAKE_CXX_COMPILERC 编译器路径如/usr/bin/gCMAKE_INSTALL_PREFIX安装前缀路径默认/usr/local三.核心语法1.include() 命令include是 CMake 代码复用与模块化的核心命令它会读取指定的.cmake脚本文件或 CMake 内置模块将其中所有代码直接在当前作用域展开执行等价于把文件内容完整复制粘贴到调用位置1完整语法include(file|module [OPTIONAL] [RESULT_VARIABLE var] [NO_POLICY_SCOPE])参数详细说明file / module两种输入形式1.本地文件路径直接指定.cmake文件的相对 / 绝对路径相对路径默认基于当前源目录2.模块名直接写模块名如GNUInstallDirsCMake 会自动在官方模块路径中查找对应.cmake文件。OPTIONAL可选标识。若文件 / 模块不存在不会报错终止仅静默跳过不加该参数时找不到文件会直接配置失败。RESULT_VARIABLE var将执行结果存入指定变量找到并执行成功变量存储文件的绝对路径失败则存储NOTFOUND。2两种典型使用方式方式 1引入本地自定义脚本封装通用函数、统一编译配置是项目内最常用的场景# 顶层 CMakeLists.txt 中引入项目自定义工具脚本 include(相对顶层CMake的相对路路径/XXXX.cmake) # 引入后可直接调用脚本中定义的函数/变量方式 2引入 CMake 内置模块CMake 自带大量官方标准模块直接写模块名即可引入无需写路径和后缀# 引入安装目录标准模块自动定义 CMAKE_INSTALL_LIBDIR 等标准路径变量 include(GNUInstallDirs) # 引入包配置辅助模块用于生成 find_package 兼容的配置文件 include(CMakePackageConfigHelpers)3include 与 add_subdirectory 的核心区别对比维度includeadd_subdirectory操作对象单个.cmake脚本文件包含 CMakeLists.txt 的工程目录作用域不创建新作用域在当前作用域直接执行创建独立子作用域子目录变量默认不会回传父目录构建目录不会在 build 目录下生成镜像子目录自动在构建树中创建对应子目录存放编译产物执行逻辑等价于代码文本复制粘贴等价于函数调用进入子目录执行完再返回典型用途封装通用函数、统一编译配置、工具链划分工程模块、分目录管理源码路径关键细节在include定义的函数中两个极易混淆的路径变量CMAKE_CURRENT_SOURCE_DIR调用时所在的当前源目录随调用位置变化CMAKE_CURRENT_LIST_DIR原本.cmake 文件所在的目录固定不变编写通用脚本时引用脚本同目录的资源必须用CMAKE_CURRENT_LIST_DIR否则换目录调用会路径错误2.add_defintions() 编译宏定义add_definitions用于向当前目录及所有子目录的编译目标添加C/C 预处理宏定义对应编译器的-DGCC/Clang或/DMSVC参数常用于条件编译、功能开关、版本号注入等场景1基本用法add_definitions(-DMACRO1 -DMACRO2value ...)多个宏定义可以一次性写入用空格分隔。格式为-D宏名或-D宏名值和编译器参数格式一致。不带值的宏默认定义为1常用示例定义功能开关宏# 定义 DEBUG_LOG 宏代码中可用 #ifdef DEBUG_LOG 判断 add_definitions(-DDEBUG_LOG)定义带值的宏# 注入项目版本号代码中可直接使用 VERSION_STR 字符串 add_definitions(-DVERSION_STR1.2.0 -DMAX_BUFFER_SIZE1024)作用域规则add_definitions是目录级别的命令作用域规则如下调用后当前目录下的所有编译目标add_executable、add_library都会继承这些宏定义。后续通过add_subdirectory进入的子目录也会自动继承父目录的宏定义。子目录中再次调用add_definitions是追加效果不会覆盖父目录的定义2与 target_compile_definitions 的对比target_compile_definitions()语法target_compild_definitions(目标 PUBLIC/PRIVATE/INTERFACE 宏 值)对比维度add_definitionstarget_compile_definitions作用域目录级影响当前 所有子目录所有目标目标级仅影响指定的单个目标粒度粗无法针对单个目标差异化配置细可精准控制每个目标的宏定义传递性自动向下传递给子目录可通过PUBLIC/PRIVATE/INTERFACE精确控制传递性推荐程度旧写法不推荐新项目使用✅ 现代 CMake 标准写法示例3注意事项与最佳实践字符串值的引号转义如果宏的值是字符串CMake 中需要转义引号否则会被解析成多个参数add_definitions(-DPROJECT_NAME\my_demo\)避免全局污染add_definitions会影响所有子目录容易造成宏名冲突新项目优先使用target_compile_definitions。不要写-D前缀的误区target_compile_definitions不需要写-D前缀CMake 会自动添加但add_definitions必须写-D。删除宏定义如果要移除已定义的宏使用remove_definitions(-DMACRO_NAME)同样是目录级生效。3.message() 消息输出命令message是 CMake 最基础的调试与信息输出命令用于在配置阶段向控制台打印文本、变量值、警告或错误信息是排查作用域、变量值问题的核心工具。语法message([级别] 消息内容1 消息内容2 ...)多个字符串参数会被自动拼接成一条消息。常用级别从低到高级别行为说明STATUS普通状态信息前缀带--是最常用的级别用于打印常规构建信息WARNING警告信息黄色高亮显示不会终止配置流程AUTHOR_WARNING面向项目开发者的警告默认和 WARNING 行为一致可通过开关抑制SEND_ERROR报错信息继续执行但配置最终失败不会生成构建文件FATAL_ERROR致命错误立即终止配置与生成流程用于不可恢复的错误不写级别时默认为普通消息无前缀示例4.if 条件判断if是 CMake 配置期逻辑控制的核心所有判断都在配置阶段执行完毕用于根据平台、编译器、用户选项等动态调整构建逻辑1基本语法if(条件表达式) # 条件为真时执行 elseif(条件表达式) # 上一个条件不满足、当前条件满足时执行 else() # 所有条件都不满足时执行 endif()CMake 的控制语句必须成对闭合if对应endif()中间可以有零个或多个elseif最多一个else2判断类型a.真假判断只要展开后的最终字符串是下面任意一种 →假FALSE匹配不区分大小写必须完整精确匹配空字符串布尔假关键字FALSE、OFF、NO、N纯数字0只有单独的000/0.0/-0都不算NOTFOUND、IGNORE、以及以-NOTFOUND结尾的字符串xxx-NOTFOUND✅其余全部 → 真TRUE只要不在上面假列表里非空就是真。例ONYESY1-1abc0.000 OFF 带空格全部为真if( ... )括号里面写不同东西展开逻辑不一样展开之后再套上面那张真假表。if(VAR)不带${}普通变量自动展开流程CMake 去找名字叫VAR的变量✅ 变量已经定义取变量的值作为最终字符串❌ 变量没有定义最终字符串 空字符串拿最终字符串去匹配真假表示例set(A OFF)if(A)# 展开得到OFF→ 在假表 → falseendif()set(B hello)if(B)# 展开得到hello→ 不在假表 → trueendif()if(C)# C没定义 → 空字符串 → falseendif()⚠️ 重点不会递归展开set(X Y)set(Y OFF)if(X)展开得到Y字符串 Y 不在假表 → true不会继续去找 Y 的值。if($ENV{VAR})先展开环境变量到字面串推荐稳定写法$ENV{VAR}是变量展开语法和${}一样在 if 解析前就展开读取环境变量 VAR环境变量不存在 / 值为空展开结果是空串环境变量存在展开结果是环境变量的文本外面加了双引号保证它是单个参数不会被分号、空格拆分结果字符串拿去匹配真假表if($ENV{PATH})#PATH 环境变量存在且非空 → 展开为长字符串不在假表 → trueendif()if($ENV{MY_NO_EXIST})# 环境变量不存在 → 展开为空串 → falseendif()if(ENV{VAR})if 内置 ENV {} 条件语法CMake 3.1 支持这是if命令单独识别的特殊形式不是 $ENV 展开不要搞混流程if 识别出ENV{VAR}直接读取环境变量不存在 / 值为空 → 最终值是空串存在 → 最终值是环境变量内容拿结果匹配真假表if(ENV{PATH})# 等价行为和上面$ENV{PATH}一致新版CMake可用endif()❗ 坑点很多老教程、旧 CMake 版本不支持 if (ENV {})写了会被当成普通变量 ENV {PATH}变量不存在 → 空 → false于是大家得出结论环境变量 if 永远是假这个结论是旧版本语法不支持导致的不是现代 CMake 的规则。✅ 稳妥兼容写法永远优先用if($ENV{VAR})if(something)引号包裹字面字符串流程不做任何变量、环境变量查找引号里面是什么最终字符串就是什么直接拿去匹配真假表if(OFF)# 字符串OFF → 假表 → falseendif()if(OFF123)# 不是OFF精确匹配 → trueendif()if(0)# 纯数字0→ falseendif()if(0.0)# 不是纯0→ trueendif()if(${VAR})${} 提前展开非常容易翻车流程${VAR}优先展开成变量的值把展开后的文本直接丢进 if 里面当作参数再执行 if 的逻辑❌ 大坑如果变量值带空格、分号展开后会变成多个参数直接语法报错set(MYhello world)if(${MY})# 展开后if(hello world)两个参数语法错误endif()结论尽量不要写if(${VAR})非要写一定要包引号if(${VAR})b.字符串比较使用STREQUAL相等、STRLESS小于、STRGREATER大于建议变量加引号避免空值报错if(${CMAKE_BUILD_TYPE}STREQUALDebug)message(当前为 Debug 构建模式)endif()c.数值比较使用EQUAL等于、LESS小于、GREATER大于set(VERSION_MAJOR2)if(VERSION_MAJOR LESS3)message(主版本号低于 3)endif()d. 逻辑组合支持AND、OR、NOT可用小括号改变运算优先级if(WIN32AND(CMAKE_CXX_COMPILER_ID STREQUALMSVC))message(当前环境Windows MSVC 编译器)endif()e.文件与路径判断EXISTS path路径文件 / 目录是否存在IS_DIRECTORY path路径是否为目录IS_ABSOLUTE path路径是否为绝对路径if(EXISTS${PROJECT_SOURCE_DIR}/third_party/lib/libfoo.a)link_directories(third_party/lib)endif()f.版本号比较使用VERSION_LESS、VERSION_GREATER、VERSION_EQUAL专门用于语义化版本对比if(CMAKE_VERSION VERSION_LESS3.20)message(FATAL_ERRORCMake 版本过低需要 3.20 及以上版本)endif()g. 正则匹配MATCHES用于正则匹配非常适合判断编译器、系统名等多值场景if(CMAKE_CXX_COMPILER_ID MATCHESGNU|Clang)target_compile_options(app PRIVATE-Wall-Wextra)endif()h.变量是否定义DEFINED判断变量是否被定义过注意不要加${}if(DEFINED MY_CUSTOM_FLAG) message(自定义标志已设置: ${MY_CUSTOM_FLAG}) endif()5.list 列表操作CMake 中的列表本质是用分号分隔的字符串比如set(SRCS a.cpp b.cpp)等价于set(SRCS a.cpp;b.cpp)。list命令专门用于操作这类列表数据a.追加元素APPEND向列表末尾追加一个或多个元素原地修改原列表:list(APPEND 变量 要追加的内容)b.删除指定元素REMOVE_ITEM删除列表中所有匹配的元素值list(REMOVE_ITEM 变量 要删除的内容c.获取列表长度LENGTHlist(LENGTH 变量 OutputVar)d.按下标取值GETlist(GET 变量 idex 下标 OutputVar)e.指定连接符用指定分隔符把列表拼接成单个字符串list(JOIN 变量 字符 OutputVar)f.查找元素FIND查找元素在列表中的下标找不到返回 -1list(FIND 变量 要找的内容 OutputVar)g.排序SORTlist(SORT 变量 COMPARE compare CASE case ORDER way)/compare/STRING按照字典序 默认FILE_BASENAME按照路径NATURAL自然数排序/case/SENSITIVE关心大小写INSENSITIVE不关心/way/DESCENDING降序ASCENDING升序6. 循环1foreach()foreach用于遍历列表、范围枚举是批量处理多个目标、多个文件的核心工具所有循环逻辑同样在配置期执行形式 1直接枚举项最基础写法逐个遍历列出的所有项foreach(target app test_util benchmark) target_compile_options(${target} PRIVATE -Wall) endforeach()形式 2遍历列表变量使用IN LISTS遍历一个列表变量是最推荐的写法可避免展开歧义set(MODULES math util net) foreach(mod IN LISTS MODULES) add_subdirectory(${mod}) endforeach()形式 3范围循环使用RANGE生成整数序列格式为RANGE 起始 结束 [步长]步长默认为 1# 输出 1 2 3 4 5 foreach(i RANGE 1 5) message(i ${i}) endforeach() # 步长为2输出 0 2 4 6 8 foreach(i RANGE 0 8 2) message(i ${i}) endforeach()注意事项循环变量只在循环体内有效退出循环后恢复为原来的值。CMake 3.19 才支持break()和continue()低版本无法直接跳出循环。遍历列表时若修改列表本身可能导致遍历异常建议先拷贝列表再操作2while()while用于条件循环适合循环次数不固定、靠条件终止的场景基本语法while(条件表达式) # 循环体 endwhile()条件表达式的语法规则和if完全一致。配合 math (EXPR) 使用CMake 中不能直接写算术运算必须通过math(EXPR)命令执行数值计算因此while几乎总是和它搭配set(n 0) while(n LESS 3) message(n ${n}) math(EXPR n ${n} 1) endwhile()7.function()函数function是 CMake 最推荐的封装方式具备独立作用域行为和常规编程语言的函数一致适合封装可复用的构建逻辑1基本语法function(函数名 [形参1 形参2 ...]) # 函数体任意 CMake 命令 endfunction()函数名全局唯一调用时直接写函数名 参数即可执行。endfunction()后可补写函数名如endfunction(add_warnings)属于可选写法用于提升可读性2参数机制CMake 函数的参数分为命名形参和内置自动变量两类a.命名形参在函数定义时声明的参数调用时按位置一一对应传入b.内置参数变量函数体内自动存在以下特殊变量无需声明即可使用变量名含义ARGC实际传入的参数总个数ARGV全部参数组成的列表ARGN命名形参之外的多余参数变长参数列表ARGV0、ARGV1…第 N 个位置的参数下标从 0 开始3返回值CMake 函数没有return 值的语法所有返回值都通过写入调用者作用域的变量来实现核心是PARENT_SCOPEreturn () 命令函数内调用return()会立即终止当前函数回到调用者作用域继续执行不会影响调用者后续逻辑结尾OK了这一期的CMake学习就到这里了我们下期继续。如果对你的学习有所帮助感谢你的点赞、收藏、关注我主页里有更好康的呦往期回顾1.【CMake】第1期 从 CMake 初识到库的构建2. 【MySQL】第4期 用户管理与 C 语言连接编程详解3. 【MySQL】第3期 索引、事务与视图系统详解
阅读完成 · 觉得有帮助?