CppStack

C++技术栈一站式学习 · ‌业精于勤,荒于嬉;行成于思,毁于随。

CMake 进阶:构建与封装 C++ 库:从发布到使用

Tags = [ CMake ]

在 C++ 工程开发中,随着项目规模的扩大,公共功能往往需要封装成独立的库(Library),以便在多个应用中复用。CMake 提供了完整的支持:

  • 在库项目中通过 install()export() 导出配置文件;
  • 在应用项目中通过 find_package() 查找并使用库。

本文将介绍如何拆分项目结构,并实现一个完整的 库发布与应用使用 的流程。

1. 项目拆分思路

我们将项目拆分为两个独立的部分:

starter/          # 库项目
  ├── CMakeLists.txt
  └── src/...
client-app/       # 应用项目
  ├── CMakeLists.txt
  └── src/...
  • starter:封装好的 C++ 库,支持安装与导出。
  • client-app:依赖 starter 的应用,通过 find_package() 使用库。

这样做的好处是:

  • 库和应用完全解耦,可以独立开发、测试和发布;
  • 库可以被多个应用复用;
  • 应用只需要通过 find_package() 即可使用库,无需关心库的源码。

2. 在库项目中配置安装与导出

starter 项目中,核心工作是:

  1. 定义库并安装目标文件(库、头文件等);
  2. 导出 CMake 配置文件(starterConfig.cmakestarterConfigVersion.cmakestarterTargets.cmake);
  3. 提供一个 starterConfig.cmake.in 模板文件,供 configure_package_config_file() 生成最终的配置文件。

2.1 安装库与头文件

CMakeLists.txt 中:

# 定义库
add_library(starter ...)

# 安装库和头文件
install(TARGETS starter
    EXPORT starterTargets
    RUNTIME DESTINATION bin       # Windows 下 DLL 放在 bin
    LIBRARY DESTINATION lib       # Linux 下 so 放在 lib
    ARCHIVE DESTINATION lib       # 静态库放在 lib
)
install(DIRECTORY include/ DESTINATION include)

安装完成后,目录结构大致如下:

dist/
├── bin/                # Windows 下 DLL
├── lib/                # Linux 下 so / Windows 下 lib
├── include/            # 头文件
└── lib/cmake/starter/  # CMake 配置文件

2.2 导出 Targets

继续在 CMakeLists.txt 中添加:

# 导出 CMake Targets
install(EXPORT starterTargets
    FILE starterTargets.cmake
    NAMESPACE starter::
    DESTINATION lib/cmake/starter
)

这样会生成 starterTargets.cmake,里面定义了 starter::starter 这个 target。


2.3 starterConfig.cmake.in 模板

这是多数人最困惑的部分。一个最小可用的写法是:

@PACKAGE_INIT@

# 加载导出的 targets 文件
include("${CMAKE_CURRENT_LIST_DIR}/starterTargets.cmake")

如果希望兼容一些老项目(它们可能习惯使用 starter_INCLUDE_DIRSstarter_LIBRARIES 变量),可以写成进阶版:

@PACKAGE_INIT@

# 设置 include 路径
set_and_check(starter_INCLUDE_DIR "@PACKAGE_INCLUDE_INSTALL_DIR@")

# 提供兼容变量
set(starter_INCLUDE_DIRS "${starter_INCLUDE_DIR}")
set(starter_LIBRARIES starter::starter)

# 加载导出的 targets 文件
include("${CMAKE_CURRENT_LIST_DIR}/starterTargets.cmake")

2.4 生成并安装配置文件

CMakeLists.txt 中继续添加:

include(CMakePackageConfigHelpers)

# 生成 starterConfig.cmake
configure_package_config_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/cmake/starterConfig.cmake.in"
    "${CMAKE_CURRENT_BINARY_DIR}/starterConfig.cmake"
    INSTALL_DESTINATION lib/cmake/starter
    PATH_VARS CMAKE_INSTALL_INCLUDEDIR
)

