30、CMake实战项目:从零构建一个完整的C++项目,包含依赖管理、测试、安装、打包
终于到了最后一章。说实话,前面讲了那么多零散的知识点,都是为这一刻准备的。今天我们就真刀真枪地干一场——从零开始,搭一个完整的C++项目。这个项目会包含依赖管理、单元测试、安装脚本,最后还能打包成可分发的形式。
我个人习惯把这种实战当作“验收”。你想想看,学了那么多CMake指令,如果连一个完整的项目都搭不起来,那跟没学有什么区别?
项目背景与目标
我们要构建一个简单的数学工具库,叫 MathUtils。它提供几个基础函数:加法、乘法、阶乘。听起来简单对吧?但我们要让它支持外部依赖(比如用Google Test做测试)、支持安装到系统目录、支持打包成deb或rpm。
整个项目的目录结构是这样的:
MathUtils/
├── CMakeLists.txt # 顶层构建文件
├── src/
│ ├── CMakeLists.txt
│ ├── add.cpp
│ ├── mul.cpp
│ └── factorial.cpp
├── include/
│ └── math_utils/
│ ├── add.h
│ ├── mul.h
│ └── factorial.h
├── tests/
│ ├── CMakeLists.txt
│ └── test_math.cpp
├── cmake/
│ └── FindMathUtils.cmake # 自定义查找模块(演示用)
└── packaging/
└── CPackConfig.cmake # 打包配置
嗯,这里要注意:include/math_utils/ 这种嵌套目录是C++项目的常见做法,防止头文件冲突。
第一步:顶层CMakeLists.txt
顶层文件是整个项目的“大脑”。我一般会在这里定义项目名称、版本、语言标准,还有全局的编译选项。
cmake_minimum_required(VERSION 3.16)
project(MathUtils VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# 让CMake能找到我们的头文件
include_directories(${PROJECT_SOURCE_DIR}/include)
# 开启测试
option(BUILD_TESTING "Build tests" ON)
if(BUILD_TESTING)
enable_testing()
add_subdirectory(tests)
endif()
add_subdirectory(src)
这里有个小细节:CMAKE_CXX_EXTENSIONS OFF 是我个人的强迫症。我不喜欢编译器偷偷开GNU扩展,万一跨平台编译出问题,排查起来很头疼。
第二步:构建库文件(src/CMakeLists.txt)
库文件是项目的核心。我们把它编译成静态库,方便其他项目链接。
set(MATH_SOURCES
add.cpp
mul.cpp
factorial.cpp
)
add_library(MathUtils STATIC ${MATH_SOURCES})
# 指定头文件安装路径
target_include_directories(MathUtils PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# 安装库和头文件
install(TARGETS MathUtils
EXPORT MathUtilsTargets
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/math_utils
DESTINATION include
)
# 导出CMake配置,方便其他项目用 find_package 找到我们
install(EXPORT MathUtilsTargets
FILE MathUtilsConfig.cmake
NAMESPACE MathUtils::
DESTINATION lib/cmake/MathUtils
)
我曾经在导出配置时忘记加 NAMESPACE,结果其他项目链接时总报找不到目标。嗯,这个坑我踩过,你们别踩。
第三步:依赖管理——引入Google Test
测试是项目质量的保证。我们用Google Test来做单元测试。CMake 3.11之后支持 FetchContent,可以自动下载依赖,非常方便。
# tests/CMakeLists.txt
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
)
FetchContent_MakeAvailable(googletest)
add_executable(test_math test_math.cpp)
target_link_libraries(test_math PRIVATE MathUtils gtest gtest_main)
add_test(NAME MathTests COMMAND test_math)
这里有个避坑指南:FetchContent_MakeAvailable 会直接引入Google Test的整个构建树。如果你的项目比较大,建议用 FetchContent_Populate 手动控制,避免变量污染。
测试代码长这样:
// tests/test_math.cpp
#include <gtest/gtest.h>
#include "math_utils/add.h"
#include "math_utils/mul.h"
#include "math_utils/factorial.h"
TEST(AddTest, PositiveNumbers) {
EXPECT_EQ(add(2, 3), 5);
EXPECT_EQ(add(0, 0), 0);
}
TEST(MulTest, Basic) {
EXPECT_EQ(mul(3, 4), 12);
EXPECT_EQ(mul(0, 5), 0);
}
TEST(FactorialTest, SmallValues) {
EXPECT_EQ(factorial(0), 1);
EXPECT_EQ(factorial(1), 1);
EXPECT_EQ(factorial(5), 120);
}
第四步:安装与打包
项目写完了,怎么给别人用?两种方式:安装到系统目录,或者打包成安装包。
安装我们已经在上面的 install() 指令里配置好了。运行 cmake --install . --prefix /usr/local 就能装到系统里。
打包则需要用CPack。我们在项目根目录下放一个 packaging/CPackConfig.cmake:
# packaging/CPackConfig.cmake
set(CPACK_PACKAGE_NAME "MathUtils")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A simple math utility library")
set(CPACK_PACKAGE_VENDOR "Your Name")
set(CPACK_GENERATOR "DEB;RPM;TGZ")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "your.email@example.com")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
然后在顶层 CMakeLists.txt 末尾加上:
include(packaging/CPackConfig.cmake)
之后运行 cpack,就能生成deb、rpm、tar.gz三种格式的安装包。我个人最喜欢TGZ,跨平台通用,解压即用。
第五步:自定义查找模块
为了让其他项目用 find_package(MathUtils) 找到我们,我们写了一个 FindMathUtils.cmake。虽然前面已经导出了 MathUtilsConfig.cmake,但这里演示一下传统查找模块的写法:
# cmake/FindMathUtils.cmake
find_path(MathUtils_INCLUDE_DIR
NAMES math_utils/add.h
PATHS /usr/local/include /usr/include
)
find_library(MathUtils_LIBRARY
NAMES MathUtils
PATHS /usr/local/lib /usr/lib
)
if(MathUtils_INCLUDE_DIR AND MathUtils_LIBRARY)
set(MathUtils_FOUND TRUE)
add_library(MathUtils::MathUtils UNKNOWN IMPORTED)
set_target_properties(MathUtils::MathUtils PROPERTIES
IMPORTED_LOCATION ${MathUtils_LIBRARY}
INTERFACE_INCLUDE_DIRECTORIES ${MathUtils_INCLUDE_DIR}
)
else()
set(MathUtils_FOUND FALSE)
endif()
使用时,其他项目的CMakeLists.txt里这样写:
list(APPEND CMAKE_MODULE_PATH "/path/to/MathUtils/cmake")
find_package(MathUtils REQUIRED)
target_link_libraries(myapp PRIVATE MathUtils::MathUtils)
整体构建流程
把上面所有文件准备好之后,构建流程是这样的:
mkdir build && cd build
cmake ..
make -j$(nproc)
ctest # 运行测试
cmake --install . --prefix ./install # 安装到本地目录
cpack # 打包
你会看到测试全部通过,然后安装目录下出现了 include 和 lib 文件夹,最后在 build 目录下生成了 MathUtils-1.0.0-Linux.deb 等安装包。
核心要点回顾:
- 用
FetchContent管理第三方依赖,自动下载Google Test - 用
install()和export()让项目可安装、可被其他项目发现 - 用
CPack一键打包成多种格式 - 自定义
FindXxx.cmake模块作为传统查找方式的补充
个人建议: 如果你的项目要开源,强烈建议把 FindXxx.cmake 和 XxxConfig.cmake 都提供。前者兼容老项目,后者是CMake官方推荐的新方式。我自己的开源库都是两套都配,用户想用哪种都行。
注意: 打包时记得检查 CPACK_GENERATOR 是否包含你需要的格式。Linux上默认支持DEB和RPM,但需要系统安装相应的打包工具(如 dpkg-deb、rpmbuild)。macOS上可以加 DragNDrop 生成dmg。
知识体系图
下面这张图展示了本章涉及的核心模块和它们之间的关系:
从图中可以看出,顶层CMakeLists.txt是总控,它调度了三个子模块:库构建、测试、安装导出。打包环节则独立于构建流程,通过CPack配置生成最终的可分发文件。
好了,整个项目从零到一,该有的都有了。你完全可以拿这个模板去套你自己的项目,把函数名换一换,依赖换一换,就是一个生产级别的CMake项目。
记住:好的CMake项目,不是代码写得多花哨,而是别人拿过去能直接编译、测试、安装、打包,不用问你怎么用。这才是真正的“工程化”。
公众号:蓝海资料掘金营,微信deep3321