☰
CMake endif 命令完全指南:if 块的闭合、向后兼容参数与源码级配对机制
2026/10/3 2:09:37 网站建设 项目流程
  • 构建工具
  • 开发工具
  • CLI

【免费下载链接】CMake

Mirror of CMake upstream repository

项目地址:https://gitcode.com/gh_mirrors/cm/CMake
点击查看免费下载

endif是 CMake 中最基础也最常用的块结构命令之一,用于结束由if命令开启的条件命令块。本文以 endif 官方文档 为核心骨架,系统讲解endif的语法、可选的向后兼容参数、与elseif/else的协作关系,并结合本仓库源码(cmIfCommand.cxx、cmListFileCache.cxx)深入剖析if/endif配对的底层实现,同时给出完整的条件表达式速查与实战示例,帮助读者彻底掌握 CMake 条件分支的正确写法。

endif 命令:语法与核心作用

endif命令的官方定义为:结束if块中的命令列表(Ends a list of commands in an if block)。它的完整语法为:

endif([<condition>])

其中<condition>参数是可选的,仅用于向后兼容。如果提供该参数,它必须是开头if子句中条件参数的逐字重复(a verbatim repeat of the argument of the openingifclause),即连空白、引号写法都必须完全一致。

在绝大多数现代 CMake 工程中,推荐写法是直接使用不带参数的endif(),这样既清晰又避免出错。

典型块结构

一个标准的if/elseif/else/endif块如下(语法定义见 if 命令文档):

if(<condition>) <commands> elseif(<condition>) # 可选,可重复多次 <commands> else() # 可选 <commands> endif()

执行流程为:CMake 先求值if子句的<condition>;若为真则执行if块内命令,否则依次按相同方式处理可选的elseif块;若所有条件都不为真,则执行可选的else块。无论哪个分支被选中,最终都统一由endif收尾闭合。

向后兼容的 参数:规则与陷阱

从 if 命令文档 可知,历史上else与endif都允许附带一个可选的条件参数,这是为了兼容早期 CMake 脚本风格:

if(WIN32) message(STATUS "Windows") endif(WIN32) # 参数必须与开头的 if 条件逐字相同

现代 CMake 已不再要求也不鼓励这种写法,官方文档明确该参数仅供向后兼容。同时else命令的可选参数同样仅用于向后兼容且不会被求值。

源码如何校验参数匹配

在 Source/cmIfCommand.cxx 中,cmIfFunctionBlocker::ArgumentsMatch定义了endif与if的参数匹配规则:

bool cmIfFunctionBlocker::ArgumentsMatch(cmListFileFunction const& lff, cmMakefile&) const { return lff.Arguments().empty() || lff.Arguments() == this->Args; }

即:当endif的参数为空(Arguments().empty()),或与打开该块的if命令的参数完全相等(lff.Arguments() == this->Args)时,才判定为匹配并闭合该块。这意味着:

  • endif()永远合法;
  • endif(WIN32)只有在配套的if(WIN32)时才匹配;
  • 若endif带了一个与if条件不同的参数(如if(WIN32) ... endif(UNIX)),块无法正确闭合,CMake 会报告配对错误。

源码级实现:if/endif 如何配对与执行

if不是普通的即时执行命令,而是通过Function Blocker(函数阻塞器)机制实现的。理解这一机制,就能明白为什么endif的参数匹配如此严格。

Function Blocker 机制

在 cmIfCommand.cxx 中定义了一个cmIfFunctionBlocker,它记录该if块的起始命令名与结束命令名:

class cmIfFunctionBlocker : public cmFunctionBlocker { public: cm::string_view StartCommandName() const override { return "if"_s; } cm::string_view EndCommandName() const override { return "endif"_s; } ... };

if命令在求值条件后(cmIfCommand 函数),会把一个cmIfFunctionBlocker压入 cmMakefile::AddFunctionBlocker 维护的栈中;只有当执行到endif且参数匹配时,该阻塞器才会被弹出(对应RemoveFunctionBlocker的实现,见 cmMakefile.cxx)。在块未闭合期间,阻塞器决定哪些命令被"跳过"(elseif/else分支切换)或真正执行(Replay逻辑,见 cmIfCommand.cxx)。

