在 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
项目中,核心工作是:
- 定义库并安装目标文件(库、头文件等);
- 导出 CMake 配置文件(
starterConfig.cmake
、starterConfigVersion.cmake
、starterTargets.cmake
); - 提供一个
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_DIRS
、starter_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. 构建与安装流程
完整流程如下:
- 构建并安装库
cd starter
cmake -B build -S . -DCMAKE_INSTALL_PREFIX=../dist
cmake --build build --target install
- 在应用中使用库
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. 总结
通过本文,我们学习了如何:
- 将库和应用拆分为两个独立的项目;
- 在库项目中使用
install()
和export()
导出 CMake 配置; - 编写
starterConfig.cmake.in
文件(基础版 + 进阶版); - 在应用项目中通过
find_package()
查找并使用库; - 通过
CMAKE_PREFIX_PATH
指定库的安装路径。
这种方式是 C++ 库开发与分发的标准流程,不仅能实现代码封装,还能让库具备良好的可移植性和可复用性。