用cmake发布库

基本原理

除了用cmake进行库的编译以外,我们还需要掌握如何写一个cmake方便别人在使用我们的库的时候能够顺利链接。
本篇仅说明一下整个链接和安装的思路,理清看文档的顺序,其具体的参数和函数签名都有详细的文档的。

首先推荐大家下载这一个Toy Example:
https://gitlab.kitware.com/cmake/community/uploads/a91192d30ee2df45bd225b08c3a20c1d/FooBar.zip
其展示了一个最小的,支持安装以及被其他库find_package的写法。
另外可以参考这篇文章,用中文讲述地比较清晰:
http://www.yeolar.com/note/2014/12/16/cmake-how-to-find-libraries/
这里是关于find_package的完整API阐述:
https://cmake.org/cmake/help/latest/command/find_package.html#command:find_package
通过本篇文章,咱们希望知道以下目的是怎样达成的:

  • 将我们需要对外公布的lib以及header安装在指定的位置
  • 提供多个.cmake脚本(XXConfig.cmake, XXTarget.cmake, XXConfigVersion.cmake), 使得客户在调用find_package的时候能够成功找到所需库和头文件的位置,这里我们用name来表示库的名字,这相当于设置了以下的变量:
    1
    2
    3
    4
    <NAME>_FOUND
    <NAME>_INCLUDE_DIRS or <NAME>_INCLUDES
    <NAME>_LIBRARIES or <NAME>_LIBRARIES or <NAME>_LIBS
    <NAME>_DEFINITIONS

find_package做了啥

为了理解要给find_package提供什么内容,我们首先要理解find_package到底做了哪一些步骤。find_package找库的位置可以分为两种方法:

  • 模块模式(Module Mode):查找形如Find<name>.cmake这样的脚本,通过该脚本定义以上的变量。查找的位置有两处:1. 设置的${CMAKE_MODULE_PATH}(由当前CmakeLists.txt脚本或调用命令行等设置)下;2. <CMAKE_ROOT>/Modules/ ,比如CMAKE_ROOT=/usr/bin/cmake/share/cmake-3.10, 这个位置保存了cmake认为编程人员常用的库,如lua: FindLua.cmake等。这些路径保留了Find<name>.cmake这样的路径,cmake匹配上后将直接执行,类似于include(Find<name>.cmake)。如果模块模式找不到相应的cmake文件,那么就会启动配置模式,配置模式则比较复杂。

  • 配置模式(Config Mode )

    The CONFIG option, the synonymous NO_MODULE option, or the use of options not specified in the basic signature all enforce pure Config mode. In pure Config mode, the command skips Module mode search and proceeds at once with Config mode search.

配置模式指定配置所在的位置非常直白,它有一系列的搜索顺序,但是日常我们只需要设置<name>_DIR为包含config的文件夹路径即可。如果<name>_DIR找到了所需的配置文件,那么就会停止搜索,否则会按照顺序进行搜索,可能构造的搜索地址为:

CMake constructs a set of possible installation prefixes for the package. Under each prefix several directories are searched for a configuration file. The tables below show the directories searched. Each entry is meant for installation trees following Windows (W), UNIX (U), or Apple (A) conventions:

1
2
3
4
5
6
7
8
9
10
<prefix>/                                                       (W)
<prefix>/(cmake|CMake)/ (W)
<prefix>/<name>*/ (W)
<prefix>/<name>*/(cmake|CMake)/ (W)
<prefix>/(lib/<arch>|lib*|share)/cmake/<name>*/ (U)
<prefix>/(lib/<arch>|lib*|share)/<name>*/ (U)
<prefix>/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)/ (U)
<prefix>/<name>*/(lib/<arch>|lib*|share)/cmake/<name>*/ (W/U)
<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/ (W/U)
<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)/ (W/U)

On systems supporting macOS Frameworks and Application Bundles the following directories are searched for frameworks or bundles containing a configuration file:

