cmake_language

在 3.18 版本中新增。

对 CMake 命令调用元操作。

概要

cmake_language(CALL <command> [<arg>...])
cmake_language(EVAL CODE <code>...)
cmake_language(DEFER <options>... CALL <command> [<arg>...])
cmake_language(SET_DEPENDENCY_PROVIDER <command> SUPPORTED_METHODS <methods>...)
cmake_language(GET_MESSAGE_LOG_LEVEL <out-var>)
cmake_language(EXIT <exit-code>)
cmake_language(TRACE <boolean> ...)

简介

此命令将对内置 CMake 命令或通过 macro()function() 命令创建的命令调用元操作。

cmake_language 不会引入新的变量或策略范围。

调用命令

cmake_language(CALL <command> [<arg>...])

使用给定的参数(如果有)调用名为 <command> 的命令。例如,代码

set(message_command "message")
cmake_language(CALL ${message_command} STATUS "Hello World!")

等价于

message(STATUS "Hello World!")

注意

为确保代码的一致性,不允许使用以下命令

  • if / elseif / else / endif

  • block / endblock

  • while / endwhile

  • foreach / endforeach

  • function / endfunction

  • macro / endmacro

求值代码

cmake_language(EVAL CODE <code>...)

<code>... 作为 CMake 代码求值。

例如,以下代码:

set(A TRUE)
set(B TRUE)
set(C TRUE)
set(condition "(A AND B) OR C")

cmake_language(EVAL CODE "
  if (${condition})
    message(STATUS TRUE)
  else()
    message(STATUS FALSE)
  endif()"
)

等价于

set(A TRUE)
set(B TRUE)
set(C TRUE)
set(condition "(A AND B) OR C")

file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/eval.cmake "
  if (${condition})
    message(STATUS TRUE)
  else()
    message(STATUS FALSE)
  endif()"
)

include(${CMAKE_CURRENT_BINARY_DIR}/eval.cmake)

推迟调用

3.19 版本新增。

cmake_language(DEFER <options>... CALL <command> [<arg>...])

将对名为 <command> 的命令(及其实例参数)的调用安排在稍后执行。默认情况下,推迟的调用会像写在当前目录的 CMakeLists.txt 文件末尾一样执行,但它们即使在调用 return() 后也会运行。参数中的变量引用将在推迟的调用执行时进行求值。

选项包括

DIRECTORY <dir>

将调用安排在指定目录的末尾执行,而不是当前目录。 <dir> 可以引用源目录或其对应的二进制目录。相对路径被视为相对于当前源目录。

指定的目录必须为 CMake 所知,该目录要么是顶层目录,要么是由 add_subdirectory() 添加的目录。此外,指定的目录必须尚未完成处理。这意味着它可以是当前目录或其祖先目录之一。

ID <id>

为推迟的调用指定一个标识符。 <id> 不能为空,也不能以大写字母 A-Z 开头。 <id> 只能以如下划线(_)开头,前提是该下划线是前一个使用 ID_VAR 获取 ID 的调用自动生成的。

ID_VAR <var>

指定一个变量,用于存储推迟调用的标识符。如果未提供 ID <id>,则会生成一个新的标识符,生成的 ID 将以一个下划线(_)开头。

当前计划的推迟调用列表可以通过以下方式检索

cmake_language(DEFER [DIRECTORY <dir>] GET_CALL_IDS <var>)

这会将推迟调用的 ID 列表存储在 <var> 中,该列表是分号分隔的。ID 指的是调用被推迟到的目录范围(即它们将被执行的位置),这可能与它们被创建的范围不同。可以使用 DIRECTORY 选项来指定要检索调用 ID 的范围。如果未给出该选项,则返回当前目录范围的调用 ID。

可以通过其 ID 获取特定调用的详细信息

cmake_language(DEFER [DIRECTORY <dir>] GET_CALL <id> <var>)

这会将一个分号分隔的列表存储在 <var> 中,其中第一个元素是要调用的命令的名称,其余元素是其未求值的参数(包含的任何 ; 字符按原样包含,无法与多个参数区分)。如果多个调用被安排为具有相同的 ID,则检索第一个。如果在指定 DIRECTORY 范围(或在未指定 DIRECTORY 选项时返回的当前目录范围)中没有安排具有给定 ID 的调用,则会将空字符串存储在变量中。

推迟的调用可以通过其 ID 被取消

cmake_language(DEFER [DIRECTORY <dir>] CANCEL_CALL <id>...)

这会取消指定 DIRECTORY 范围(或在未指定 DIRECTORY 选项时返回的当前目录范围)中与任何给定 ID 匹配的所有推迟调用。未知的 ID 将被静默忽略。

