CMake如何查找链接库

转载 Public Wiki; Yeolar [译]   2014-12-16 13:46  

如果编译软件使用了外部库,事先并不知道它的头文件和链接库的位置。得在编译命令中加上包含它们的查找路径。CMake使用 find_package 命令来解决这个问题。本文讨论了如何在CMake项目中使用外部库,以及如何给没有查找模块的库写一个。

1 使用外部库

为了能支持各种常见的库和包,CMake自带了很多模块。可以通过命令 cmake --help-module-list 得到你的CMake支持的模块的列表,或者直接查看模块路径。比如Ubuntu linux上,模块的路径是 /usr/share/cmake/Modules/ 。

让我们以bzip2库为例。CMake中有个 FindBZip2.cmake 模块。只要使用 find_package(BZip2) 调用这个模块,cmake会自动给一些变量赋值,然后就可以在CMake脚本中使用它们了。变量的列表可以查看cmake模块文件,或者使用命令 cmake --help-module FindBZip2

比如一个使用bzip2的简单程序,编译器需要知道 bzlib.h 的位置,链接器需要找到bzip2库(动态链接的话,Unix上是 libbz2.so 类似的文件,Windows上是 libbz2.dll )。

1 cmake_minimum_required(VERSION 2.8)
2 project(helloworld)
3 add_executable(helloworld hello.c)
4 find_package (BZip2)
5 if (BZIP2_FOUND)
6   include_directories(${BZIP_INCLUDE_DIRS})
7   target_link_libraries (helloworld ${BZIP2_LIBRARIES})
8 endif (BZIP2_FOUND)

可以用 cmakemake VERBOSE=1 来验证传给编译器和链接器的flag是否正确。也可以用ldd或者dependency walker之类的工具在编译后验证 helloworld 链接的文件。

2 使用CMake没有自带查找模块的外部库

假设你想要使用LibXML++库。在写本文时,CMake还没有一个libXML++的查找模块。但是可以在网上搜索到一个( FindLibXML++.cmake )。在 CMakeLists.txt 中写,

1 find_package(LibXML++ REQUIRED)
2 include_directories(${LibXML++_INCLUDE_DIRS})
3 set(LIBS ${LIBS} ${LibXML++_LIBRARIES})

如果包是可选的,可以忽略 REQUIRED 关键字,通过 LibXML++_FOUND 布尔变量来判断是否找到。检测完所有的库后,对于链接目标有,

1 target_link_libraries(exampleProgram ${LIBS})

为了能正常的工作,需要把 FindLibXML++.cmake 文件放到CMake的模块路径。因为CMake还不包含它,需要在项目中指定。在项目根目录下创建一个 cmake/Modules/ 文件夹,并且在主 CMakeLists.txt 中包含下面的代码:

1 set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/")

可能你已经猜到了,把刚才的需要用到的CMake模块放到这个文件夹下。

一般来说就是这样。有些库可能还需要些其他的什么,所以要再看一下 FindSomething.cmake 文件的文档。

2.1 包含组件的包

有些库不是一个整体,还包含一些依赖的库或者组件。一个典型的例子是Qt库,它其中包含QtOpenGL和QtXml组件。使用下面的 find_package 命令来使用这些组件:

1 find_package(Qt COMPONENTS QtOpenGL QtXml REQUIRED)

如果包是可选的,这里同样可以忽略 REQUIRED 关键字。这时可以使用 <PACKAGE>_<COMPONENT>_FOUND 变量(如 Qt_QtXml_FOUND )来检查组件是否被找到。下面的 find_package 命令是等价的:

1 find_package(Qt COMPONENTS QtOpenGL QtXml REQUIRED)
2 find_package(Qt REQUIRED COMPONENTS QtOpenGL QtXml)
3 find_package(Qt REQUIRED QtOpenGL QtXml)

如果包中的组件有些是必需的,有些不是,可以调用 find_package 两次:

1 find_package(Qt COMPONENTS QtXml REQUIRED)
2 find_package(Qt COMPONENTS QtOpenGL)