解析阶段的嵌套校验

在 CMake 读取脚本的解析阶段,cmListFileCache.cxx 的 CheckNesting 函数 还会用栈结构校验if/elseif/else/endif的嵌套平衡:

} else if (name == "endif") { if (!TopIs(stack, NestingStateEnum::If) && !TopIs(stack, NestingStateEnum::Else)) { return cmListFileContext::FromListFileFunction(func, this->FileName); } stack.pop_back(); }

即:endif只有在栈顶状态是If或Else时才被接受,否则立即返回出错的上下文位置。elseif只能在If状态下出现,else之后不允许再有elseif。这一机制保证了任何多出的endif或缺失的endif都会在解析期被精确定位报错。

此外,cmCommands.cxx 将endif注册为合法命令,而在 cmMakefile.cxx 的 CMP0000 兼容命令白名单中,if、endif、else、elseif均被列为"永久的简单命令",即使脚本未调用cmake_minimum_required也允许使用。

条件表达式速查:endif 所闭合的求值规则

endif本身不参与条件求值,但它闭合的if/elseif条件支持一整套表达式语法(完整定义见 if 命令文档)。掌握这些规则是正确使用if/endif的前提。

复合条件的优先级

条件按以下优先级求值:

  1. 括号(condition):最内层先求值;
  2. 一元测试:存在性检查与文件操作;
  3. 二元测试:比较、版本比较、路径比较、IN_LIST、IS_NEWER_THAN;
  4. 一元逻辑运算符NOT;
  5. 二元逻辑运算符AND、OR,从左到右求值,不短路。

示例:

if((condition) AND (condition OR (condition))) ... endif()

常量与真假判定

  • 真值常量(大小写不敏感):1、ON、YES、TRUE、Y,以及任意非零数字(含浮点数);
  • 假值常量:0、OFF、NO、FALSE、N、IGNORE、NOTFOUND、空字符串,以及以-NOTFOUND结尾的值;
  • 变量:已定义且值不是假常量时为真,未定义则为假(注意:宏参数不是变量,if(ENV{some_var})恒为假);
  • 字符串:带引号的字符串除非其值为真常量,否则恒为假;
  • 空参数if():恒为假。

存在性检查

表达式说明
if(COMMAND <name>)名称是可调用的命令、宏或函数
if(DEFINED <name>|CACHE{<name>}|ENV{<name>})变量/缓存变量/环境变量已定义(CACHE{}支持自 3.14 起)
if(EXISTS <path>)文件或目录存在且可读,解析符号链接
if(TARGET <name>)已创建的逻辑目标(add_executable/add_library/add_custom_target)
if(TEST <name>)已由 add_test 创建的测试(3.3 起)
if(POLICY <id>)存在CMP<NNNN>形式的策略
if(<x> IN_LIST <list>)元素在列表变量中(3.3 起)

文件操作与路径判断

if(IS_READABLE <path>) # 3.29 起,可读 if(IS_WRITABLE <path>) # 3.29 起,可写 if(IS_EXECUTABLE <path>) # 3.29 起,可执行 if(IS_DIRECTORY <path>) # 是否为目录 if(IS_SYMLINK <path>) # 是否为符号链接 if(IS_ABSOLUTE <path>) # 是否为绝对路径 if(<file1> IS_NEWER_THAN <file2>) # 时间戳比较,相同时为真

注意:文件类判断仅对显式完整路径有明确定义行为,前导~/不会被展开为主目录。

数值 / 字符串 / 版本比较

  • 数值比较(按 Cdouble解析实数):LESS、GREATER、EQUAL、LESS_EQUAL、GREATER_EQUAL(后两者 3.7 起);
  • 字符串比较(字典序):STRLESS、STRGREATER、STREQUAL、STRLESS_EQUAL、STRGREATER_EQUAL;
  • 正则匹配:if(<var|str> MATCHES <regex>),括号分组会捕获到CMAKE_MATCH_<n>变量;
  • 版本比较:VERSION_LESS、VERSION_GREATER、VERSION_EQUAL及_EQUAL变体,格式为major[.minor[.patch[.tweak]]],缺失组件按 0 处理,非整数部分会被截断。

