CMake ——库的安装与导出

CMake ——库的安装与导出

RayAlto OP

如何成为 C++ 程序员

不知道是哪一天的哪个程序吸引了你,或者单纯是脑子里哪根弦搭错了,你决定要成为一名 C++ 程序员。

受到国内 C++ 领域大神谭浩强的影响,你入坑了 Visual C++ 6.0 ,去国内精英程序员论坛 CSDN 搜索了半天之后你终于在 Windows 11 上成功安装了 Visual C++ 6.0 ,身边人纷纷投来看到了原始人的目光,你发现了异样,又在对简体中文的支持处于世界领先地位的百度上找到了 Visual C++ 6.0 的上位替代 Visual Studio ,装完之后发现 C 盘急得红温了。

你心想:这下终于能写一写 C++ 了,对着 Visual Studio 疯狂输出,写了几坨 C with class 之后你发现你的源文件、头文件越来越多,还有了链接别的库的需求,对着 GUI 像猴子一样戳来戳去终于配置好了之后你发现你的项目结构一坨,你觉得 Visual Studio 虽然有 GUI ,但也不怎么好用。

你打算学一学命令行自己管理项目文件,离开 M$ 之后你首先看中了 GNU GCC ,你倒了八辈子血霉选择去 SourceForge 手动下载一个 MinGW-W64 ,废了九牛二虎之力你终于学会了 g++ test.cpp -o test ,你觉得自己很牛逼,但你还是不知道这句指令背后发生了什么,同时你发现你的编辑器没有代码补全之类的现代人应该用的东西,而且组织多个源文件时也不能一直手写 g++ 编译,你又决定学习 Makefile ,恭喜你,你已经在学习 C++ 的道路上越走越远了。

你一边学习着 Neovim, Lua, Makefile, CMake, clangd, clang-tidy, clang-format 之类花里胡哨的东西,一边想着“我到底什么时候能开始学习 C++ ?”

1. Targets 文件

这里有一个命名方式的问题, CMake 支持 PascalCase 和 dash-case , i.e. ProjectNameTargets.cmakeproject-name-targets.cmake 都是正确的命名方式,我选择 dash-case

因为现在站在了库的开发者视角,提供 project-name-targets.cmake 才是正确的导出做法, CMake 提供了一个方便的生成 targets.cmake 的方式:

1
2
3
4
5
6
7
install(TARGETS ${PROJECT_NAME}
EXPORT ${PROJECT_NAME}-targets
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
INCLUDES DESTINATION include
)