或者也可以不加 REQUIRED 关键字用 find_package 同时查找全部组件,然后再显式地检查必需的组件:

1 find_package(Qt COMPONENTS QtOpenGL QtXml)
2 if ( NOT Qt_FOUND OR NOT QtXml_FOUND )
3   message(FATAL_ERROR "Package Qt and component QtXml required, but not found!")
4 endif( NOT Qt_FOUND OR NOT QtXml_FOUND )

3 包查找是如何工作的

find_package() 命令会在模块路径中寻找 Find<name>.cmake ,这是查找库的一个典型方式。首先CMake查看 ${CMAKE_MODULE_PATH} 中的所有目录,然后再查看它自己的模块目录 <CMAKE_ROOT>/share/cmake-x.y/Modules/ 。

如果没找到这样的文件,会寻找 <Name>Config.cmake 或者 <lower-case-name>-config.cmake ,它们是假定库会安装的文件(但是目前还没有多少库会安装它们)。不做检查,直接包含安装的库的固定值。

前面的称为模块模式,后面的称为配置模式。配置模式的文件的编写见 这里的文档 。可能还会用到 importing and exporting targets 这篇文档。

模块系统好像还没有文档,所以本文主要讨论这方面的内容。

不管使用哪一种模式,只要找到包,就会定义下面这些变量:

<NAME>_FOUND
<NAME>_INCLUDE_DIRS or <NAME>_INCLUDES
<NAME>_LIBRARIES or <NAME>_LIBRARIES or <NAME>_LIBS
<NAME>_DEFINITIONS

这些都在 Find<name>.cmake 文件中。

现在,在你的代码(要使用库 <name> 的代码)的顶层目录中的 CMakeLists.txt 文件中,我们检查变量 <NAME>_FOUND 来确定包是否被找到。大部分包的这些变量中的包名是全大写的,如 LIBFOO_FOUND ,有些包则使用包的实际大小写,如 LibFoo_FOUND 。如果找到这个包,我们用 <NAME>_INCLUDE_DIRS 调用 include_directories() 命令,用 <NAME>_LIBRARIES 调用 target_link_libraries() 命令。

这些约定的文档在CMake模块目录中的 readme.txt 文件中。

REQUIRED 和其他可选的 find_package 的参数被 find_package 传给模块,模块由此确定操作。

4 捎带介绍下pkg-config

pkg-config是个用来帮助构建的工具,它基于记录库文件和头文件位置的 .pc 文件。主要用在类Unix系统上。可以在 pkg-config的网站 找到更多的信息。CMake可以利用pkg-config,可以在CMake的模块目录下的 FindPkgConfig.cmake 文件中找到相关的文档。这在当你处理一个没有cmake脚本的库的时候,或者遇到CMake的查找脚本失效的情况,非常有帮助。

但是,直接使用pkg-config的结果需要非常小心。一个主要原因是对于ccmake手动定义的库路径,可能覆盖到或者发生冲突。此外,也有可能pkg-config提供了错误的信息(错误的编辑器等)。对于这些情况,让CMake不依赖pkg-config做检测,而只用pkg-config作为查找路径的提示。

5 编写查找模块

首先,注意传给 find_package 的名字或者前缀,是用于全部变量的部分文件名和前缀。这很重要,名字必须完全匹配。不幸的是很多情况下,即使是CMake自带的模块,也有不匹配的名字,导致各种问题。