推迟调用示例

例如,以下代码:

cmake_language(DEFER CALL message "${deferred_message}")
cmake_language(DEFER ID_VAR id CALL message "Canceled Message")
cmake_language(DEFER CANCEL_CALL ${id})
message("Immediate Message")
set(deferred_message "Deferred Message")

打印

Immediate Message
Deferred Message

Canceled Message 永远不会被打印,因为它被取消了。 deferred_message 变量引用直到调用点才被求值,因此可以在安排推迟调用后设置它。

为了在安排推迟调用时立即求值变量引用,请使用 cmake_language(EVAL) 进行包装。但是,请注意,参数将在推迟调用中重新求值,尽管这可以通过使用括号参数来避免。例如

set(deferred_message "Deferred Message 1")
set(re_evaluated [[${deferred_message}]])
cmake_language(EVAL CODE "
  cmake_language(DEFER CALL message [[${deferred_message}]])
  cmake_language(DEFER CALL message \"${re_evaluated}\")
")
message("Immediate Message")
set(deferred_message "Deferred Message 2")

也打印

Immediate Message
Deferred Message 1
Deferred Message 2

依赖提供者

在 3.24 版本中添加。

注意

此功能的高级介绍可以在 使用依赖项指南 中找到。

cmake_language(SET_DEPENDENCY_PROVIDER <command> SUPPORTED_METHODS <methods>...)

当调用 find_package()FetchContent_MakeAvailable() 时,该调用可能会转发给依赖提供者,然后依赖提供者有机会满足请求。如果请求是针对提供者设置时指定的 <methods> 之一,CMake 将使用一组特定于方法的参数调用提供者的 <command>。如果提供者未满足请求,或者提供者不支持请求的方法,或者未设置提供者,则使用内置的 find_package()FetchContent_MakeAvailable() 实现以常规方式满足请求。

设置提供者时,可以指定一个或多个以下值作为 <methods>

FIND_PACKAGE

提供者命令接受 find_package() 请求。

FETCHCONTENT_MAKEAVAILABLE_SERIAL

提供者命令接受 FetchContent_MakeAvailable() 请求。它期望每个依赖项一次性馈送给提供者命令,而不是一次馈送整个列表。

任何时候只能设置一个提供者。如果当调用 cmake_language(SET_DEPENDENCY_PROVIDER) 时已设置提供者,则新提供者将替换之前设置的提供者。指定的 <command> 必须在调用 cmake_language(SET_DEPENDENCY_PROVIDER) 时已经存在。作为特殊情况,将空字符串提供给 <command> 且不提供 <methods> 将丢弃任何先前设置的提供者。

依赖提供者只能在处理 CMAKE_PROJECT_TOP_LEVEL_INCLUDES 变量指定的某个文件时设置。因此,依赖提供者只能作为首次调用 project() 的一部分来设置。在该上下文之外调用 cmake_language(SET_DEPENDENCY_PROVIDER) 将导致错误。

3.30 版本新增: 如果依赖提供者也希望在对 try_compile() 进行的整个项目调用中启用,则可以设置 PROPAGATE_TOP_LEVEL_INCLUDES_TO_TRY_COMPILE 全局属性。

注意

依赖提供者的选择应始终由用户控制。为了方便起见,项目可以选择提供一个用户可以列在其 CMAKE_PROJECT_TOP_LEVEL_INCLUDES 变量中的文件,但使用此类文件应始终由用户自行决定。

提供者命令

提供者定义一个单独的 <command> 来接受请求。命令的名称应该特定于该提供者,而不是过于通用以至于其他提供者也可能使用。这使得用户可以组合他们自己的自定义提供者中的不同提供者。推荐的格式是 xxx_provide_dependency(),其中 xxx 是提供者特定的部分(例如 vcpkg_provide_dependency()conan_provide_dependency()ourcompany_provide_dependency() 等)。

xxx_provide_dependency(<method> [<method-specific-args>...])

由于某些方法需要调用范围中设置某些变量,因此提供者命令通常应该实现为宏而不是函数。这确保它不会引入新的变量范围。

CMake 传递给依赖提供者的参数取决于请求的类型。第一个参数始终是方法,它将仅是设置提供者时指定的 <methods> 之一。

FIND_PACKAGE

<method-specific-args> 将是传递给请求依赖项的 find_package() 调用中的所有内容。因此,这些 <method-specific-args> 中的第一个将始终是依赖项的名称。对于此方法,依赖项名称区分大小写,因为 find_package() 也区分大小写。

如果提供者命令满足请求,它必须设置与 find_package() 预期设置的变量相同的变量。对于名为 depName 的依赖项,如果提供者满足了请求,它必须将 depName_FOUND 设置为 true。如果提供者在未设置此变量的情况下返回,CMake 将假设请求未被满足,并将回退到内置实现。

如果提供者需要在其处理过程中调用内置的 find_package() 实现,它可以做到,通过包含 BYPASS_PROVIDER 关键字作为参数之一。

FETCHCONTENT_MAKEAVAILABLE_SERIAL

<method-specific-args> 将是传递给与请求的依赖项对应的 FetchContent_Declare() 调用中的所有内容,但有以下例外

  • 如果 SOURCE_DIRBINARY_DIR 不是原始声明参数的一部分,它们将被添加并带有默认值。

  • 如果 FETCHCONTENT_TRY_FIND_PACKAGE_MODE 设置为 NEVER,则将省略任何 FIND_PACKAGE_ARGS

  • OVERRIDE_FIND_PACKAGE 关键字始终被省略。

这些 <method-specific-args> 中的第一个将始终是依赖项的名称。对于此方法,依赖项名称不区分大小写,因为 FetchContent 也将其视为不区分大小写。

如果提供者满足请求,它应该调用 FetchContent_SetPopulated(),并将依赖项名称作为第一个参数传递。只有当提供者以与内置的 FetchContent_MakeAvailable() 命令完全相同的方式提供依赖项的源目录和构建目录时,才应向该命令提供 SOURCE_DIRBINARY_DIR 参数。

如果提供者在未为指定的依赖项调用 FetchContent_SetPopulated() 的情况下返回,CMake 将假设请求未被满足,并将回退到内置实现。

请注意,对于此方法,空参数可能很重要(例如,GIT_SUBMODULES 关键字后面的空字符串)。因此,如果将这些参数转发给另一个命令,则必须格外小心,以避免此类参数被静默丢弃。

如果设置了 FETCHCONTENT_SOURCE_DIR_<uppercaseDepName>,那么对于此方法,依赖提供者将永远不会收到对 <depName> 依赖项的请求。当用户设置此类变量时,他们明确地覆盖了从何处获取该依赖项,并承担其覆盖版本满足该依赖项的任何要求并与项目中其他使用它的部分兼容的责任。根据 FETCHCONTENT_TRY_FIND_PACKAGE_MODE 的值以及是否向 FetchContent_Declare() 提供了 OVERRIDE_FIND_PACKAGE 选项,设置 FETCHCONTENT_SOURCE_DIR_<uppercaseDepName> 也可能阻止依赖提供者收到对 find_package(depName) 调用的请求。

提供者示例

第一个示例仅拦截 find_package() 调用。提供者命令运行一个外部工具,如果该工具了解依赖项,则该工具会将相关工件复制到特定于提供者的目录中。然后,它依赖于内置实现来查找这些工件。 FetchContent_MakeAvailable() 调用将不会通过提供者。

mycomp_provider.cmake
# Always ensure we have the policy settings this provider expects
cmake_minimum_required(VERSION 3.24)

set(MYCOMP_PROVIDER_INSTALL_DIR ${CMAKE_BINARY_DIR}/mycomp_packages
  CACHE PATH "The directory this provider installs packages to"
)
# Tell the built-in implementation to look in our area first, unless
# the find_package() call uses NO_..._PATH options to exclude it
list(APPEND CMAKE_MODULE_PATH ${MYCOMP_PROVIDER_INSTALL_DIR}/cmake)
list(APPEND CMAKE_PREFIX_PATH ${MYCOMP_PROVIDER_INSTALL_DIR})

macro(mycomp_provide_dependency method package_name)
  execute_process(
    COMMAND some_tool ${package_name} --installdir ${MYCOMP_PROVIDER_INSTALL_DIR}
    COMMAND_ERROR_IS_FATAL ANY
  )
endmacro()

cmake_language(
  SET_DEPENDENCY_PROVIDER mycomp_provide_dependency
  SUPPORTED_METHODS FIND_PACKAGE
)

用户通常会像这样使用上述文件

cmake -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=/path/to/mycomp_provider.cmake ...

下一个示例演示了一个接受两种方法的提供者,但只处理一个特定的依赖项。它强制使用 FetchContent 来提供 Google Test,但将所有其他依赖项留给 CMake 的内置实现来满足。它接受几个不同的名称,这演示了一种解决方法,用于处理硬编码了添加此特定依赖项到构建的非标准或不理想方式的项目。该示例还演示了如何使用 list() 命令来保留可能被 FetchContent_MakeAvailable() 调用覆盖的变量。

mycomp_provider.cmake
cmake_minimum_required(VERSION 3.24)

# Because we declare this very early, it will take precedence over any
# details the project might declare later for the same thing
include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        e2239ee6043f73722e7aa812a459f54a28552929 # release-1.11.0
)

# Both FIND_PACKAGE and FETCHCONTENT_MAKEAVAILABLE_SERIAL methods provide
# the package or dependency name as the first method-specific argument.
macro(mycomp_provide_dependency method dep_name)
  if("${dep_name}" MATCHES "^(gtest|googletest)$")
    # Save our current command arguments in case we are called recursively
    list(APPEND mycomp_provider_args ${method} ${dep_name})

    # This will forward to the built-in FetchContent implementation,
    # which detects a recursive call for the same thing and avoids calling
    # the provider again if dep_name is the same as the current call.
    FetchContent_MakeAvailable(googletest)

    # Restore our command arguments
    list(POP_BACK mycomp_provider_args dep_name method)

    # Tell the caller we fulfilled the request
    if("${method}" STREQUAL "FIND_PACKAGE")
      # We need to set this if we got here from a find_package() call
      # since we used a different method to fulfill the request.
      # This example assumes projects only use the gtest targets,
      # not any of the variables the FindGTest module may define.
      set(${dep_name}_FOUND TRUE)
    elseif(NOT "${dep_name}" STREQUAL "googletest")
      # We used the same method, but were given a different name to the
      # one we populated with. Tell the caller about the name it used.
      FetchContent_SetPopulated(${dep_name}
        SOURCE_DIR "${googletest_SOURCE_DIR}"
        BINARY_DIR "${googletest_BINARY_DIR}"
      )
    endif()
  endif()
endmacro()

cmake_language(
  SET_DEPENDENCY_PROVIDER mycomp_provide_dependency
  SUPPORTED_METHODS
    FIND_PACKAGE
    FETCHCONTENT_MAKEAVAILABLE_SERIAL
)

最后一个示例演示了如何修改传递给 find_package() 调用的参数。它强制所有此类调用都具有 QUIET 关键字。它使用 BYPASS_PROVIDER 关键字来防止对同一依赖项进行递归调用提供者命令。

mycomp_provider.cmake
cmake_minimum_required(VERSION 3.24)

macro(mycomp_provide_dependency method)
  find_package(${ARGN} BYPASS_PROVIDER QUIET)
endmacro()

cmake_language(
  SET_DEPENDENCY_PROVIDER mycomp_provide_dependency
  SUPPORTED_METHODS FIND_PACKAGE
)

获取当前消息日志级别

在 3.25 版本中新增。

cmake_language(GET_MESSAGE_LOG_LEVEL <output_variable>)

将当前 message() 日志级别写入给定的 <output_variable>

有关可能的日志级别,请参见 message()

当前消息日志级别可以通过 cmake(1) 程序的 --log-level 命令行选项或通过 CMAKE_MESSAGE_LOG_LEVEL 变量来设置。

如果同时设置了命令行选项和变量,则命令行选项具有优先权。如果两者都未设置,则返回默认日志级别。

终止脚本

在版本 3.29 中添加。

cmake_language(EXIT <exit-code>)

终止当前 cmake -P 脚本并以 <exit-code> 退出。

此命令仅在 脚本模式 下有效。如果在该上下文之外使用,它将导致致命错误。

<exit-code> 应为非负数。如果 <exit-code> 为负数,则行为不确定(例如,在 Windows 上,错误代码 -1 变为 0xffffffff,在 Linux 上,它变为 255)。大于 255 的退出代码可能不受底层 shell 或平台支持,并且某些 shell 可能会特殊解释大于 125 的值。因此,建议仅在 0 到 125 的范围内指定 <exit-code>

跟踪控制

版本 4.2 中添加。

cmake_language(TRACE ON [EXPAND])
cmake_language(TRACE OFF)

TRACE 子命令控制当前进程中已执行 CMake 命令和宏的运行时跟踪。启用后,跟踪输出将以与使用 cmake --tracecmake --trace-expand 命令行选项启动 CMake 时相同的格式进行写入。

跟踪范围是可嵌套的。多个 TRACE ON 调用可能同时处于活动状态,并且每个 TRACE OFF 调用会解除一个嵌套级别。

如果 CMake 使用 cmake --tracecmake --trace-expand 运行,这些选项将覆盖并全局强制跟踪,无论 cmake_language(TRACE OFF) 调用如何。在这种情况下,该命令仍可调用,但对跟踪状态没有影响。