CMake ——库的安装与导出
如何成为 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.cmake和project-name-targets.cmake都是正确的命名方式,我选择 dash-case
因为现在站在了库的开发者视角,提供 project-name-targets.cmake 才是正确的导出做法, CMake 提供了一个方便的生成 targets.cmake 的方式:
1 | install(TARGETS ${PROJECT_NAME} |
其实这句是用来安装你的 target 的,包括你的库文件、可执行程序等,只不过其中的 EXPORT ${PROJECT_NAME}-targets 会使 CMake 帮你生成一个 targets.cmake ,位置在 ${PROJECT_BINARY_DIR}/CMakeFiles/Export/*/project-name-targets.cmake ,但显式写出 bin, lib, include 目录有些原始,可以借助 GNUInstallDirs 模块:
1 | include(GNUInstallDirs) # 引入 CMAKE_INSTALL_*DIR 变量 |
生成后需要配置安装,可以用 install(EXPORT) :
1 | install(EXPORT ${PROJECT_NAME}-targets |
如果正确安装后,一份 project-name-targets.cmake 会出现在比如 /usr/lib/cmake/project-name/project-name-targets.cmake ,用户现在可以通过 find_package(project-name) 来找到你的库了,因为加了 NAMESPACE ${PROJECT_NAME}:: ,用户在使用时需要像这样写:
1 | find_package(project-name) |
2. Config 文件
Config 文件用来保证用户在 find_package 之后真的配置好了库的所有细节,官方推荐的做法是写一个 project-name-config.cmake ,这里用 *.in 文件生成:
1 | @PACKAGE_INIT@ |
我的喜好是把这个文件放进 project_root/cmake/config.cmake.in ,然后在主 CMakeLists.txt 中这样输出实际的 Config 文件:
1 | include(CMakePackageConfigHelpers) |
这里的
INSTALL_DESTINATION貌似没什么用,后面还要手动配置安装方式,但官方这么写应该有官方的道理
官方还推荐写一个 ConfigVersion 文件,用来配置库的版本号:
1 | write_basic_package_version_file( |
这两个文件也需要配置安装,用 install(FILES) 即可:
1 | install(FILES |
如果正确安装后,这两个文件会出现在比如 /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这样组织的好处是,安装时只需要把
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_INTERFACE 和 INSTALL_INTERFACE 生成表达式:
1 | target_include_directories(${PROJECT_NAME} PUBLIC |
现在需要考虑头文件的安装方式, 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 | . |
Config 文件模板 ./cmake/config.cmake.in 文件应该是这样的:
1 | @PACKAGE_INIT@ |
./CMakeLists.txt 中安装和导出相关的语句是这样的:
1 | # 引入要用到的 CMake 模块 |
废话
这么看的话 CMake 的安装和导出部分代码是可以与项目无关的,只要项目没有用 sub module 之类的奇葩方式引入其他库,估计这些代码就可以一直用到似,或者说 CMake 后面会有更方便的配置方式?