其实这句是用来安装你的 target 的,包括你的库文件、可执行程序等,只不过其中的 EXPORT ${PROJECT_NAME}-targets 会使 CMake 帮你生成一个 targets.cmake ,位置在 ${PROJECT_BINARY_DIR}/CMakeFiles/Export/*/project-name-targets.cmake ,但显式写出 bin, lib, include 目录有些原始,可以借助 GNUInstallDirs 模块:

1
2
3
4
5
6
7
8
9
include(GNUInstallDirs) # 引入 CMAKE_INSTALL_*DIR 变量

install(TARGETS ${PROJECT_NAME}
EXPORT ${PROJECT_NAME}-targets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

生成后需要配置安装,可以用 install(EXPORT)

1
2
3
4
5
install(EXPORT ${PROJECT_NAME}-targets
FILE ${PROJECT_NAME}-targets.cmake
NAMESPACE ${PROJECT_NAME}::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)

如果正确安装后,一份 project-name-targets.cmake 会出现在比如 /usr/lib/cmake/project-name/project-name-targets.cmake ,用户现在可以通过 find_package(project-name) 来找到你的库了,因为加了 NAMESPACE ${PROJECT_NAME}:: ,用户在使用时需要像这样写:

1
2
find_package(project-name)
target_link_libraries(target PRIVATE project-name::project-name)

2. Config 文件

Config 文件用来保证用户在 find_package 之后真的配置好了库的所有细节,官方推荐的做法是写一个 project-name-config.cmake ,这里用 *.in 文件生成:

1
2
3
4
@PACKAGE_INIT@

include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@-targets.cmake")
check_required_components("@PROJECT_NAME@")

我的喜好是把这个文件放进 project_root/cmake/config.cmake.in ,然后在主 CMakeLists.txt 中这样输出实际的 Config 文件:

1
2
3
4
5
6
7
include(CMakePackageConfigHelpers)

configure_package_config_file(
${PROJECT_SOURCE_DIR}/cmake/config.cmake.in
${PROJECT_BINARY_DIR}/${PROJECT_NAME}-config.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)

这里的 INSTALL_DESTINATION 貌似没什么用,后面还要手动配置安装方式,但官方这么写应该有官方的道理

官方还推荐写一个 ConfigVersion 文件,用来配置库的版本号:

1
2
3
4
5
write_basic_package_version_file(
${PROJECT_NAME}-config-version.cmake
VERSION ${PACKAGE_VERSION}
COMPATIBILITY AnyNewVersion
)

这两个文件也需要配置安装,用 install(FILES) 即可:

1
2
3
4
5
install(FILES
${PROJECT_BINARY_DIR}/${PROJECT_NAME}-config.cmake
${PROJECT_BINARY_DIR}/${PROJECT_NAME}-config-version.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)

如果正确安装后,这两个文件会出现在比如 /usr/lib/cmake/project-name/project-name-config.cmake/usr/lib/cmake/project-name/project-name-config-version.cmake

3. 头文件

下面的配置方式有一个前提,关于头文件的结构,按照我喜欢的方式的话,项目的头文件目录应该像这样:

1
2
3
4
5
project_root
└── include
   └── project_name
   ├── ***.hpp
   └── ***.hpp

大概是把 project_root 当作 PREFIX 的感觉,项目内引入头文件时要这样写:

1
#include "project_name/***.hpp"

这样组织的好处是,安装时只需要把 project_name 目录复制进 PREFIX/include 即可,安装后使用这个库时也不需要引入额外的头文件目录,因为 PREFIX/include 一般是默认的头文件目录之一,也就不需要告诉 CMake 使用这个库需要额外的头文件目录了

之前没有考虑配置安装或导出的项目可能会有这样的 CMake 代码:

1
target_include_directories(${PROJECT_NAME} PRIVATE ${${PROJECT_NAME}_INCLUDES})

这样写的话 CMake 会认为你的头文件不需要安装,所以要把 PRIVATE 改成 PUBLIC

1
target_include_directories(${PROJECT_NAME} PUBLIC ${${PROJECT_NAME}_INCLUDES})

这样写还有一个问题,你的头文件目录是一个绝对目录,可能是类似 /home/user/Projects/proj/include 的目录, CMake 不能智能地帮你处理好头文件目录,你需要用 BUILD_INTERFACEINSTALL_INTERFACE 生成表达式:

1
2
3
4
target_include_directories(${PROJECT_NAME} PUBLIC
$<BUILD_INTERFACE:${${PROJECT_NAME}_INCLUDES}> # 编译项目时引用项目下的头文件目录
$<INSTALL_INTERFACE:include> # 安装项目时引用 `<PREFIX>/include` ,比如 `/usr/include`
)

现在需要考虑头文件的安装方式, CMake 3.23 后加入了 target_sources(FILE_SET) 可以方便地管理头文件,但我选择了 CMake 3.9 版本,所以只能用相对原始的方式, i.e. install(DIRECTORY)

1
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

翻译过来就是把 <project_root>/include 下的 <project_name> 目录复制到 PREFIX/include 目录下,如果成功安装后,比如 /usr/include 目录下会出现 <project_name> 目录,目录内的结构与你的项目里的结构一模一样

4. 完整示例

比如项目叫 foo ,是一个库,库名也叫 foo ,项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
├── cmake
│   └── config.cmake.in
├── CMakeLists.txt
├── include
│   └── foo
│   ├── ***.hpp
│   └── ***.hpp
├── LICENSE
├── readme.md
└── src
   ├── CMakeLists.txt
   ├── ***.cpp
   └── ***.cpp

Config 文件模板 ./cmake/config.cmake.in 文件应该是这样的:

1
2
3
4
@PACKAGE_INIT@

include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@-targets.cmake")
check_required_components("@PROJECT_NAME@")

./CMakeLists.txt 中安装和导出相关的语句是这样的:

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
# 引入要用到的 CMake 模块
include(CMakePackageConfigHelpers)
include(GNUInstallDirs)

# 基本安装及 Targets 文件的生成
install(TARGETS ${PROJECT_NAME}
EXPORT ${PROJECT_NAME}-targets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

# Targets 文件的安装
install(EXPORT ${PROJECT_NAME}-targets
FILE ${PROJECT_NAME}-targets.cmake
NAMESPACE ${PROJECT_NAME}::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)

# Config 文件的导出(与安装?)
configure_package_config_file(
${PROJECT_SOURCE_DIR}/cmake/config.cmake.in
${PROJECT_BINARY_DIR}/${PROJECT_NAME}-config.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)

# ConfigVersion 文件的导出
write_basic_package_version_file(
${PROJECT_NAME}-config-version.cmake
VERSION ${PACKAGE_VERSION}
COMPATIBILITY AnyNewerVersion
)

# Config 和 ConfigVersion 文件的安装
install(FILES
${PROJECT_BINARY_DIR}/${PROJECT_NAME}-config.cmake
${PROJECT_BINARY_DIR}/${PROJECT_NAME}-config-version.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)

# 头文件的安装
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

废话

这么看的话 CMake 的安装和导出部分代码是可以与项目无关的,只要项目没有用 sub module 之类的奇葩方式引入其他库,估计这些代码就可以一直用到似,或者说 CMake 后面会有更方便的配置方式?

此页目录
CMake ——库的安装与导出