1
2
3
4
5
6
<prefix>/<name>.framework/Resources/                    (A)
<prefix>/<name>.framework/Resources/CMake/ (A)
<prefix>/<name>.framework/Versions/*/Resources/ (A)
<prefix>/<name>.framework/Versions/*/Resources/CMake/ (A)
<prefix>/<name>.app/Contents/Resources/ (A)
<prefix>/<name>.app/Contents/Resources/CMake/ (A)

这其中<prefix>的变量替换顺序为(其实不用记住这么多,实际上常用的就是<name>_DIR):

  1. Search paths specified in the _ROOT CMake variable and the _ROOTenvironment variable, where <PackageName> is the package to be found. The package root variables are maintained as a stack so if called from within a find module, root paths from the parent’s find module will also be searched after paths for the current package. This can be skipped if NO_PACKAGE_ROOT_PATH is passed. See policy CMP0074.
  2. Search paths specified in cmake-specific cache variables. These are intended to be used on the command line with a -DVAR=value. The values are interpreted as semicolon-separated lists. This can be skipped if NO_CMAKE_PATH is passed:

    1
    2
    3
    CMAKE_PREFIX_PATH
    CMAKE_FRAMEWORK_PATH
    CMAKE_APPBUNDLE_PATH
  3. Search paths specified in cmake-specific environment variables. These are intended to be set in the user’s shell configuration, and therefore use the host’s native path separator (; on Windows and : on UNIX). This can be skipped if NO_CMAKE_ENVIRONMENT_PATH is passed:

    1
    2
    3
    4
    <PackageName>_DIR
    CMAKE_PREFIX_PATH
    CMAKE_FRAMEWORK_PATH
    CMAKE_APPBUNDLE_PATH
  4. Search paths specified by the HINTS option. These should be paths computed by system introspection, such as a hint provided by the location of another item already found. Hard-coded guesses should be specified with the PATHS option.

  5. Search the standard system environment variables. This can be skipped if NO_SYSTEM_ENVIRONMENT_PATHis passed. Path entries ending in /bin or /sbin are automatically converted to their parent directories:

    1
    PATH
  6. Search paths stored in the CMake User Package Registry. This can be skipped if NO_CMAKE_PACKAGE_REGISTRY is passed or by setting the CMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY to TRUE. See the cmake-packages(7)) manual for details on the user package registry.

  7. Search cmake variables defined in the Platform files for the current system. This can be skipped if NO_CMAKE_SYSTEM_PATH is passed:

    1
    2
    3
    CMAKE_SYSTEM_PREFIX_PATH
    CMAKE_SYSTEM_FRAMEWORK_PATH
    CMAKE_SYSTEM_APPBUNDLE_PATH
  8. Search paths stored in the CMake System Package Registry. This can be skipped if NO_CMAKE_SYSTEM_PACKAGE_REGISTRY is passed or by setting theCMAKE_FIND_PACKAGE_NO_SYSTEM_PACKAGE_REGISTRY to TRUE. See the cmake-packages(7)) manual for details on the system package registry.

  9. Search paths specified by the PATHS option. These are typically hard-coded guesses.

config的名字只能有这两种形式:<name>Config.cmake 或者 <lower-case-package-name>-config.cmake,一旦设置好<name>_DIR,并且cmake通过这个路径找到了相应的两种形式(之一)的配置文件,就会设置<name>_CONFIG为找到的配置文件(两者之一)的全部路径。

自定义模块config.cmake

find_package. 不管是配置式还是模块式,最终都是设置相应的库的路径。

模块模式

下面我们看一下模块式的一个例子:FindMFC.cmake

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
# file Copyright.txt or https://cmake.org/licensing for details.

#.rst:
# FindMFC
# -------
#
# Find MFC on Windows
#
# Find the native MFC - i.e. decide if an application can link to the
# MFC libraries.
#
# ::
#
# MFC_FOUND - Was MFC support found
#
# You don't need to include anything or link anything to use it.

# Assume no MFC support
set(MFC_FOUND "NO")

# Only attempt the try_compile call if it has a chance to succeed:
set(MFC_ATTEMPT_TRY_COMPILE 0)
if(WIN32 AND NOT UNIX AND NOT BORLAND AND NOT MINGW)
set(MFC_ATTEMPT_TRY_COMPILE 1)
endif()

