多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

C++项目自动化构建与部署:GitLab+Arbess完整搭建设计

C++项目自动化构建与部署:GitLab+Arbess完整搭建设计 1. 先说说我把C项目搬上自动化构建的真实动机前两年带个小团队维护几个C服务端程序发布基本靠人肉链路开发在自己机器上编好压缩包传到服务器手动停服务、替换文件、再启动。版本一多就开始乱有一次漏拷了配置文件线上直接报警大家查了半小时才发现是部署目录里的 ini 是旧版。那次之后我下定决心把 C 项目的自动化构建与主机部署整套流程接到 Arbess GitLab 上用两天时间搭完之后只要代码推上去剩下的编译、测试、打包、部署全部自动跑。这套组合到现在用了很久今天把搭建思路和踩过的坑完整写出来给同样维护C项目的朋友做个参考。1.1 手工人肉构建的日子在没有自动化流水线之前我们的流程看起来很“规范”开发在本地拉分支、改代码、本地编译验证然后手动合并到 develop部署时挑一个“信得过”的人远程登服务器操作。问题不在某一次操作而在于这套流程每次都要靠人记住所有细节。C项目不像Web前端那种打完包传上去就行它涉及编译环境、第三方依赖、运行库、配置文件、服务重启顺序漏掉任何一步都可能在线上暴雷。我统计过当时发布一次平均要 40 分钟其中真正拷贝文件不到 5 分钟大部分时间花在“确认改了哪个模块、要不要重新编译依赖、本地和服务器环境是否一致”这些事上。最难受的是换人新同事第一次做发布光教他“先备份、再停服务、拷贝哪些目录、改哪个配置”就讲了半天。后来我给自己定了个目标不管谁来维护只要代码进了 GitLab 的 master 分支就自动完成“构建 - 测试 - 打包 - 部署到指定主机 - 重启服务 - 通知结果”。人只负责 push 代码机器负责干剩下的活。1.2 GitLab管代码、Arbess管调度、C管产出先说清楚这套组合里每个工具负责什么很多人一开始就把角色搞混了。GitLab 在这里的核心作用是代码托管和事件源头它不负责编译也不负责部署。我们看中的是它的 Merge Request 审查、分支权限、Tag 管理和 Webhook 能力。当有代码 push 上去或者有人点了“创建 Release Tag”GitLab 会把这些事件通过 Webhook 推给下游工具。Arbess 是 CI/CD 调度中心负责接收 GitLab 推过来的事件按照预先配置好的流水线编排去执行构建、测试、部署步骤。你可以理解为它是个“导演”GitLab 只是通知它“有新剧本了”接下来演员怎么走位全是 Arbess 在调度。C 项目本身要做的是配合这套流程改造成“能在任意干净机器上编译”的标准化工程。这个改造很关键后面专门说。1.3 这套组合适合谁如果你维护的是以下这类项目这套方案的参考价值最大用 C 写的 Windows 服务、桌面工具、内部中间件需要频繁迭代。项目托管在 GitLab但不想被绑定到某一家 CI 平台。部署目标就是普通 Windows/Linux 主机不是容器集群。团队规模不大希望用尽量少的维护成本把发布这件事稳住。我当时之所以没选 GitLab CI 自带的 Runner 直接跑而是引入 Arbess是因为我们除了构建之外还有很多主机部署动作远程拷贝文件、备份目录、停服务、改配置、起服务、探测健康状态。Arbess 把这些步骤可视化地编排出来比写一堆 .gitlab-ci.yml 更直观而且不同项目之间可以复用同一套部署脚本。2. 从零搭环境GitLab、Arbess、构建机、部署机四件套搭建这套环境之前我建议你先画一张网络拓扑图把各个组件的部署位置和访问关系写清楚。别嫌这一步麻烦后面排查 Webhook 不通、部署超时时这张图能帮你省很多时间。2.1 各组件的角色和网络关系我当时的部署形态是这样的组件角色关键点GitLab CE代码仓库和事件源Merge Request、Push、Tag 都会触发 WebhookArbessCI/CD 调度中心接收 Webhook编排流水线调度执行器构建机执行编译打包Windows Server 2019 VS Build Tools CMake Git部署机承载 C 业务服务Windows Server预装 VC 运行库构建机和部署机可以是同一台也可以分开。我强烈建议分开哪怕只是两台虚拟机。因为构建机经常装各种 SDK、更新编译工具链而部署机上跑着线上服务最忌讳的就是环境变动。曾经有人为了省机器把构建工具直接装在生产服务器上结果一次 CMake 升级把系统 PATH 搞乱了线上程序起不来教训很深。GitLab 和 Arbess 我装在两台 Linux 服务器上构建机和部署机是 Windows。Arbess 通过执行器不同版本里叫 Agent 或 Runner来连接 Windows 机器执行器需要在构建机和部署机上各装一个。构建机的执行器负责拉代码、跑编译脚本部署机的执行器负责跑部署脚本。2.2 GitLab与Arbess打通Token、Webhook、执行器打通分三步顺序不能乱。第一步在 GitLab 里创建一个 Access Token用于 Arbess 拉取代码。建议只给 read_repository 权限不要图省事用管理员 token。Token 过期时间要设置好我们踩过 token 过期导致流水线突然失败的坑后面细说。第二步在 Arbess 里创建项目绑定 GitLab 仓库地址填入 Token。Arbess 会生成一个 Webhook URL 和 Secret Token。第三步回到 GitLab 项目设置里的 Webhooks 页面把 Arbess 给的 URL 和 Secret 填进去勾选 Push events、Merge request events、Tag push events。保存后 GitLab 会显示一个“Test”按钮点一下测试如果 Arbess 那边收到事件说明链路通了。执行器的安装相对简单。在 Windows 机器上下载 Arbess 执行器安装包装完会要求填 Arbess 服务端地址和一段注册凭据凭据在 Arbess 的“执行器”页面生成。有一点要特别注意执行器服务默认可能是以本地系统账号运行的它拿不到你交互登录时那些环境变量后面编译失败大多和这个有关。2.3 C构建机上最容易被忽略的MSVC环境构建机上要装的东西比你想的多一点Git for Windows、CMake建议 3.21 以上、Visual Studio Build Tools以及项目第三方依赖。VS Build Tools 不用装完整版 Visual Studio体积小很多而且不会有 VS 自动更新把环境搞坏的风险。很多刚搭 CI 的人在这里都会卡一道本地能编译到了构建机上就报“找不到 MSBuild”或者“无法识别 C 编译器”。原因是执行器进程不是交互式登录没有加载 Visual Studio 开发者环境。普通命令行里能用的 cl、msbuild、环境变量在服务环境下不一定有。解决办法有两个。一是安装 Visual Studio Build Tools 之后在流水线脚本里先调用 VsDevCmd.bat 初始化环境二是直接把完整路径写死到脚本里。我采用的是前者因为 VsDevCmd 会自动把当前架构的编译工具链加进来省心。另外一个细节CMake 的生成器要显式指定。我用的命令是cmake -S . -B build -A x64-A x64表示目标平台不要只依赖默认值否则有时候会生成 32 位版本部署到 64 位系统上虽然能跑但性能和兼容性都受影响。3. 把C工程改造成“可以在任意机器上编”的标准化工程这一步是整个自动化能不能成立的前提。如果你的项目还依赖某个同事机器上的某个路径下的第三方库那永远谈不上自动化。3.1 统一编译入口CMakePresets我们的项目有多个子模块原来每个开发者的编译方式都不一样有人用 VS 打开 sln 点生成有人用命令行敲 NMake。为了让流水线有一个统一入口我引入了 CMakePresets.json。这个文件放在仓库根目录内容类似{ version: 3, configurePresets: [ { name: windows-x64-release, generator: Visual Studio 17 2022, architecture: x64, binaryDir: ${sourceDir}/build, cacheVariables: { CMAKE_BUILD_TYPE: Release } } ], buildPresets: [ { name: release, configurePreset: windows-x64-release, configuration: Release } ] }有了它不管在哪个环境只要执行cmake --preset windows-x64-release就能完成配置执行cmake --build --preset release就能编译。这比每个人都记一长串参数可靠得多。CMakePresets 还有一个好处团队里其他人也用同一套入口本地开发和流水线行为一致大大减少了“我这能编机器上不能编”的扯皮。3.2 构建、测试、打包一条命令跑完构建脚本我放在仓库的scripts\build.ps1里核心逻辑是$ErrorActionPreference Stop # 定位 Visual Studio 安装路径并进入开发者环境 $vswhere ${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe $vsPath $vswhere -latest -property installationPath if (-not $vsPath) { throw No Visual Studio installation found } Import-Module $vsPath\Common7\Tools\Microsoft.VisualStudio.DevShell.dll Enter-VsDevShell -VsInstallPath $vsPath -SkipAutomaticLocation -DevCmdArguments -archx64 # 配置、编译、测试 cmake --preset windows-x64-release if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } cmake --build --preset release --parallel if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } ctest --test-dir build -C Release --output-on-failure if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # 打包 cpack --config build/CPackConfig.cmake -B artifacts注意$LASTEXITCODE检查这是很多 PowerShell 脚本容易漏的地方。PowerShell 不像 bash 那样命令失败会自动中断如果$ErrorActionPreference设成 Stop 只对 cmdlet 生效外部 exe 的返回码不会触发异常必须手动检查。CMakeLists.txt 里需要加上安装规则和 CPack 配置install(TARGETS myapp RUNTIME DESTINATION bin) install(DIRECTORY config/ DESTINATION config) set(CPACK_GENERATOR ZIP) include(CPack)这样cpack就能生成一个 zip 包里面包含 exe、dll、配置文件整个产物是一个自包含目录部署时解压直接用。3.3 制品的目录约定与版本命名制品打包出来之后要有一个清晰的名字和目录约定。我用的命名格式是myapp-版本号-构建号.zip。版本号从代码里读取构建号用 Arbess 每次执行流水线自动生成的编号。Arbess 构建完成后制品会被归档到服务端的一个目录比如C:\ArbessArtifacts\myapp\123\123 是构建号。部署时从指定构建号目录取包这样每一次发布对应哪一次代码提交都能查。有人可能会问为什么不用 Git commit SHA 当版本号我的做法是两者都保留zip 文件名带可读版本号制品目录里放一个build-info.txt记录 Git SHA、构建时间、触发人。出问题的时候能快速定位到具体提交。4. Arbess流水线里的三个阶段到底怎么编排流水线我在 Arbess 里定义了三个阶段构建、测试、部署。名字听起来很普通但每个阶段里的细节才是关键。4.1 阶段划分构建-测试-部署三个阶段的依赖关系是测试必须等构建成功部署必须等测试成功。Arbess 里把阶段配置好之后默认就是这种串行依赖。如果你的项目测试很多、跑得慢可以把测试阶段拆分到多台机器上并行但我们当时的单元测试不到 5 分钟没必要。构建阶段做的事情上面已经写了拉代码、初始化环境、CMake 配置、编译、打包、把 zip 归档。测试阶段除了跑 ctest我还会额外跑一个参数冒烟测试就是启动程序跑几个典型场景检查退出码。部署阶段是这套流水线的核心价值。我的做法不是让 Arbess 直接远程执行 PowerShell而是通过部署机上的执行器来执行。部署机上放了一个deploy.ps1流水线调用它传入制品路径和部署目录参数。4.2 关键脚本Windows主机部署部分部署脚本的逻辑很简单但每一步都要稳。核心代码param( [string]$SourceDir, [string]$TargetDir, [string]$ServiceName ) $ErrorActionPreference Stop $backupDir $TargetDir\backup_$(Get-Date -Format yyyyMMdd_HHmmss) # 1. 停服务并等待进程退出 if (Get-Service -Name $ServiceName -ErrorAction SilentlyContinue) { Stop-Service -Name $ServiceName -Force Start-Sleep -Seconds 3 } $proc Get-Process -Name myapp -ErrorAction SilentlyContinue if ($proc) { Wait-Process -Id $proc.Id -Timeout 30 -ErrorAction SilentlyContinue } # 2. 备份当前目录 if (Test-Path $TargetDir) { Move-Item -Path $TargetDir -Destination $backupDir -Force } New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null # 3. 拷贝新制品 Copy-Item -Path $SourceDir\* -Destination $TargetDir -Recurse -Force # 4. 如果备份里有配置目录还原配置不随版本走 if (Test-Path $backupDir\config) { Copy-Item -Path $backupDir\config -Destination $TargetDir -Recurse -Force } # 5. 启动服务并做健康检查 if (Get-Service -Name $ServiceName -ErrorAction SilentlyContinue) { Start-Service -Name $ServiceName Start-Sleep -Seconds 10 if ((Get-Service $ServiceName).Status -ne Running) { throw Service $ServiceName failed to start } }这里有两个设计原则值得借鉴。第一程序目录和配置目录分离。部署时新代码整体替换配置从备份还原这样数据库连接串、端口号这些环境相关配置不会因为更新被冲掉。一开始我们没想到这个后来有过一次配置被覆盖的线上事故才改成现在这样。第二部署失败要立刻抛出异常。Arbess 拿到非零退出码就会把流水线标成失败然后触发通知而不是静默继续。我见过很多部署脚本不管结果看起来执行成功了实际上目标目录是空的。4.3 触发方式与失败通知流水线触发方式我配了三种Push 到 develop 分支时触发但不自动部署只做构建和测试Push 到 master 分支时触发完整的三阶段创建 Tag比如v1.2.3时也触发完整流程用来发布正式版本。这样日常开发提交不会动不动就自动发到生产环境。失败通知我们接的是钉钉机器人。Arbess 流水线失败时往群里的 Webhook 发一条消息包含项目名、构建号、失败阶段、日志链接。消息不要只给一句“构建失败”那样维护的人还得去翻日志。我把关键错误日志片段直接截取出来发群里虽然有一定信息泄露风险但团队内部工具可以接受。你也可以用邮件或者直接查 Arbess 后台看团队习惯。5. 跑通流水线后我踩过的四个真实坑自动化流程刚跑通的前两周几乎每天都有新问题。这些坑我逐一记录下来排查思路比答案本身更值得参考。5.1 MSBuild找不到因为VS开发者环境没初始化这个坑在前面提到过这里说完整链路。现象是流水线日志里出现CMake Error: Could not find any instance of Visual Studio或者编到一半报MSBuild is not found。我当时的排查步骤在构建机上手动跑跟流水线一样的命令发现能通过。这就说明代码和脚本本身没问题。对比手动环境和执行器环境的差异发现执行器服务运行时没有加载 VsDevCmd 的环境变量。手动跑cmd /k C:\Program Files\Microsoft Visual Studio\2022\BuildTools\Common7\Tools\VsDevCmd.bat -archx64之后再执行 cmake成功。最终修改脚本在每次构建前主动进入 VS 开发者环境。这个问题本质上是“环境漂移”。本机能编是因为你登录时已经初始化了环境自动化任务跑在服务上下文里环境是干净的。解决之后我养成了一个习惯任何构建脚本的第一条命令一定是环境初始化绝不依赖机器的默认配置。5.2 离线C程序在目标主机上双击没反应有一次流水线全绿部署也显示成功但目标主机上的程序双击后毫无反应进程列表里也没有。第一反应是拷贝的文件不全后来发现根本不是。排查过程是这样的到部署机上把制品目录里所有文件列出来exe、dll、配置文件都在。在部署机上直接命令行启动 exe没有输出也没报错。打开 Windows 事件查看器在应用程序日志里看到无法找到 VCRUNTIME140.dll。罪魁祸首是部署机没有安装 Microsoft Visual C Redistributable。这个坑非常典型。C 程序如果是动态链接到运行时库/MD目标机器就必须装对应版本的 VC 运行库。构建机上因为有 VS Build Tools运行库齐全所以编译和本地测试都正常但干净的目标主机上没有程序自然起不来。我的处理方式是两种手段并用内部工具优先在 CMake 里设置CMAKE_MSVC_RUNTIME_LIBRARYMultiThreaded也就是静态链接运行库这样 exe 不依赖 VC 运行库拷过去就能跑对外发布的程序则保留动态链接但在部署脚本里增加一步检测如果没有运行库就先静默安装对应的 Redistributable 包。检测运行库的小工具网上有现成的自己用dumpbin /dependents也能查依赖。5.3 制品被进程占用导致部署步骤失败这个问题出现得很隐蔽。部署时报Copy-Item : 进程无法访问文件因为另一个程序正在使用。理论上我们先停了服务怎么会占用实际排查结果有几种可能服务停止了但程序启动时又拉起了一个子进程服务管理器和子进程没绑定。程序通过 Windows 计划任务定时被唤起不在服务管理器控制范围内。Windows Defender 实时扫描正在读 exe 文件导致短暂锁定。我当时把 Stop-Service 之后又加了Get-Process -Name myapp | Stop-Process -Force并且等待几秒就解决了一大半。对于杀毒软件锁文件我把部署目录加入了 Defender 排除列表。注意别为了省事直接停掉 Defender不值得。另外进程占用往往意味着程序里还有文件句柄没有释放。尤其 C 程序如果启动了后台线程写日志日志文件一直打开替换目录就会失败。所以部署脚本里的日志文件建议单独放在一个固定目录不要跟程序目录混在一起。程序目录只管替换二进制文件日志目录永远不动问题一下少很多。5.4 Webhook时灵时不灵排查链路是怎样的还有一次比较抓狂GitLab 上点一下 push流水线偶尔触发偶尔不触发。查 Arbess 服务端日志什么都没收到。这个问题的根因是 GitLab 出于安全考虑默认会阻止 Webhook 发送到本地网络地址。如果 GitLab 和 Arbess 部署在同一台内网机器Webhook URL 是http://192.168.x.x:port或者http://127.0.0.1:portGitLab 会把它当成 SSRF 风险直接拦掉。排查链路GitLab 项目 - Settings - Webhooks - 找到对应 Webhook点开 Recent deliveries能看到每次投递记录和响应码。如果一直显示 500 或者 connection failure说明请求可能根本没到 Arbess。查看 GitLab 管理后台的 Network - Outbound requests 选项把“Allow requests to the local network from webhooks and integrations”打开再把 Arbess 地址加入白名单。保存后再测流水线马上就通了。另一种“时灵时不灵”是 Secret Token 不一致。Arbess 生成的 token 和 GitLab Webhook 里填的如果对不上GitLab 投递记录里会显示 403。我后来把 token 统一放在团队的密码管理工具里避免每次配置都重新生成导致不一致。6. 现在的固定玩法和还能继续扩展的事这套体系稳定运行之后我基本不需要再手动发布程序了。每次上线只需要做一件事在 GitLab 上创建 Release Tag剩下全自动。6.1 我当前流水线的最终形态以我们其中一个 Windows 服务为例完整流程是开发推送代码到 develop 分支GitLab 触发 WebhookArbess 开始跑构建测试如果测试通过流水线结束不部署版本要发布时维护者在 GitLab 上创建v1.2.3的 TagArbess 收到 Tag 事件跑完整流水线构建 - 测试 - 制品归档 - 部署机执行 deploy.ps1 - 重启服务 - 健康检查 - 钉钉通知如果任一步骤失败流水线停止钉钉群里收到失败详情和日志链接。这套流程最大的价值不是“快”而是稳定。人可能会忘步骤机器不会。6.2 给C项目加质量门槛的扩展思路自动化跑顺之后我下一个阶段做的是加质量门槛而不是继续加功能。第一个是静态代码分析。在构建阶段里加一步 Cppcheck扫描新提交的代码有没有明显的空指针、数组越界、资源泄漏。这一步不阻断发布只把报告存档并通知到群里。刚开始报告里会有一些历史遗留问题我会在 Cppcheck 配置里对老文件加抑制只关注增量问题。第二个是单元测试覆盖率。C 项目做覆盖率比 Java、Python 麻烦工具链不统一。我们用的是 OpenCppCoverage测试阶段跑完之后生成覆盖率报告低于某个阈值就不允许进入部署阶段。这个门槛刚开始设得很低比如 60%后面慢慢往上抬。第三个是制品自动清理。Arbess 制品目录只保留最近 20 次旧的自动删避免磁盘被占满。部署机上的备份目录保留最近 5 份再多也会清理。这个不写脚本的话半年之后真的会占几个 GB 的空间。6.3 几点个人体会如果你也准备给 C 项目上自动化构建我的建议是先别急着把测试、覆盖率、通知全堆上去先把最简单的“push 代码 - 自动编译 - 自动打包 - 自动部署 - 自动重启”这条链路跑通哪怕前期只有一个编译脚本也行。链路通了后面加什么都是增量改进链路不通配置再花哨也是白搭。另外自动化环境本身也要纳入维护。我每个月会在构建机上手动执行一次完整构建确认工具链没有因为系统更新或者软件自动升级出问题。这个动作不花时间但能预防那种“流水线前一天还好好的第二天莫名其妙失败”的情况。最后想提醒一点不要把部署脚本写成一次性脚本。它应该像代码一样有版本、有注释、有测试。我们后来把 deploy.ps1 也放进了 GitLab 仓库谁改了部署逻辑都有记录方便回溯。这套环境能长期稳定跑下来靠的不是某一个工具而是所有细节都有人盯着。
返回列表