# 生成版本文件
write_basic_package_version_file(
    "${CMAKE_CURRENT_BINARY_DIR}/starterConfigVersion.cmake"
    COMPATIBILITY SameMajorVersion
)

# 安装配置文件
install(FILES
    "${CMAKE_CURRENT_BINARY_DIR}/starterConfig.cmake"
    "${CMAKE_CURRENT_BINARY_DIR}/starterConfigVersion.cmake"
    DESTINATION lib/cmake/starter
)

安装完成后,最终目录结构如下:

dist/
├── bin/
│   └── starter.dll (Windows)
├── lib/
│   ├── libstarter.so (Linux)
│   ├── starter.lib (Windows import lib)
│   └── cmake/starter/
│       ├── starterConfig.cmake
│       ├── starterConfigVersion.cmake
│       └── starterTargets.cmake
└── include/

至此,库的安装与导出就完成了。


3. 在应用项目中使用库

client-app 项目中,只需要:

cmake_minimum_required(VERSION 3.15)
project(client-app)

# 查找 starter 库
find_package(starter REQUIRED)

# 定义可执行文件
add_executable(client ...)

# 链接 starter 库
target_link_libraries(client PRIVATE starter::starter)

只要 starter 已经安装到 dist/,我们就可以在配置时指定路径:

cmake -B build -S . -DCMAKE_PREFIX_PATH=/path/to/starter/dist

CMake 会自动在 dist/lib/cmake/starter/ 下找到 starterConfig.cmake,并完成依赖解析。


4. 构建与安装流程

完整流程如下:

  1. 构建并安装库
cd starter
cmake -B build -S . -DCMAKE_INSTALL_PREFIX=../dist
cmake --build build --target install
  1. 在应用中使用库
cd client-app
cmake -B build -S . -DCMAKE_PREFIX_PATH=../dist
cmake --build build

这样,client-app 就能正确找到并链接 starter 库。


4.5 starter 库的完整 CMakeLists.txt

下面给出一个完整的 starter/CMakeLists.txt 示例,可供参考使用:

cmake_minimum_required(VERSION 3.15)
project(starter VERSION 1.0.0 LANGUAGES CXX)

include(GNUInstallDirs)

# 定义库
add_library(starter src/starter.cpp)
target_include_directories(starter PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

# 安装库和头文件
install(TARGETS starter
    EXPORT starterTargets
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}   # Windows DLL
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}   # Linux so
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}   # 静态库
)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

# 导出 Targets
install(EXPORT starterTargets
    FILE starterTargets.cmake
    NAMESPACE starter::
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/starter
)

# 生成并安装 Config 文件
include(CMakePackageConfigHelpers)

configure_package_config_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/cmake/starterConfig.cmake.in"
    "${CMAKE_CURRENT_BINARY_DIR}/starterConfig.cmake"
    INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/starter
    PATH_VARS CMAKE_INSTALL_INCLUDEDIR
)

write_basic_package_version_file(
    "${CMAKE_CURRENT_BINARY_DIR}/starterConfigVersion.cmake"
    COMPATIBILITY SameMajorVersion
)

install(FILES
    "${CMAKE_CURRENT_BINARY_DIR}/starterConfig.cmake"
    "${CMAKE_CURRENT_BINARY_DIR}/starterConfigVersion.cmake"
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/starter
)

这样,starter 库就具备了完整的安装与导出能力。


5. 总结

通过本文,我们学习了如何:

  1. 将库和应用拆分为两个独立的项目;
  2. 在库项目中使用 install()export() 导出 CMake 配置;
  3. 编写 starterConfig.cmake.in 文件(基础版 + 进阶版);
  4. 在应用项目中通过 find_package() 查找并使用库;
  5. 通过 CMAKE_PREFIX_PATH 指定库的安装路径。

这种方式是 C++ 库开发与分发的标准流程,不仅能实现代码封装,还能让库具备良好的可移植性和可复用性。