30、综合实战:从零构建一个中型C++项目
终于到了最后一章。说实话,前面讲了那么多零散的知识点,你可能已经有点晕了。别担心,这一章我们把所有东西串起来——从零开始,完整搭建一个真正能用的中型C++项目。
这个项目包含:一个核心库、一个可执行文件、单元测试、安装脚本、打包配置,还有CI自动化。说白了,就是你在公司里会遇到的真实项目结构。
本章目标: 用CMake完整构建一个可交付的C++项目,覆盖开发到部署全流程。
项目结构设计
先看看我们要搭什么。我个人习惯把项目分成几个清晰的层级:
my_project/
├── CMakeLists.txt # 顶层构建文件
├── cmake/
│ └── MyProjectConfig.cmake.in # 安装后的CMake配置模板
├── src/
│ ├── CMakeLists.txt
│ ├── main.cpp # 可执行文件入口
│ └── app_version.h.in # 版本号模板
├── lib/
│ ├── CMakeLists.txt
│ ├── include/
│ │ └── mylib/
│ │ ├── core.h
│ │ └── utils.h
│ └── src/
│ ├── core.cpp
│ └── utils.cpp
├── tests/
│ ├── CMakeLists.txt
│ ├── test_core.cpp
│ └── test_utils.cpp
├── packaging/
│ └── CPackConfig.cmake # 打包配置
└── .github/
└── workflows/
└── ci.yml # GitHub Actions配置
嗯,这个结构你可能觉得眼熟。我在公司里见过不少项目都长这样——库和可执行文件分开,测试独立,打包和CI各归其位。
顶层CMakeLists.txt
顶层文件是整个项目的入口。它负责定义项目、设置全局选项、然后添加子目录。
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)
# 构建类型
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "构建类型" FORCE)
endif()
# 选项
option(BUILD_TESTS "构建测试" ON)
option(BUILD_SHARED_LIBS "构建动态库" OFF)
# 添加子目录
add_subdirectory(lib)
add_subdirectory(src)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# 安装规则
include(GNUInstallDirs)
install(
TARGETS mylib myapp
EXPORT MyProjectTargets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
install(
EXPORT MyProjectTargets
FILE MyProjectTargets.cmake
NAMESPACE MyProject::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject
)
# 打包配置
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(cmake/CPackConfig.cmake)
这里有个小细节:CMAKE_CXX_EXTENSIONS OFF。我遇到过好几次因为编译器扩展导致跨平台编译失败的情况,所以现在习惯把它关掉。
库模块:lib/CMakeLists.txt
库是项目的核心。我们把它做成一个独立的模块,方便复用。
# 头文件目录
set(MYLIB_PUBLIC_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/include/mylib/core.h
${CMAKE_CURRENT_SOURCE_DIR}/include/mylib/utils.h
)
# 源文件
set(MYLIB_SOURCES
src/core.cpp
src/utils.cpp
)
# 创建库
add_library(mylib
${MYLIB_SOURCES}
${MYLIB_PUBLIC_HEADERS}
)
# 包含路径
target_include_directories(mylib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
# 安装头文件
install(
DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
FILES_MATCHING PATTERN "*.h"
)
注意那个$<BUILD_INTERFACE>和$<INSTALL_INTERFACE>的写法。这是CMake的生成器表达式,说白了就是让头文件路径在构建时和安装后自动切换。我第一次用的时候也觉得麻烦,但后来发现这其实是保证项目可移植的关键。
可执行文件:src/CMakeLists.txt
可执行文件依赖库,同时生成版本信息。
# 配置版本头文件
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/app_version.h.in
${CMAKE_CURRENT_BINARY_DIR}/app_version.h
)
# 创建可执行文件
add_executable(myapp
main.cpp
${CMAKE_CURRENT_BINARY_DIR}/app_version.h
)
# 链接库
target_link_libraries(myapp PRIVATE mylib)
# 包含路径
target_include_directories(myapp PRIVATE
${CMAKE_CURRENT_BINARY_DIR}
)
版本头文件模板长这样:
#ifndef APP_VERSION_H
#define APP_VERSION_H
#define APP_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define APP_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define APP_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define APP_VERSION_STRING "@PROJECT_VERSION@"
#endif
为什么用configure_file?因为这样版本号只需要在顶层改一次,所有地方自动同步。我曾经见过有人手动维护三个地方的版本号,结果经常对不上——嗯,那场面挺尴尬的。
测试模块:tests/CMakeLists.txt
测试我们用Google Test。当然你也可以用Catch2或者别的,原理一样。
# 查找或下载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_core test_core.cpp)
target_link_libraries(test_core PRIVATE mylib gtest_main)
add_executable(test_utils test_utils.cpp)
target_link_libraries(test_utils PRIVATE mylib gtest_main)
# 注册测试
add_test(NAME TestCore COMMAND test_core)
add_test(NAME TestUtils COMMAND test_utils)
这里用FetchContent自动下载Google Test,省去了手动安装的麻烦。不过要注意:如果你的项目需要离线构建,最好把依赖提前下载好放到本地。
打包配置:packaging/CPackConfig.cmake
打包我们用CPack,它支持生成多种格式的安装包。
set(CPACK_GENERATOR "TGZ;DEB;RPM")
set(CPACK_PACKAGE_NAME "myproject")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "一个示例C++项目")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_PACKAGE_CONTACT "developer@mycompany.com")
# DEB包特定配置
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "developer@mycompany.com")
set(CPACK_DEBIAN_PACKAGE_SECTION "devel")
# RPM包特定配置
set(CPACK_RPM_PACKAGE_GROUP "Development/Libraries")
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
打包命令很简单:
cmake --build . --target package
然后你就能在构建目录下看到.tar.gz、.deb、.rpm文件了。我经常用这个给客户发SDK,省心不少。
CI配置:.github/workflows/ci.yml
最后是CI。我们用GitHub Actions,每次推送代码自动构建、测试、打包。
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 安装依赖
run: sudo apt-get update && sudo apt-get install -y cmake g++
- name: 配置
run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=ON
- name: 构建
run: cmake --build build --parallel
- name: 测试
run: cd build && ctest --output-on-failure
- name: 打包
run: cmake --build build --target package
- name: 上传构建产物
uses: actions/upload-artifact@v3
with:
name: packages
path: build/*.tar.gz
这个配置我用了很久,基本没出过问题。唯一要注意的是:如果项目依赖第三方库,记得在安装依赖那一步提前装好。
核心流程一览
下面这张图展示了整个项目的构建流程:
从源码到最终交付,CMake把整个流程串了起来。你只需要写一次配置,剩下的交给工具链自动完成。
避坑指南
最后分享几个我踩过的坑:
- 路径问题: 我曾经在Windows上写死路径,结果换到Linux全崩了。后来统一用
${CMAKE_CURRENT_SOURCE_DIR}这类变量,再也没出过问题。 - 测试依赖: 如果测试依赖外部库,记得在CI里提前安装。我遇到过测试编译通过但运行时找不到动态库的情况——嗯,那排查起来挺费劲的。
- 版本号同步: 用
configure_file自动生成版本头文件,别手动维护。血的教训。 - 打包格式: 不同平台用不同格式。Linux用DEB/RPM,macOS用DMG,Windows用NSIS。CPack都支持,但需要单独配置。
小技巧: 在项目根目录放一个build.sh脚本,把配置、构建、测试、打包一条命令搞定。团队里其他人用起来会感谢你的。
注意: 如果项目要开源,记得在CI里加上cmake --build build --target package_source,生成源码包。很多用户喜欢自己编译。
好了,这一章的内容就到这里。从零到一,从代码到交付,你应该已经掌握了用CMake管理中型C++项目的完整流程。剩下的就是动手实践了——相信我,写一遍比看十遍管用。
公众号:蓝海资料掘金营,微信deep3321