if(MFC_ATTEMPT_TRY_COMPILE)
if(NOT DEFINED MFC_HAVE_MFC)
set(CHECK_INCLUDE_FILE_VAR "afxwin.h")
configure_file(${CMAKE_ROOT}/Modules/CheckIncludeFile.cxx.in
${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/CheckIncludeFile.cxx)
message(STATUS "Looking for MFC")
# Try both shared and static as the root project may have set the /MT flag
try_compile(MFC_HAVE_MFC
${CMAKE_BINARY_DIR}
${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/CheckIncludeFile.cxx
CMAKE_FLAGS
-DCMAKE_MFC_FLAG:STRING=2
-DCOMPILE_DEFINITIONS:STRING=-D_AFXDLL
OUTPUT_VARIABLE OUTPUT)
if(NOT MFC_HAVE_MFC)
configure_file(${CMAKE_ROOT}/Modules/CheckIncludeFile.cxx.in
${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/CheckIncludeFile.cxx)
try_compile(MFC_HAVE_MFC
${CMAKE_BINARY_DIR}
${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/CheckIncludeFile.cxx
CMAKE_FLAGS
-DCMAKE_MFC_FLAG:STRING=1
OUTPUT_VARIABLE OUTPUT)
endif()
if(MFC_HAVE_MFC)
message(STATUS "Looking for MFC - found")
set(MFC_HAVE_MFC 1 CACHE INTERNAL "Have MFC?")
file(APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeOutput.log
"Determining if MFC exists passed with the following output:\n"
"${OUTPUT}\n\n")
else()
message(STATUS "Looking for MFC - not found")
set(MFC_HAVE_MFC 0 CACHE INTERNAL "Have MFC?")
file(APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeError.log
"Determining if MFC exists failed with the following output:\n"
"${OUTPUT}\n\n")
endif()
endif()

if(MFC_HAVE_MFC)
set(MFC_FOUND "YES")
endif()
endif()

配置模式

具体参考
https://gitlab.kitware.com/cmake/community/uploads/a91192d30ee2df45bd225b08c3a20c1d/FooBar.zip 关键是提供<name>Config.cmake<name>ConfigVersion.cmake两个文件,并且在Version中配置PACKAGE_VERSION_COMPATIBLEPACKAGE_VERSION_EXACT两个变量。

Export及其使用

我们虽然通过find_package能够定义这些库所在的位置以及头文件的位置,但是需要一种方式将其作为一个target加进来,以用于编译和链接(否则他们只是纯粹的放在内存里的几个变量而已。这些需要通过target脚本来实现,一般target脚本都是和配置模式一起使用的,<name>Config.cmake中常include ("${CMAKE_CURRENT_LIST_DIR}/<name>-target.cmake")来直接调用target,直白来说,target里面就是添加了这几个库作为target,一个例子如下, 这个脚本是cmake通过export命令生成的,具体可以参考https://cmake.org/cmake/help/v3.12/command/export.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# Generated by CMake

if("${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}" LESS 2.5)
message(FATAL_ERROR "CMake >= 2.6.0 required")
endif()
cmake_policy(PUSH)
cmake_policy(VERSION 2.6)
#----------------------------------------------------------------
# Generated CMake target import file.
#----------------------------------------------------------------

# Commands may need to know the format version.
set(CMAKE_IMPORT_FILE_VERSION 1)

# Protect against multiple inclusion, which would fail when already imported targets are added once more.
set(_targetsDefined)
set(_targetsNotDefined)
set(_expectedTargets)
foreach(_expectedTarget foo bar)
list(APPEND _expectedTargets ${_expectedTarget})
if(NOT TARGET ${_expectedTarget})
list(APPEND _targetsNotDefined ${_expectedTarget})
endif()
if(TARGET ${_expectedTarget})
list(APPEND _targetsDefined ${_expectedTarget})
endif()
endforeach()
if("${_targetsDefined}" STREQUAL "${_expectedTargets}")
unset(_targetsDefined)
unset(_targetsNotDefined)
unset(_expectedTargets)
set(CMAKE_IMPORT_FILE_VERSION)
cmake_policy(POP)
return()
endif()
if(NOT "${_targetsDefined}" STREQUAL "")
message(FATAL_ERROR "Some (but not all) targets in this export set were already defined.\nTargets Defined: ${_targetsDefined}\nTargets not yet defined: ${_targetsNotDefined}\n")
endif()
unset(_targetsDefined)
unset(_targetsNotDefined)
unset(_expectedTargets)


# Create imported target foo
add_library(foo SHARED IMPORTED)

# Create imported target bar
add_executable(bar IMPORTED)

# Import target "foo" for configuration "Debug"
set_property(TARGET foo APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(foo PROPERTIES
IMPORTED_LOCATION_DEBUG "/Users/rmk/Downloads/FooBar/cmake-build-debug/foo/libfoo.dylib"
IMPORTED_SONAME_DEBUG "/Users/rmk/Downloads/FooBar/cmake-build-debug/foo/libfoo.dylib"
)

# Import target "bar" for configuration "Debug"
set_property(TARGET bar APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(bar PROPERTIES
IMPORTED_LOCATION_DEBUG "/Users/rmk/Downloads/FooBar/cmake-build-debug/bar/bar"
)

# This file does not depend on other imported targets which have
# been exported from the same project but in a separate export set.

# Commands beyond this point should not need to know the version.
set(CMAKE_IMPORT_FILE_VERSION)
cmake_policy(POP)

生成配置模式所需文件的CMakeLists.txt脚本

摘自https://gitlab.kitware.com/cmake/community/uploads/a91192d30ee2df45bd225b08c3a20c1d/FooBar.zip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Add all targets to the build-tree export set
export(TARGETS foo bar
FILE "${PROJECT_BINARY_DIR}/FooBarTargets.cmake")

# Export the package for use from the build-tree
# (this registers the build-tree with a global CMake-registry)
export(PACKAGE FooBar)

# Create the FooBarConfig.cmake and FooBarConfigVersion files
file(RELATIVE_PATH REL_INCLUDE_DIR "${INSTALL_CMAKE_DIR}"
"${INSTALL_INCLUDE_DIR}")
# ... for the build tree
set(CONF_INCLUDE_DIRS "${PROJECT_SOURCE_DIR}" "${PROJECT_BINARY_DIR}" CACHE PATH "")
configure_file(FooBarConfig.cmake.in
"${PROJECT_BINARY_DIR}/FooBarConfig.cmake" @ONLY)
# ... for the install tree
set(CONF_INCLUDE_DIRS "\${FOOBAR_CMAKE_DIR}/${REL_INCLUDE_DIR}")
configure_file(FooBarConfig.cmake.in
"${PROJECT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/FooBarConfig.cmake" @ONLY)
# ... for both
configure_file(FooBarConfigVersion.cmake.in
"${PROJECT_BINARY_DIR}/FooBarConfigVersion.cmake" @ONLY)

# Install the FooBarConfig.cmake and FooBarConfigVersion.cmake
install(FILES
"${PROJECT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/FooBarConfig.cmake"
"${PROJECT_BINARY_DIR}/FooBarConfigVersion.cmake"
DESTINATION "${INSTALL_CMAKE_DIR}" COMPONENT dev)

# Install the export set for use with the install-tree
# allowing others to include FooBarTargets.cmake
install(EXPORT FooBarTargets DESTINATION
"${INSTALL_CMAKE_DIR}" COMPONENT dev)

参考资料

http://www.voidcn.com/article/p-ydfqrabf-ru.html
https://gitlab.kitware.com/cmake/community/wikis/doc/tutorials/How-to-create-a-ProjectConfig.cmake-file
http://www.yeolar.com/note/2014/12/16/cmake-how-to-find-libraries/

例子:https://gitlab.kitware.com/cmake/community/uploads/a91192d30ee2df45bd225b08c3a20c1d/FooBar.zip
export函数: https://cmake.org/cmake/help/v3.12/command/export.html

0%