CMake 秘籍(四)(3)

简介: CMake 秘籍(四)

CMake 秘籍(四)(2)https://developer.aliyun.com/article/1525211

准备

遵循前一个食谱的推荐实践,我们将在一个模块(set_compiler_flag.cmake)中定义函数,包含该模块,然后调用该函数。该模块包含以下代码,我们将在后面讨论:

include(CheckCCompilerFlag)
include(CheckCXXCompilerFlag)
include(CheckFortranCompilerFlag)
function(set_compiler_flag _result _lang)
  # build a list of flags from the arguments
  set(_list_of_flags)
# also figure out whether the function
  # is required to find a flag
  set(_flag_is_required FALSE)
  foreach(_arg IN ITEMS ${ARGN})
    string(TOUPPER "${_arg}" _arg_uppercase)
    if(_arg_uppercase STREQUAL "REQUIRED")
      set(_flag_is_required TRUE)
    else()
      list(APPEND _list_of_flags "${_arg}")
    endif()
  endforeach()
  set(_flag_found FALSE)
  # loop over all flags, try to find the first which works
  foreach(flag IN ITEMS ${_list_of_flags})
    unset(_flag_works CACHE)
    if(_lang STREQUAL "C")
      check_c_compiler_flag("${flag}" _flag_works)
    elseif(_lang STREQUAL "CXX")
      check_cxx_compiler_flag("${flag}" _flag_works)
    elseif(_lang STREQUAL "Fortran")
      check_Fortran_compiler_flag("${flag}" _flag_works)
    else()
      message(FATAL_ERROR "Unknown language in set_compiler_flag: ${_lang}")
    endif()
    # if the flag works, use it, and exit
    # otherwise try next flag
    if(_flag_works)
      set(${_result} "${flag}" PARENT_SCOPE)
      set(_flag_found TRUE)
      break()
    endif()
  endforeach()
  # raise an error if no flag was found
  if(_flag_is_required AND NOT _flag_found)
    message(FATAL_ERROR "None of the required flags were supported")
  endif()
endfunction()

如何操作

这是我们如何在CMakeLists.txt中使用set_compiler_flag函数的方法:

  1. 在前言中,我们定义了最低 CMake 版本、项目名称和支持的语言(在这种情况下,C 和 C++):
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-03 LANGUAGES C CXX)
  1. 然后,我们明确地包含set_compiler_flag.cmake
include(set_compiler_flag.cmake)
  1. 然后,我们尝试一组 C 标志:
set_compiler_flag(
  working_compile_flag C REQUIRED
  "-foo"             # this should fail
  "-wrong"           # this should fail
  "-wrong"           # this should fail
  "-Wall"            # this should work with GNU
  "-warn all"        # this should work with Intel
  "-Minform=inform"  # this should work with PGI
  "-nope"            # this should fail
  )
message(STATUS "working C compile flag: ${working_compile_flag}")
  1. 我们尝试一组 C++标志:
set_compiler_flag(
  working_compile_flag CXX REQUIRED
  "-foo"    # this should fail
  "-g"      # this should work with GNU, Intel, PGI
  "/RTCcsu" # this should work with MSVC
  )
message(STATUS "working CXX compile flag: ${working_compile_flag}")
  1. 现在,我们可以配置项目并验证输出。仅显示相关输出,输出可能因编译器而异:
$ mkdir -p build
$ cd build
$ cmake ..
-- ...
-- Performing Test _flag_works
-- Performing Test _flag_works - Failed
-- Performing Test _flag_works
-- Performing Test _flag_works - Failed
-- Performing Test _flag_works
-- Performing Test _flag_works - Failed
-- Performing Test _flag_works
-- Performing Test _flag_works - Success
-- working C compile flag: -Wall
-- Performing Test _flag_works
-- Performing Test _flag_works - Failed
-- Performing Test _flag_works
-- Performing Test _flag_works - Success
-- working CXX compile flag: -g
-- ...

工作原理