路径比较(PATH_EQUAL / PATH_IS_PREFIX)

PATH_EQUAL(3.24 起)按组件逐段比较路径而不访问文件系统,多个路径分隔符会被折叠;PATH_IS_PREFIX(4.5 起)判断左侧路径是否是右侧路径的前缀,注意它是纯词法判断,不做归一化:

# PATH_EQUAL 比较结果为 TRUE,而 STREQUAL 为 FALSE if ("/a//b/c" PATH_EQUAL "/a/b/c") ... endif() # PATH_IS_PREFIX 判断 if ("/a/b" PATH_IS_PREFIX "/a/b/c") ... endif()

变量自动求值(Variable Expansion)

if命令诞生早于${}语法,为方便起见会自动对其参数中命名的变量求值(详见 if 文档的 Variable Expansion 一节)。例如:

set(var1 OFF) set(var2 "var1") if(${var2}) # 等价于 if(var1),结果为假 if(var2) # 直接写变量名,var2 已定义且值不是假常量,结果为真

自 3.1 起(配合策略 CMP0054),用引号或括号包裹的变量名会被当作字符串而非解引用。环境变量与缓存变量不会自动求值,必须显式写$ENV{<name>}或$CACHE{<name>}。

嵌套使用与配对注意事项

if/endif块可以自由嵌套,源码的CheckNesting栈机制正是为处理任意深度的嵌套设计的。嵌套时必须保证每个if都有对应的endif:

if(BUILD_SHARED_LIBS) if(MSVC) message(STATUS "Building shared libs with MSVC") endif() else() message(STATUS "Building static libs") endif()

实践建议:

  • 统一使用endif()形式,避免带参数写法带来的拼写不一致风险;
  • 条件较长时,可用endif()加行尾注释说明闭合的是哪个条件,例如endif() # BUILD_SHARED_LIBS,但注意注释不能替代参数的逐字匹配规则;
  • 在函数(function/endfunction)与宏(macro/endmacro)内部使用if/endif时同样遵循本规则,且函数内未闭合的块会导致语法错误。

常见错误与排查方法

  1. 缺失 endif:CMake 在解析阶段通过CheckNesting检测到栈不平衡,报错信息会精确指向未闭合的if所在文件与行号;
  2. 多余的 endif:栈顶不是If/Else状态时被拒绝,提示该endif没有对应的if;
  3. endif 参数不匹配:如if(WIN32) ... endif(UNIX),ArgumentsMatch返回假,块无法闭合而报错;
  4. else 后再用 elseif:源码中Replay逻辑(cmIfCommand.cxx)会抛出 "A duplicate ELSE command" 或 "An ELSEIF command was found after an ELSE command" 的致命错误;
  5. 条件类型混淆:例如用STREQUAL与数字比较混用、未加引号的字符串被当作变量解引用,可参考上文条件表达式速查逐一核对。

调试时可用cmake --trace跟踪命令执行,if分支的实际走向会在跟踪输出中体现。

总结

endif虽只有一行语法,却是 CMake 条件逻辑的收口点。正确理解其三点核心:无参数的标准用法、仅供向后兼容且必须逐字匹配的可选参数、以及源码层面基于 Function Blocker 栈与解析期嵌套校验的双重配对机制(cmIfCommand.cxx、cmListFileCache.cxx),有助于写出结构清晰、跨版本健壮的 CMake 脚本。配合 if 命令文档 中完整的条件表达式体系,即可自如地驾驭 CMake 中一切条件分支场景。

  • 构建工具
  • 开发工具
  • CLI

【免费下载链接】CMake

Mirror of CMake upstream repository

项目地址:https://gitcode.com/gh_mirrors/cm/CMake
点击查看免费下载
上一篇:SOCD Cleaner终极指南:4种智能模式彻底解决键盘输入冲突
下一篇:Hitboxer深度解析:5大核心功能彻底解决游戏键盘输入冲突

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询