系统检查¶
本章将介绍如何使用 CMake 检查软件构建系统的环境。这是创建跨平台应用程序或库的关键因素。它涵盖了如何查找和使用系统和用户安装的头文件和库。它还涵盖了 CMake 的一些更高级的功能,包括 try_compile
和 try_run
命令。这些命令是确定托管您的软件的系统和编译器的功能的极其强大的工具。
使用头文件和库¶
许多 C 和 C++ 程序依赖于外部库;但是,当谈到项目的编译和链接的实际方面时,利用现有库对于开发人员和用户来说都可能很困难。问题通常会在软件在开发系统之外的其他系统上构建后立即出现。关于库和头文件所在位置的假设在它们未安装在新计算机上的同一位置且构建系统无法找到它们时变得很明显。CMake 具有许多功能来帮助开发人员将外部软件库集成到项目中。
与这种集成最相关的 CMake 命令是 find_file
、find_library
、find_path
、find_program
和 find_package
命令。对于大多数 C 和 C++ 库,find_library
和 find_path
的组合足以编译和链接已安装的库。命令 find_library
可用于定位库或允许用户定位库,而 find_path
可用于查找项目中代表性包含文件的路径。例如,如果您想链接到 tiff 库,您可以在 CMakeLists.txt 文件中使用以下命令
# find libtiff, looking in some standard places
find_library(TIFF_LIBRARY
NAMES tiff tiff2
PATHS /usr/local/lib /usr/lib
)
# find tiff.h looking in some standard places
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
的值将评估为假。
使用的下一个命令是 find_path
,这是一个通用命令,在本例中,它用于定位来自库的头文件。头文件和库通常安装在不同的位置,并且编译和链接使用它们的程序需要这两个位置。 find_path
的使用类似于 find_library
,尽管它只支持一个名称,一个搜索路径列表。
CMakeLists 文件的其余部分可以使用 find_*
命令创建的变量。这些变量可以在不检查有效值的情况下使用,因为如果任何必需的变量未设置,CMake 会打印一条错误消息通知用户。然后,用户可以设置缓存值并重新配置,直到消息消失。或者,CMakeLists 文件可以使用 if
命令来使用备用库或选项来构建项目,而无需使用该库(如果找不到该库)。
从上面的示例中,您可以看到如何使用 find_*
命令可以帮助您的软件在各种系统上进行编译。值得注意的是,find_*
命令从第一个参数和第一个路径开始查找匹配项,因此在列出路径和库名称时,首先列出您首选的路径和名称。如果有多个版本的库,并且您更喜欢 tiff 而不是 tiff2,请确保它们按此顺序排列。
系统属性¶
尽管在 C 和 C++ 代码中,在预处理器 ifdef
指令内添加平台特定代码是一种常见做法,但为了最大限度地提高可移植性,应避免这种做法。软件不应使用 ifdefs
针对特定平台进行调整,而应针对包含一组功能的规范系统进行调整。针对特定系统进行编码会降低软件的可移植性,因为系统及其支持的功能会随着时间的推移而变化,甚至会因系统而异。过去可能在平台上无法正常工作的功能,将来可能会成为该平台的必备功能。以下代码片段说明了针对规范系统和特定系统进行编码之间的区别
// coding to a feature
#ifdef HAS_FOOBAR_CALL
foobar();
#else
myfoobar();
#endif
// coding to specific platforms
#if defined(SUN) && defined(HPUX) && !defined(GNUC)
foobar();
#else
myfoobar();
#endif
第二种方法的问题是,代码必须针对软件编译的每个新平台进行修改。例如,SUN 的未来版本可能不再具有 foobar 调用。使用 HAS_FOOBAR_CALL
方法,只要 HAS_FOOBAR_CALL
正确定义,软件就可以正常工作,而这正是 CMake 可以提供帮助的地方。CMake 可以利用 try_compile
和 try_run
命令,正确地定义 HAS_FOOBAR_CALL
并自动执行。这些命令可以在 CMake 配置步骤中用于编译和运行小型测试程序。测试程序将被发送到将用于构建项目的编译器,如果出现错误,则可以禁用该功能。这些命令要求您编写一个小型 C 或 C++ 程序来测试该功能。例如,要测试系统上是否提供了 foobar
调用,请尝试编译一个使用 foobar
的简单程序。首先编写简单的测试程序(本例中为 testNeedFoobar.c),然后将 CMake 调用添加到 CMakeLists 文件中以尝试编译该代码。如果编译成功,则 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_compile
和 try_run
操作,可能需要将标志传递给编译器或 CMake。这两个命令都支持可选参数 CMAKE_FLAGS
和 COMPILE_DEFINITIONS
。 CMAKE_FLAGS
可用于将 -DVAR:TYPE=VALUE
标志传递给 CMake。 COMPILE_DEFINITIONS
的值将直接传递到编译器命令行。
CMake cmake-modules(7)
中提供了几个预定义的 try-run 和 try-compile 模块,其中一些列在下面。这些模块允许执行一些常见的检查,而无需为每个测试创建源文件。这些模块中的许多将查看 CMAKE_REQUIRED_FLAGS
和 CMAKE_REQUIRED_LIBRARIES
变量的当前值,以向测试添加额外的编译标志或链接库。
CheckIncludeFile
提供一个宏,通过接收两个参数来检查系统上的包含文件,第一个参数是要查找的包含文件,第二个参数是要将结果存储到的变量。可以使用第三个参数或通过设置
CMAKE_REQUIRED_FLAGS
传递额外的 CFlags。CheckIncludeFileCXX
提供一个宏,通过接收两个参数来检查 C++ 程序中的包含文件,第一个参数是要查找的包含文件,第二个参数是要将结果存储到的变量。可以使用第三个参数传递额外的 CFlags。
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 all the necessary files for macros
include(CheckIncludeFiles)
include(CheckLibraryExists)
include(CheckSymbolExists)
include(CheckTypeSize)
# Check for header files
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()
# Check for all needed libraries
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()
# Add the libraries we found to the libraries to use when
# looking for symbols with the check_symbol_exists macro
set(CMAKE_REQUIRED_LIBRARIES ${LIBS})
# Check for some functions that are used
check_symbol_exists(socket "${INCLUDES}" HAVE_SOCKET)
check_symbol_exists(poll "${INCLUDES}" HAVE_POLL)
# Various type sizes
check_type_size(int SIZEOF_INT)
check_type_size(size_t SIZEOF_SIZE_T)
如何将参数传递给编译¶
确定了感兴趣的系统功能后,就可以根据所发现的功能来配置软件。将此信息传递给编译器的两种常用方法是:在编译行上,或者使用预先配置的头文件。第一种方法是在编译行上传递定义。可以使用 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()
如何配置头文件¶
将定义传递到源代码的第二种方法是配置头文件。头文件将包含构建项目所需的所有#define
宏。要使用 CMake 配置文件,可以使用configure_file
命令。此命令需要一个输入文件,CMake 会解析该文件以生成一个包含所有已扩展或替换变量的输出文件。在输入文件中为configure_file
指定变量有三种方法。
#cmakedefine VARIABLE
如果 VARIABLE 为真,则结果将为
#define VARIABLE
如果 VARIABLE 为假,则结果将为
/* #undef VARIABLE */
在编写要配置的文件时,请考虑使用@VARIABLE@
而不是${VARIABLE}
来表示预计由 CMake 扩展的变量。由于${}
语法通常由其他语言使用,用户可以告诉configure_file
命令仅使用@var@
语法扩展变量,方法是将@ONLY
选项传递给该命令;如果您要配置可能包含${var}
字符串(您希望保留这些字符串)的脚本,这将非常有用。这很重要,因为如果在 CMake 中未定义var
,CMake 将用空字符串替换${var}
的所有出现。
以下示例配置包含预处理器变量的项目的 .h 文件。第一个定义指示库中是否存在FOOBAR
调用,下一个定义包含构建树的路径。
# ---- CMakeLists.txt file-----
# Configure a file from the source tree
# called projectConfigure.h.in and put
# the resulting configured file in the build
# tree and call it projectConfigure.h
configure_file(
${PROJECT_SOURCE_DIR}/projectConfigure.h.in
${PROJECT_BINARY_DIR}/projectConfigure.h
@ONLY
)
// -----projectConfigure.h.in file------
/* define a variable to tell the code if the */
/* foobar call is available on this system */
#cmakedefine HAS_FOOBAR_CALL
/* define a variable with the path to the */
/* build directory */
#define PROJECT_BINARY_DIR "@PROJECT_BINARY_DIR@"
将文件配置到二进制树中而不是源树中非常重要。单个源树可能被多个构建树或平台共享。通过将文件配置到二进制树中,构建或平台之间的差异将被隔离在构建树中,不会破坏其他构建。这意味着您需要使用target_include_directories
命令将配置头文件所在构建树的目录包含到项目的包含目录列表中。