【学习笔记】Mastering CMake (九)—— 系统检查

前言:

学习笔记,随时更新。如有谬误,欢迎指正。


说明:

  1. 红色字体为较为重要部分。
  2. 绿色字体为个人理解部分。

9 系统检查

本章将描述如何使用 CMake 来检查构建软件的系统环境。这是创建跨平台应用程序或库的关键因素。它涵盖了如何查找和使用系统和用户安装的头文件和库。它还介绍了 CMake 的一些更高级的特性,包括 try_compiletry_run 命令。这些命令是非常强大的工具,用于确定承载软件的系统和编译器的功能。

9.1 使用头文件和库

许多 C 和 C++ 程序依赖于外部库。然而,当涉及到编译和链接项目的实际方面时,利用现有库对开发人员和用户来说都是困难的。只要软件构建在不是开发它的系统的其他系统上时问题就会出现。当库和头文件没有安装在新计算机上的相同位置,构建系统无法找到它们时,关于库和头文件所在位置的假设变得显而易见。 CMake 有许多特性来帮助开发人员将外部软件库集成到项目中。

与这种类型的集成最相关的 CMake 命令是 find_filefind_libraryfind_pathfind_programfind_package 命令。对于大多数 C 和 C++ 库, find_libraryfind_path 的组合就足以编译和链接到已安装的库。 find_library 可用于定位或允许用户定位库,而 find_path 可用于从项目中查找具有代表性的包含文件的路径。例如,如果想链接到 tiff 库,可以在 CMakeLists.txt 文件中使用以下命令:

# 在一些标准位置查找 libtiff 
find_library(TIFF_LIBRARY
             NAMES tiff tiff2
             PATHS /usr/local/lib /usr/lib
            )

# 在一些标准位置查找 tiff.h
find_path(TIFF_INCLUDES tiff.h
           /usr/local/include
           /usr/include
          )

add_executable(mytiff mytiff.c )

target_link_libraries(mytiff ${TIFF_LIBRARY})

target_include_directories(mytiff ${TIFF_INCLUDES})

使用的第一个命令是 find_library ,在本例中,它将查找名称为 tiff 或 tiff2 的库。 find_library 命令只需要库的基本名称,不需要任何特定于平台的前缀或后缀,例如 .lib 和 .dll 。当 CMake 试图查找库名称时,将自动为运行 CMake 的系统添加相应的前缀和后缀。所有 find_* 命令都将查找 PATH 环境变量。此外,这些命令允许在 PATHS 标记参数后面列出附加的搜索路径作为参数。除了支持标准路径之外,还可以使用 Windows 注册表项和环境变量构造搜索路径。注册表项的语法如下:

[HKEY_CURRENT_USER\\Software\\Kitware\\Path;Build1]

由于软件可以安装在许多不同的地方,所以 CMake 不可能每次都找到库,但是应该涵盖大多数标准安装。 find_* 命令自动创建一个缓存变量,以便用户可以从 CMake GUI 中覆盖或指定位置。这样,如果 CMake 无法定位它正在寻找的文件,用户仍然有机会指定它们。如果 CMake 没有找到一个文件,该值设置为 VAR-NOTFOUND 。这个值告诉 CMake ,它应该在每次运行 CMake 的配置步骤时继续查找。注意,if 语句中, VAR-NOTFOUND 的值将计算为 false

下一个使用的命令是 find_path ,这是一个通用命令,在本例中用于从库中定位头文件。头文件和库通常安装在不同的位置,编译和链接使用它们的程序都需要这两个位置。 find_path 的用法类似于 find_library ,(使用)它需要一个名称和一个搜索路径的列表。

CMakeLists 文件的其余部分可以使用 find_* 命令创建的变量。可以在不检查有效值的情况下使用变量,因为如果没有设置所需的变量, CMake 将打印错误消息通知用户。然后,用户可以设置缓存值并重新配置,直到消息消失。如果找不到库, CMakeLists 文件可以使用 if 命令使用替代库或选项来构建没有库的项目。

从上面的例子中,你可以看到如何使用 find_* 命令帮助你的软件在各种系统上编译。值得注意的是, find_* 命令搜索从第一个参数和第一个路径开始的匹配,因此在列出路径和库名称时,首先列出首选路径和名称。如果一个库有多个版本,并且你更喜欢 tiff 而不是tiff2,请确保按此顺序列出它们。

