30、实战项目:从零构建一个跨平台C++项目(库+可执行文件+测试+安装+CPack打包+CI集成)
终于到了这一章。说实话,前面讲了那么多CMake的语法和技巧,如果你没有亲手把一个项目从零搭起来,那些知识始终是散的。我个人习惯,学完一个工具链之后,一定要找个周末,关掉所有干扰,完整地跑一遍“从代码到交付”的全流程。
这一章,我们就来干这件事。我会带着你,从一个空目录开始,一步步构建出一个包含静态库、可执行文件、单元测试、安装规则、CPack打包、以及GitHub Actions CI集成的完整项目。你想想看,这才是真实世界里一个C++库该有的样子。
30.1 项目结构设计
先别急着写代码。我每次开始一个新项目,第一件事是在纸上画目录结构。好的结构能让你少踩很多坑。下面是我推荐的一种布局:
my_project/
├── CMakeLists.txt # 顶层CMake文件
├── cmake/
│ └── MyProjectConfig.cmake.in # 用于安装后的find_package支持
├── include/
│ └── my_project/
│ └── calculator.h # 公共头文件
├── src/
│ ├── CMakeLists.txt
│ ├── calculator.cpp # 库的实现
│ └── main.cpp # 可执行文件入口
├── tests/
│ ├── CMakeLists.txt
│ └── test_calculator.cpp # 单元测试
├── scripts/
│ └── ci_build.sh # CI脚本(可选)
└── .github/
└── workflows/
└── ci.yml # GitHub Actions配置
嗯,这里要注意:include/my_project/ 这种嵌套目录,是为了防止头文件冲突。我在项目中遇到过好几次,不同库都叫 utils.h,结果链接时一片混乱。从那以后,我强制要求所有公共头文件都放在以项目名命名的子目录下。
30.2 顶层CMakeLists.txt:项目的“大脑”
顶层文件负责定义项目、设置C++标准、添加子目录。这是整个构建系统的入口。我习惯把版本号、许可证、以及一些全局编译选项都放在这里:
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# 让install导出的目标可以被find_package找到
include(GNUInstallDirs)
# 添加子目录
add_subdirectory(src)
# 如果开启了测试,添加测试子目录
option(BUILD_TESTS "Build unit tests" ON)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# 安装配置
install(
TARGETS my_project_lib
EXPORT MyProjectTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/my_project
)
install(
EXPORT MyProjectTargets
FILE MyProjectTargets.cmake
NAMESPACE MyProject::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject
)
# CPack打包配置
set(CPACK_PACKAGE_NAME "MyProject")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_GENERATOR "TGZ;DEB;RPM")
include(CPack)
为什么要把 BUILD_TESTS 做成一个option?因为CI环境里你可能只想跑测试,而用户安装时根本不需要测试代码。说白了,这就是“关注点分离”。
30.3 库的实现:src/CMakeLists.txt
库的CMake文件相对简单。我习惯把库和可执行文件分开定义,这样测试和安装都能复用库目标:
# 定义库
add_library(my_project_lib STATIC
calculator.cpp
)
target_include_directories(my_project_lib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include>
$<INSTALL_INTERFACE:include>
)
set_target_properties(my_project_lib PROPERTIES
PUBLIC_HEADER "../include/my_project/calculator.h"
)
# 定义可执行文件
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE my_project_lib)
这里有个细节:$<BUILD_INTERFACE> 和 $<INSTALL_INTERFACE> 是生成器表达式。它们的作用是——在构建时用源码目录的头文件,在安装后则用安装目录的头文件。我曾经因为没写这个,导致安装后的项目找不到头文件,排查了半天。
30.4 单元测试:tests/CMakeLists.txt
测试我推荐用 FetchContent 拉取Google Test。这样不需要把第三方库的源码提交到你的仓库里:
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_calculator test_calculator.cpp)
target_link_libraries(test_calculator PRIVATE
my_project_lib
GTest::gtest_main
)
add_test(NAME CalculatorTests COMMAND test_calculator)
为什么用 GTest::gtest_main 而不是 GTest::gtest?因为前者自带main函数,你只需要写测试用例就行。少写一行是一行,对吧?
30.5 安装与CPack打包
安装规则已经在顶层CMakeLists.txt里定义好了。你只需要运行:
mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local
cmake --build .
cmake --install .
然后CPack打包就更简单了:
cpack -G TGZ # 生成tar.gz
cpack -G DEB # 生成Debian包
cpack -G RPM # 生成Red Hat包
嗯,这里要注意:生成DEB和RPM包时,你的系统需要安装相应的打包工具(dpkg-dev 或 rpm-build)。我在Ubuntu上第一次打包DEB时,就因为没装 dpkg-dev 而报错,折腾了半小时。
30.6 CI集成:GitHub Actions
最后,我们把整个流程自动化。在 .github/workflows/ci.yml 里写入:
name: CI
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
build_type: [Debug, Release]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Configure
run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
- name: Build
run: cmake --build build --config ${{ matrix.build_type }}
- name: Test
run: ctest --test-dir build --output-on-failure
- name: Package
run: cpack --config build/CPackConfig.cmake -G TGZ
这个配置会在三个操作系统上,分别以Debug和Release模式构建、测试、打包。说白了,一次提交,全平台验证。我现在的项目都这么配,省心。
30.7 知识体系总览
下面这张图,把整个项目的构建流程和各个模块的关系画了出来。你可以把它当作一个“地图”,以后写项目时照着这个结构来:
30.8 避坑指南
最后,分享几个我亲身踩过的坑:
- 路径问题: 我曾经把
CMAKE_INSTALL_PREFIX设成了相对路径,结果安装时文件散落一地。记住,安装路径一定要用绝对路径。 - 测试找不到库: 如果测试链接了库,但库还没构建完,ctest会报错。解决办法是在
add_test之前确保库目标已经存在。 - CPack版本号不一致: 我见过有人手动在CPack配置里写版本号,结果和项目版本号对不上。直接用
${PROJECT_VERSION}变量,一劳永逸。 - CI中Windows路径反斜杠: 在Windows上,CMake生成的路径是反斜杠,但某些shell脚本不认。用
cmake -E命令代替原生shell命令,可以跨平台。
好了,这一章的内容就到这里。你现在应该有了一个完整的、可以直接上手的跨平台C++项目模板。把它保存好,以后每次开新项目,都可以从这里开始。