我们在这里使用的模式是:

  1. 定义一个函数或宏并将其放入模块中
  2. 包含模块
  3. 调用函数或宏

从输出中,我们可以看到代码检查列表中的每个标志,一旦检查成功,它就会打印出成功的编译标志。让我们看看set_compiler_flag.cmake模块内部。这个模块反过来包含了三个模块:

include(CheckCCompilerFlag)
include(CheckCXXCompilerFlag)
include(CheckFortranCompilerFlag)

这些是标准的 CMake 模块,CMake 将在${CMAKE_MODULE_PATH}中找到它们。这些模块提供了check_c_compiler_flagcheck_cxx_compiler_flagcheck_fortran_compiler_flag宏,分别。然后是函数定义:

function(set_compiler_flag _result _lang)
  ...
endfunction()

set_compiler_flag函数期望两个参数,我们称它们为_result(这将保存成功的编译标志或空字符串"")和_lang(指定语言:C、C++或 Fortran)。

我们希望能够这样调用函数:

set_compiler_flag(working_compile_flag C REQUIRED "-Wall" "-warn all")

这个调用有五个参数,但函数头只期望两个。这意味着REQUIRED"-Wall""-warn all"将被放入${ARGN}中。从${ARGN}中,我们首先使用foreach构建一个标志列表。同时,我们从标志列表中过滤掉REQUIRED,并使用它来设置_flag_is_required

# build a list of flags from the arguments
set(_list_of_flags)
# also figure out whether the function
# is required to find a flag
set(_flag_is_required FALSE)
foreach(_arg IN ITEMS ${ARGN})
  string(TOUPPER "${_arg}" _arg_uppercase)
  if(_arg_uppercase STREQUAL "REQUIRED")
    set(_flag_is_required TRUE)
  else()
    list(APPEND _list_of_flags "${_arg}")
  endif()
endforeach()

现在,我们将循环遍历${_list_of_flags},尝试每个标志,如果_flag_works被设置为TRUE,我们将_flag_found设置为TRUE并终止进一步的搜索:

set(_flag_found FALSE)
# loop over all flags, try to find the first which works
foreach(flag IN ITEMS ${_list_of_flags})
  unset(_flag_works CACHE)
  if(_lang STREQUAL "C")
    check_c_compiler_flag("${flag}" _flag_works)
  elseif(_lang STREQUAL "CXX")
    check_cxx_compiler_flag("${flag}" _flag_works)
  elseif(_lang STREQUAL "Fortran")
    check_Fortran_compiler_flag("${flag}" _flag_works)
  else()
    message(FATAL_ERROR "Unknown language in set_compiler_flag: ${_lang}")
  endif()
  # if the flag works, use it, and exit
  # otherwise try next flag
  if(_flag_works)
    set(${_result} "${flag}" PARENT_SCOPE)
    set(_flag_found TRUE)
    break()
  endif()
endforeach()

unset(_flag_works CACHE)这一行是为了确保check_*_compiler_flag的结果不会在多次调用中使用相同的_flag_works结果变量时被缓存。

如果找到标志并且_flag_works被设置为TRUE,我们定义映射到_result的变量:

set(${_result} "${flag}" PARENT_SCOPE)

这需要使用PARENT_SCOPE,因为我们希望修改的变量在函数体外打印和使用。此外,请注意我们是如何使用${_result}语法从父作用域传递的变量_result进行解引用的。这是必要的,以确保在调用函数时,无论其名称如何,都将工作标志设置为从父作用域传递的变量的值。如果没有找到标志并且提供了REQUIRED关键字,我们通过错误消息停止配置:

# raise an error if no flag was found
if(_flag_is_required AND NOT _flag_found)
  message(FATAL_ERROR "None of the required flags were supported")
endif()

还有更多

我们可以通过宏来完成这项任务,但使用函数,我们可以更好地控制作用域。我们知道,函数只能修改结果变量。