9.2 系统属性

尽管在 C 和 C++ 代码中在预处理器 ifdef 指令中添加特定于平台的代码是一种常见的做法,但为了最大程度的可移植性,应该避免这样做。软件不应该转到具有 ifdefs 的特定平台,而应该转到由一组特性组成的规范系统。针对特定系统编写代码会降低软件的可移植性,因为系统和它们支持的功能会随着时间而变化,甚至会在不同系统之间发生变化。过去在某个平台上无法使用的功能,将来可能成为该平台所必需的功能。以下代码片段说明了向规范系统编码向特定系统编码之间的区别:

// 针对特性进行编码
#ifdef HAS_FOOBAR_CALL
  foobar();
#else
  myfoobar();
#endif

// 针对特定平台进行编码
#if defined(SUN) && defined(HPUX) && !defined(GNUC)
  foobar();
#else
  myfoobar();
#endif

第二种方法的问题是,在编译软件的每个新平台上,代码都必须修改。例如,未来版本的 SUN 可能不再有 foobar 调用。使用 HAS_FOOBAR_CALL 方法,只要 HAS_FOOBAR_CALL 定义正确,软件就可以工作,这就是 CMake 可以帮忙的地方。通过使用 try_compiletry_run 命令, CMake 可以正确和自动地定义 HAS_FOOBAR_CALL 。这些命令可用于在 CMake 配置步骤中编译和运行小型测试程序。测试程序将被发送到编译器,编译器将用其来构建项目,如果出现错误,则可以禁用该特性。这些命令要求你编写一个小型 C 或 C++ 程序来测试该特性。例如,要测试系统上是否提供了 foobar 调用,请尝试编译一个使用 foobar 的简单程序。首先编写简单的测试程序(在本例中是 testNeedFoobar.c ),然后向 CMakeLists 文件添加 CMake 调用,以尝试编译该代码。如果编译成功,那么 HAS_FOOBAR_CALL 将被设置为 true 。

// --- testNeedFoobar.c -----

#include <foobar.h>
main()
{
  foobar();
}
# --- testNeedFoobar.cmake ---

try_compile (HAS_FOOBAR_CALL
  ${CMAKE_BINARY_DIR}
  ${PROJECT_SOURCE_DIR}/testNeedFoobar.c
  )

现在 HAS_FOOBAR_CALL 已经在 CMake 中正确设置好了,你可以通过 target_compile_definitions 命令在源代码中使用它。另外,也可以配置一个头文件。这将在 如何配置一个头文件 中进一步讨论。

有时编译一个测试程序是不够的。在某些情况下,你实际上可能希望编译并运行一个程序来获得它的输出。一个很好的例子是测试机器的字节顺序。下面的示例展示了如何编写一个小程序, CMake 将编译并运行该程序来确定机器的字节顺序。

// ---- TestByteOrder.c ------

int main () {
  /* Are we most significant byte first or last */
  union
  {
    long l;
    char c[sizeof (long)];
  } u;
  u.l = 1;
  exit (u.c[sizeof (long) - 1] == 1);
}
# ----- TestByteOrder.cmake-----

try_run(RUN_RESULT_VAR
  COMPILE_RESULT_VAR
  ${CMAKE_BINARY_DIR}
  ${PROJECT_SOURCE_DIR}/Modules/TestByteOrder.c
  OUTPUT_VARIABLE OUTPUT
  )

运行的返回结果将存入 RUN_RESULT_VAR ,编译的结果将存入 COMPILE_RESULT_VAR ,运行的任何输出将存入 OUTPUT 。你可以使用这些变量向项目的用户报告调试信息。

对于小型测试程序,可以使用带 WRITE 选项的 file 命令从 CMakeLists 文件创建源文件。下面的示例测试 C 编译器,以验证它是否可以运行。

file(WRITE
  ${CMAKE_BINARY_DIR}/CMakeTmp/testCCompiler.c
  "int main(){return 0;}"
  )

try_compile(CMAKE_C_COMPILER_WORKS
  ${CMAKE_BINARY_DIR}
  ${CMAKE_BINARY_DIR}/CMakeTmp/testCCompiler.c
  OUTPUT_VARIABLE OUTPUT
  )