模块的基本操作应该大体按下面的顺序:

  • 使用 find_package 检测库依赖的其他的库
    • 需要转发 QUIETLYREQUIRED 参数(比如,如果当前的包是 REQUIRED 的,它的依赖也应该是)
  • 可选地使用pkg-config来检测 include/library 的路径(如果pkg-config可用的话)
  • 分别使用 find_pathfind_library 寻找头文件和库文件
    • pkg-config提供的路径只用来作为查找位置的提示
    • CMake也有很多其他查找路径是写死的
    • 结果应该保存在 <name>_INCLUDE_DIR<name>_LIBRARY 变量中(注意不是复数形式)
  • 设置 <name>_INCLUDE_DIRS<name>_INCLUDE_DIR <dependency1>_INCLUDE_DIRS ...
  • 设置 <name>_LIBRARIES<name>_LIBRARY <dependency1>_LIBRARIES ...
    • 依赖使用复数形式,包自身使用 find_pathfind_library 定义的单数形式
  • 调用 find_package_handle_standard_args() 宏来设置 <name>_FOUND 变量,并打印一条成功或者失败的消息
 1 # - Try to find LibXml2
 2 # Once done this will define
 3 #  LIBXML2_FOUND - System has LibXml2
 4 #  LIBXML2_INCLUDE_DIRS - The LibXml2 include directories
 5 #  LIBXML2_LIBRARIES - The libraries needed to use LibXml2
 6 #  LIBXML2_DEFINITIONS - Compiler switches required for using LibXml2
 7 
 8 find_package(PkgConfig)
 9 pkg_check_modules(PC_LIBXML QUIET libxml-2.0)
10 set(LIBXML2_DEFINITIONS ${PC_LIBXML_CFLAGS_OTHER})
11 
12 find_path(LIBXML2_INCLUDE_DIR libxml/xpath.h
13           HINTS ${PC_LIBXML_INCLUDEDIR} ${PC_LIBXML_INCLUDE_DIRS}
14           PATH_SUFFIXES libxml2 )
15 
16 find_library(LIBXML2_LIBRARY NAMES xml2 libxml2
17              HINTS ${PC_LIBXML_LIBDIR} ${PC_LIBXML_LIBRARY_DIRS} )
18 
19 set(LIBXML2_LIBRARIES ${LIBXML2_LIBRARY} )
20 set(LIBXML2_INCLUDE_DIRS ${LIBXML2_INCLUDE_DIR} )
21 
22 include(FindPackageHandleStandardArgs)
23 # handle the QUIETLY and REQUIRED arguments and set LIBXML2_FOUND to TRUE
24 # if all listed variables are TRUE
25 find_package_handle_standard_args(LibXml2  DEFAULT_MSG
26                                   LIBXML2_LIBRARY LIBXML2_INCLUDE_DIR)
27 
28 mark_as_advanced(LIBXML2_INCLUDE_DIR LIBXML2_LIBRARY )

在第一行,包含了LibFindMacros。因为当前CMake中并没有,所以要想生效,就必须把 LibFindMacros.cmake 文件放到模块路径下。

5.1 查找文件

然后是实际的检测。给 find_pathfind_library 提供一个变量名作为第一个参数。如果你需要多个 include 路径,用不同的变量名多次调用 find_pathfind_library 类似。

NAMES 指定目标的一个或多个名字,只要匹配上一个,就会选中它。在 find_path 中应该使用主头文件或者C/C++代码导入的文件。也有可能会包含目录,比如 alsa/asound.h,它会使用 asound.h 所在文件夹的父目录作为结果。

PATHS 用来给CMake提供额外的查找路径,他不应该用于定义pkg-config以外的东西(CMake有自己的内置默认值,如果需要可以通过各种配置变量添加更多)。如果你不使用它,忽略这部分内容。

PATH_SUFFIXES 对于某些系统上的库很有用,这类库把它们的文件放在类似 /usr/include/ExampleLibrary-1.23/ExampleLibrary/main.h 这样的路径。这种情况你可以使用 NAMES ExampleLibrary/main.h PATH_SUFFIXES ExampleLibrary-1.23 。可以指定多个后缀,CMake会在所有包含的目录和主目录逐一尝试,也包括没有后缀的情况。

库名不包括UNIX系统上使用的前缀,也不包括任何文件扩展名或编译器标准之类的,CMake会不依赖平台地检测它们。如果库文件名中有库的版本号,那么它仍然需要。

5.2 使用LibFindMacros

有一个 LibFindMacros.cmake 文件,用来便于写查找模块。它包含对于每个库都相同的各种 libfind 宏。使用它,一个脚本看起来像这样:

 1 # - Try to find ImageMagick++
 2 # Once done, this will define
 3 #
 4 #  Magick++_FOUND - system has Magick++
 5 #  Magick++_INCLUDE_DIRS - the Magick++ include directories
 6 #  Magick++_LIBRARIES - link these to use Magick++
 7 
 8 include(LibFindMacros)
 9 