此外,请注意,某些标志需要在编译和链接时都设置,通过为check__compiler_flag函数设置CMAKE_REQUIRED_FLAGS来正确报告成功。正如我们在第五章,配置时间和构建时间操作,第 7 个配方,探测编译标志中讨论的,这是针对 sanitizers 的情况。

定义一个带有命名参数的函数或宏

本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-07/recipe-04找到,并包含一个 C++示例。该食谱适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。

在前面的食谱中,我们探索了函数和宏并使用了位置参数。在本食谱中,我们将定义一个带有命名参数的函数。我们将增强来自食谱 1 的示例,即使用函数和宏重用代码,并使用以下方式定义测试:

add_catch_test(short 1.5)

我们将能够调用以下内容:

add_catch_test(
  NAME
    short
  LABELS
    short
    cpp_test
  COST
    1.5
  )

准备就绪

我们将使用来自食谱 1 的示例,即使用函数和宏重用代码,并保持 C++源文件不变,文件树基本相同:

.
├── cmake
│   └── testing.cmake
├── CMakeLists.txt
├── src
│   ├── CMakeLists.txt
│   ├── main.cpp
│   ├── sum_integers.cpp
│   └── sum_integers.hpp
└── tests
    ├── catch.hpp
    ├── CMakeLists.txt
    └── test.cpp

如何做到这一点

我们将在 CMake 代码中引入小的修改,如下所示:

  1. 由于我们将包含位于cmake下的模块,因此在顶层CMakeLists.txt中只添加了一行额外的代码:
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
  1. 我们保持src/CMakeLists.txt不变。
  2. tests/CMakeLists.txt中,我们将add_catch_test函数定义移动到cmake/testing.cmake,并定义了两个测试:
add_executable(cpp_test test.cpp)
target_link_libraries(cpp_test sum_integers)
include(testing)
add_catch_test(
  NAME
    short
  LABELS
    short
    cpp_test
  COST
    1.5
  )
add_catch_test(
  NAME
    long
  LABELS
    long
    cpp_test
  COST
    2.5
  )
  1. add_catch_test函数现在在cmake/testing.cmake中定义:
function(add_catch_test)
  set(options)
  set(oneValueArgs NAME COST)
  set(multiValueArgs LABELS DEPENDS REFERENCE_FILES)
  cmake_parse_arguments(add_catch_test
    "${options}"
    "${oneValueArgs}"
    "${multiValueArgs}"
    ${ARGN}
    )
  message(STATUS "defining a test ...")
  message(STATUS " NAME: ${add_catch_test_NAME}")
  message(STATUS " LABELS: ${add_catch_test_LABELS}")
  message(STATUS " COST: ${add_catch_test_COST}")
  message(STATUS " REFERENCE_FILES: ${add_catch_test_REFERENCE_FILES}")
  add_test(
    NAME
      ${add_catch_test_NAME}
    COMMAND
      $<TARGET_FILE:cpp_test>
      [${add_catch_test_NAME}] --success --out
      ${PROJECT_BINARY_DIR}/tests/${add_catch_test_NAME}.log --durations yes
    WORKING_DIRECTORY
      ${CMAKE_CURRENT_BINARY_DIR}
    )
  set_tests_properties(${add_catch_test_NAME}
    PROPERTIES
      LABELS "${add_catch_test_LABELS}"
    )
  if(add_catch_test_COST)
    set_tests_properties(${add_catch_test_NAME}
      PROPERTIES
        COST ${add_catch_test_COST}
      )
  endif()
  if(add_catch_test_DEPENDS)
    set_tests_properties(${add_catch_test_NAME}
      PROPERTIES
        DEPENDS ${add_catch_test_DEPENDS}
      )
  endif()
  if(add_catch_test_REFERENCE_FILES)
    file(
      COPY
        ${add_catch_test_REFERENCE_FILES}
      DESTINATION
        ${CMAKE_CURRENT_BINARY_DIR}
      )
  endif()
endfunction()
  1. 我们准备好测试输出。首先,我们配置以下内容:
$ mkdir -p build
$ cd build
$ cmake ..
-- ...
-- defining a test ...
--     NAME: short
--     LABELS: short;cpp_test
--     COST: 1.5
--     REFERENCE_FILES: 
-- defining a test ...
--     NAME: long
--     LABELS: long;cpp_test
--     COST: 2.5
--     REFERENCE_FILES:
-- ...
  1. 然后,编译并测试代码:
$ cmake --build .
$ ctest

它是如何工作的

本食谱中的新内容是命名参数,因此我们可以专注于cmake/testing.cmake模块。CMake 提供了cmake_parse_arguments命令,我们用函数名(add_catch_test)调用它,选项(在我们的例子中没有),单值参数(这里,NAMECOST),以及多值参数(这里,LABELSDEPENDS,和REFERENCE_FILES):

function(add_catch_test)
  set(options)
  set(oneValueArgs NAME COST)
  set(multiValueArgs LABELS DEPENDS REFERENCE_FILES)
  cmake_parse_arguments(add_catch_test
    "${options}"
    "${oneValueArgs}"
    "${multiValueArgs}"
    ${ARGN}
    )
  ...
endfunction()

cmake_parse_arguments命令解析选项和参数,并在我们的情况下定义以下内容:

  • add_catch_test_NAME
  • add_catch_test_COST
  • add_catch_test_LABELS
  • add_catch_test_DEPENDS
  • add_catch_test_REFERENCE_FILES

然后我们可以在函数内部查询和使用这些变量。这种方法为我们提供了实现具有更健壮接口和更易读的函数/宏调用的函数和宏的机会。

还有更多

选项关键字(在本示例中未使用)由cmake_parse_arguments定义为TRUEFALSE。对add_catch_test函数的进一步增强可能是提供测试命令作为命名参数,为了更简洁的示例,我们省略了这一点。

cmake_parse_arguments命令在 CMake 3.5 发布之前在CMakeParseArguments.cmake模块中可用。因此,可以通过在cmake/testing.cmake模块文件的顶部使用include(CMakeParseArguments)命令来使本食谱与早期版本的 CMake 兼容。

重新定义函数和宏

本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-07/recipe-05找到。该食谱适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。

我们提到过,模块包含不应该用作函数调用,因为模块可能会被(意外地)多次包含。在本食谱中,我们将编写我们自己的简单包含保护,如果我们尝试多次包含模块,它将警告我们。内置的include_guard命令自 CMake 3.10 版本起可用,并且行为类似于 C/C++头文件的#pragma once。对于这个版本的 CMake,我们将讨论和演示如何重新定义函数和宏。我们将展示如何检查 CMake 版本,对于 3.10 以下的版本,我们将使用我们自己的自定义包含保护。

准备工作

在本例中,我们将使用三个文件:

.
├── cmake
│   ├── custom.cmake
│   └── include_guard.cmake
└── CMakeLists.txt

自定义的custom.cmake模块包含以下代码:

include_guard(GLOBAL)
message(STATUS "custom.cmake is included and processed")

稍后我们将讨论cmake/include_guard.cmakeCMakeLists.txt

如何操作

这是我们的三个 CMake 文件的逐步分解:

  1. 在本食谱中,我们不会编译任何代码,因此我们的语言要求是NONE
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-05 LANGUAGES NONE)
  1. 然后我们定义一个include_guard宏,我们将其放置在一个单独的模块中:
# (re)defines include_guard
include(cmake/include_guard.cmake)
  1. cmake/include_guard.cmake文件包含以下内容(我们稍后将详细讨论):