对于更高级的 try_compiletry_run 操作,可能需要将标识传递给编译器或 CMake 。这两个命令都支持可选参数 CMAKE_FLAGS 和 COMPILE_DEFINITIONS 。 CMAKE_FLAGS 可以用来传递 -DVAR:TYPE=VALUE 标志给 CMake 。 COMPILE_DEFINITIONS 的值直接传递给编译器命令行。

cmake-modules 中有几个预定义的试运行和试编译的宏,下面列出了其中一些。这些模块允许执行一些常见的检查,而不必为每个测试创建源文件。(想要获取详细说明或者想看这些宏是如何工作的,请看你的 CMake 安装目录下 CMake/Module 目录中这宏的实现文件。)这些模块将查看 CMAKE_REQUIRED_FLAGS 和 CMAKE_REQUIRED_LIBRARIES 变量的当前值,以向测试添加额外的编译标志或链接库。

CheckIncludeFile

  • 提供一个宏,用于检查系统上的包含文件。该宏接受两个参数,第一个参数是要查找的包含文件,第二个参数是存储结果的变量。附加的 CFlag 可以作为第三个参数传入或者通过设置 CMAKE_REQUIRED_FLAGS 传入。

CheckIncludeFileCXX

  • 提供一个宏,用于检查 C++ 程序中的包含文件。该宏接受两个参数,第一个参数是要查找的包含文件,第二个参数是存储结果的变量。附加的 CFlag 可以作为第三个参数传入。

CheckIncludeFiles

  • 提供一个宏,用于检查给定的头文件是否可以一起包含。如果 CMAKE_REQUIRED_FLAGS 被设置了,则该宏会使用它。当你感兴趣的头文件依赖于首先包含另一个头文件时,该宏非常有用。

CheckLibraryExists

  • 提供一个宏,用于检查库是否存在。该宏接受四个参数,第一个参数是要检查的库的名称,第二个是应该在该库中的函数的名称,第三个参数是应该找到库的位置,第四个参数是一个用来存储结果的变量。如果 CMAKE_REQUIRED_FLAGS 和 CMAKE_REQUIRED_LIBRARIES 被设置了,该宏会使用它们。

CheckSymbolExists

  • 提供一个宏,该宏检查是否在头文件中定义了符号。该宏接受三个参数,第一个参数为要查找的符号。第二个参数是要尝试包含的头文件列表,第三个参数是结果存储的位置。如果 CMAKE_REQUIRED_FLAGS 和 CMAKE_REQUIRED_LIBRARIES 被设置了,该宏会使用它们。

CheckTypeSize

  • 提供一个宏,用于确定变量类型的字节大小。该宏接受两个参数,第一个参数是要计算的类型,第二个参数是结果的存储位置。如果 CMAKE_REQUIRED_FLAGS 和 CMAKE_REQUIRED_LIBRARIES 被设置了,该宏会使用它们。

CheckVariableExists

  • 提供一个宏,用于检查是否存在全局变量。该宏接受两个参数,第一个参数是要查找的变量,第二个参数是要存储结果的变量。这个宏将创建命名变量的原型,然后尝试使用它。如果测试程序被编译,那么变量就存在。这只适用于 C (语言)变量。如果 CMAKE_REQUIRED_FLAGS 和 CMAKE_REQUIRED_LIBRARIES 被设置了,该宏会使用它们。

考虑下面的示例,该示例展示了各种用于计算平台属性的模块。在示例的开始,从 CMake 加载了四个模块。该示例的其余部分使用这些模块中定义的宏分别测试头文件、库、符号和类型大小。

# 包括所有必要的宏文件
include(CheckIncludeFiles)
include(CheckLibraryExists)
include(CheckSymbolExists)
include(CheckTypeSize)

# 检查头文件
set(INCLUDES "")
check_include_files("${INCLUDES};winsock.h" HAVE_WINSOCK_H)

if(HAVE_WINSOCK_H)
  set(INCLUDES ${INCLUDES} winsock.h)
endif()

check_include_files("${INCLUDES};io.h" HAVE_IO_H)
if (HAVE_IO_H)
  set(INCLUDES ${INCLUDES} io.h)
endif()

# 检查所有所需的库
set(LIBS "")
check_library_exists("dl;${LIBS}" dlopen "" HAVE_LIBDL)
if(HAVE_LIBDL)
  set(LIBS ${LIBS} dl)