10 # Dependencies
11 libfind_package(Magick++ Magick)
12 
13 # Use pkg-config to get hints about paths
14 libfind_pkg_check_modules(Magick++_PKGCONF ImageMagick++)
15 
16 # Include dir
17 find_path(Magick++_INCLUDE_DIR
18   NAMES Magick++.h
19   PATHS ${Magick++_PKGCONF_INCLUDE_DIRS}
20 )
21 
22 # Finally the library itself
23 find_library(Magick++_LIBRARY
24   NAMES Magick++
25   PATHS ${Magick++_PKGCONF_LIBRARY_DIRS}
26 )
27 
28 # Set the include dir variables and the libraries and let libfind_process do the rest.
29 # NOTE: Singular variables for this library, plural for libraries this this lib depends on.
30 set(Magick++_PROCESS_INCLUDES Magick++_INCLUDE_DIR Magick_INCLUDE_DIRS)
31 set(Magick++_PROCESS_LIBS Magick++_LIBRARY Magick_LIBRARIES)
32 libfind_process(Magick++)

libfind_pkg_check_modules 是CMake自己的pkg-config模块的一个用来简化的封装。你不用再检查CMake的版本,加载合适的模块,检查是否被加载,等等。参数和传给 pkg_check_modules 的一样:先是待返回变量的前缀,然后是包名(pkg-config的)。这样就定义了 <prefix>_INCLUDE_DIRS 和其他的这种变量。

5.2.1 依赖(可选)

libfind_packagefind_package 类似,区别是它转发 QUIETLYREQUIRED 参数。第一个参数是当前的包名。即,这里Magick++依赖于Magick。其他参数比如版本可以添加在Magick后面,它们被转发给CMake的内部 find_package 命令。对你的库依赖的每个库加上其中一行,并且提供查找模块。

5.2.2 最后处理

最后的处理,幸运的是非常程序化,可以通过 libfind_process 宏和示例中的最后三行来完成。你需要把 <name>_PROCESS_INCLUDES 设置为 <name>_INCLUDE_DIRS 包含的全部变量,把 <name>_PROCESS_LIBS 设置为 <name>_LIBRARIES 包含的全部变量。然后调用 libfind_process(<name>) 完成剩下的事情。

只有提供的全部变量都有有效值时,库被认为 FOUND

6 性能和缓存

CMake的变量系统要比初看起来的要复杂得多。有些变量做了缓存。做了缓存的变量有内部的(不能用ccmake编辑)和外部的(可以被ccmake修改)。另外,外部变量只能在ccmake的高级模式可见。

默认情况下,所有变量都是不缓存的。

为了避免每次执行时都重复检测全部的库,更为了允许用户在ccmake中设置include目录和库,需要支持缓存。幸运的是,这已经被 find_pathfind_library 支持,它们可以缓存它们的变量。如果变量已经设置为有效值(比如不是 -NOTFOUND 或者未定义),这些函数将什么也不做,保持旧值。类似地, pkg_check_modules 支持结果的内部缓存,因此不需要每次都再调用pkg-config。

另一方面,查找模块的输出值( <name>_FOUND, <name>_INCLUDE_DIRS<name>_LIBRARIES )不应该被缓存,否则修改其他缓存的变量就不能改变输出,这显然是不期望的。

7 查找模块的常见问题

  • 文件名和变量名中的大小写问题或者名字不匹配问题
  • 模块不检查 <name>_FIND_REQUIRED<name>_FIND_QUIETLY ,因此 find_package 的参数 QUIETREQUIRED 没有效果
  • 没有设置 <name>_INCLUDE_DIRS<name>_LIBRARIES ,只有单数形式的可用

http://vtk.org/Wiki/CMake:How_To_Find_Libraries

http://www.yeolar.com/note/2014/12/16/cmake-how-to-find-libraries/

http://www.yeolar.com/note/2014/12/16/cmake-how-to-find-libraries/