macro(include_guard)
  if (CMAKE_VERSION VERSION_LESS "3.10")
    # for CMake below 3.10 we define our
    # own include_guard(GLOBAL)
    message(STATUS "calling our custom include_guard")
    # if this macro is called the first time
    # we start with an empty list
    if(NOT DEFINED included_modules)
        set(included_modules)
    endif()
    if ("${CMAKE_CURRENT_LIST_FILE}" IN_LIST included_modules)
      message(WARNING "module ${CMAKE_CURRENT_LIST_FILE} processed more than once")
    endif()
    list(APPEND included_modules ${CMAKE_CURRENT_LIST_FILE})
  else()
    # for CMake 3.10 or higher we augment
    # the built-in include_guard
    message(STATUS "calling the built-in include_guard")
    _include_guard(${ARGV})
  endif()
endmacro()
  1. 在主CMakeLists.txt中,然后我们模拟意外地两次包含自定义模块:
include(cmake/custom.cmake)
include(cmake/custom.cmake)
  1. 最后,我们使用以下命令进行配置:
$ mkdir -p build
$ cd build
$ cmake ..
  1. 使用 CMake 3.10 及以上版本的结果如下:
-- calling the built-in include_guard
-- custom.cmake is included and processed
-- calling the built-in include_guard
  1. 使用 CMake 3.10 以下版本的结果如下:
-- calling our custom include_guard
-- custom.cmake is included and processed
-- calling our custom include_guard
CMake Warning at cmake/include_guard.cmake:7 (message):
  module
  /home/user/example/cmake/custom.cmake
  processed more than once
Call Stack (most recent call first):
  cmake/custom.cmake:1 (include_guard)
  CMakeLists.txt:12 (include)

它是如何工作的

我们的include_guard宏包含两个分支,一个用于 CMake 3.10 以下版本,另一个用于 CMake 3.10 及以上版本:

macro(include_guard)
  if (CMAKE_VERSION VERSION_LESS "3.10")
    # ...
  else()
    # ...
  endif()
endmacro()

如果 CMake 版本低于 3.10,我们进入第一个分支,内置的include_guard不可用,因此我们定义我们自己的:

message(STATUS "calling our custom include_guard")
# if this macro is called the first time
# we start with an empty list
if(NOT DEFINED included_modules)
    set(included_modules)
endif()
if ("${CMAKE_CURRENT_LIST_FILE}" IN_LIST included_modules)
  message(WARNING "module ${CMAKE_CURRENT_LIST_FILE} processed more than once")
endif()
list(APPEND included_modules ${CMAKE_CURRENT_LIST_FILE})

如果宏第一次被调用,那么included_modules变量未定义,因此我们将其设置为空列表。然后我们检查${CMAKE_CURRENT_LIST_FILE}是否是included_modules列表的元素。如果是,我们发出警告。如果不是,我们将${CMAKE_CURRENT_LIST_FILE}添加到此列表中。在 CMake 输出中,我们可以验证第二次包含自定义模块确实会导致警告。

对于 CMake 3.10 及以上的情况则不同;在这种情况下,存在一个内置的include_guard,我们用我们自己的宏接收的参数调用它:

macro(include_guard)
  if (CMAKE_VERSION VERSION_LESS "3.10")
    # ...
  else()
    message(STATUS "calling the built-in include_guard")
    _include_guard(${ARGV})
  endif()
endmacro()

在这里,_include_guard(${ARGV})指向内置的include_guard。在这种情况下,我们通过添加自定义消息(“调用内置的include_guard”)来增强内置命令。这种模式为我们提供了一种重新定义自己的或内置的函数和宏的机制。这在调试或记录目的时可能很有用。

这种模式可能很有用,但应谨慎应用,因为 CMake 不会警告宏或函数的重新定义。

弃用函数、宏和变量

本示例的代码可在 github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-07/recipe-06 获取。该示例适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。

弃用是在项目发展过程中向开发者发出信号的重要机制,表明某个函数、宏或变量将在未来的某个时候被移除或替换。在一定时期内,该函数、宏或变量将继续可用,但会发出警告,最终可以升级为错误。

准备就绪

我们将从以下 CMake 项目开始:

cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-06 LANGUAGES NONE)
macro(custom_include_guard)
  if(NOT DEFINED included_modules)
    set(included_modules)
  endif()
  if ("${CMAKE_CURRENT_LIST_FILE}" IN_LIST included_modules)
    message(WARNING "module ${CMAKE_CURRENT_LIST_FILE} processed more than once")
  endif()
  list(APPEND included_modules ${CMAKE_CURRENT_LIST_FILE})
endmacro()
include(cmake/custom.cmake)
message(STATUS "list of all included modules: ${included_modules}")

这段代码定义了一个自定义的包含保护,包含了一个自定义模块(与前一个示例相同的模块),并打印了所有包含的模块列表。对于 CMake 3.10 及以上版本,我们知道从之前的示例中有一个内置的 include_guard。但不是简单地移除 custom_include_guard${included_modules},我们将通过弃用警告来弃用宏和变量,这样在某个时刻我们可以将其转换为 FATAL_ERROR,使代码停止并强制开发者切换到内置命令。

如何操作

弃用函数、宏和变量可以按如下方式进行:

  1. 首先,我们定义一个函数,用于弃用变量:
function(deprecate_variable _variable _access)
  if(_access STREQUAL "READ_ACCESS")
    message(DEPRECATION "variable ${_variable} is deprecated")
  endif()
endfunction()
  1. 然后,如果 CMake 版本大于 3.9,我们重新定义 custom_include_guard 并将 variable_watch 附加到 included_modules
if (CMAKE_VERSION VERSION_GREATER "3.9")
  # deprecate custom_include_guard
  macro(custom_include_guard)
    message(DEPRECATION "custom_include_guard is deprecated - use built-in include_guard instead")
    _custom_include_guard(${ARGV})
  endmacro()
  # deprecate variable included_modules
  variable_watch(included_modules deprecate_variable)
endif()
  1. 在 CMake 版本低于 3.10 的项目中配置会产生以下结果:
$ mkdir -p build
$ cd build
$ cmake ..
-- custom.cmake is included and processed
-- list of all included modules: /home/user/example/cmake/custom.cmake
  1. CMake 3.10 及以上版本将产生预期的弃用警告:
CMake Deprecation Warning at CMakeLists.txt:26 (message):
  custom_include_guard is deprecated - use built-in include_guard instead
Call Stack (most recent call first):
  cmake/custom.cmake:1 (custom_include_guard)
  CMakeLists.txt:34 (include)
-- custom.cmake is included and processed
CMake Deprecation Warning at CMakeLists.txt:19 (message):
  variable included_modules is deprecated
Call Stack (most recent call first):
  CMakeLists.txt:9999 (deprecate_variable)
  CMakeLists.txt:36 (message)
-- list of all included modules: /home/user/example/cmake/custom.cmake

CMake 秘籍(四)(4)https://developer.aliyun.com/article/1525216

相关文章
|
6月前
|
编译器 Linux C语言
CMake 秘籍(二)(2)
CMake 秘籍(二)
51 2
|
6月前
|
编译器 Shell
CMake 秘籍(八)(3)
CMake 秘籍(八)
39 2
|
6月前
|
Linux iOS开发 C++
CMake 秘籍(六)(3)
CMake 秘籍(六)
47 1
|
6月前
|
Linux API iOS开发
CMake 秘籍(六)(1)
CMake 秘籍(六)
44 1
|
6月前
|
消息中间件 Unix C语言
CMake 秘籍(二)(5)
CMake 秘籍(二)
131 1
|
6月前
|
Shell Linux C++
CMake 秘籍(六)(4)
CMake 秘籍(六)
49 1
|
6月前
|
测试技术 C++
CMake 秘籍(四)(5)
CMake 秘籍(四)
28 0
|
6月前
|
编译器 Linux C++
CMake 秘籍(二)(1)
CMake 秘籍(二)
36 0
|
6月前
|
并行计算 关系型数据库 编译器
CMake 秘籍(七)(3)
CMake 秘籍(七)
87 0
|
6月前
|
并行计算 编译器 Linux
CMake 秘籍(二)(3)
CMake 秘籍(二)
32 0