endif()

check_library_exists("ucb;${LIBS}" gethostname "" HAVE_LIBUCB)
if(HAVE_LIBUCB)
  set(LIBS ${LIBS} ucb)
endif()

# 将我们找到的库添加到使用 check_symbol_exists 宏查找符号时使用的库(列表)中
set(CMAKE_REQUIRED_LIBRARIES ${LIBS})

# 检查一些被使用的函数
check_symbol_exists(socket "${INCLUDES}" HAVE_SOCKET)
check_symbol_exists(poll "${INCLUDES}" HAVE_POLL)

# 多种(变量)类型的大小
check_type_size(int SIZEOF_INT)
check_type_size(size_t SIZEOF_SIZE_T)

9.3 如何传递参数到编译

一旦确定了你所感兴趣的系统特性,就该根据所发现的特性来配置软件了。有两种常用的方式将此信息传递给编译器:在编译行上,或使用预先配置的头文件。第一种方法是在编译行上传递定义。预处理器定义可以通过 target_compile_definitions 命令从 CMakeLists 文件传递给编译器。例如, C 代码中的一个常见实践是能够有选择地编入或不编入调试语句。

#ifdef DEBUG_BUILD
  printf("the value of v is %d", v);
#endif

CMake 变量可以用来使用 option 命令来打开或关闭调试构建:

option(DEBUG_BUILD
      "Build with extra debug print messages.")

if(DEBUG_BUILD)
  target_compile_definitions(mytarget PUBLIC DEBUG_BUILD)
endif()

另一个例子是告诉编译器本章前面讨论过的上一个 HAS_FOOBAR_CALL 测试的结果。你可以用以下方法做到这一点:

if (HAS_FOOBAR_CALL)
  target_compile_definitions(mytarget PUBLIC HAS_FOOBAR_CALL)
endif()

9.4 如何配置一个头文件

第二种给源代码传递定义的方式时配置一个头文件。这个头文件将会包含构建项目所需的所有的 #define 的宏。要用 CMake 配置一个文件,可以使用 configure_file 命令。该命令需要一个由 CMake 解析的输入文件,以生成一个所有变量都展开或替换的输出文件。有三种方法可以在 configure_file 的输入文件中指定变量。

#cmakedefine VARIABLE

如果 VARIABLE 为 true ,那么结果将是:

#define VARIABLE

如果 VARIABLE 为 false ,那么结果将是:

/* #undef VARIABLE */

当写一个要配置的文件时,考虑使用 @VARIABLE@ 而不是 ${VARIABLE} 来表示那些被 CMake 扩展的变量。由于 ${} 语法很常见地被其他语言使用,用户通过给 configure_file 命令传递 @ONLY 选项来告诉该命令仅仅扩展使用 @var@ 语法的变量。当你在配置一个脚本,该脚本中可能含有你想保留的 ${var} 字符串时,这就非常有用了。这非常重要,因为如果 var 在 CMake 中未定义,CMake 将会将所有遇到的 ${var} 都设置为空字符串。

下面的例子为一个项目配置了一个 .h 文件,文件中包含了预处理器变量。第一个定义指示库中是否存在 FOOBAR 调用,下一个定义包含构建树的路径。

# ---- CMakeLists.txt file-----

# 从源码树中配置一个名为 projectConfigure.h.in 的文件,并将配置后的结果文件放入构建树中,并将其命名为 projectConfigure.h 
configure_file(
   ${PROJECT_SOURCE_DIR}/projectConfigure.h.in
   ${PROJECT_BINARY_DIR}/projectConfigure.h
   @ONLY
   )
// -----projectConfigure.h.in file------
/* 定义一个变量来告诉代码 foobar 调用在这个系统上是否可用 */
#cmakedefine HAS_FOOBAR_CALL

/* 定义一个表示构建目录路径的变量 */
#define PROJECT_BINARY_DIR "@PROJECT_BINARY_DIR@"

重要的是将文件配置到二进制树中,而不是源码树中。单个源码树可以由多个构建树或平台共享。通过将文件配置到二进制树中,构建或平台之间的差异将被隔离在构建树中,不会破坏其他构建。这意味着你需要使用 target_include_directories 命令将配置头文件的构建树的目录包含到项目的包含目录列表中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值