首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
C/C++持续集成实战:CONAN依赖管理与Azure DevOps流水线
📅 2026/10/16 1:29:57
✍️ 爱科研究院
👁 阅读 3,247
1. 为什么要在 C/C 项目里折腾持续集成如果你写过 C 或 C 的中大型项目大概率经历过这种场景本地编译一切正常提交代码之后同事拉下来一编译就报错折腾半天发现是某个头文件路径没同步或者某个第三方库的版本对不上。更头疼的是这类问题往往在集成阶段才暴露而那时候已经有好几个人在等你的代码合入。C/C 没有像 Java、Go 那样开箱即用的包管理和构建生态依赖管理、编译环境、链接顺序每一个环节都可能成为最后一根稻草。这就是持续集成CI在 C/C 项目里格外重要的原因。它本质上解决的是一个很朴素的问题让在我机器上能跑这句话变得可验证。每次提交代码自动触发一次干净的构建和测试把环境差异、依赖缺失、编译警告这些问题尽早暴露出来而不是等到发布前夜才发现。这篇要聊的是用CONAN做依赖管理、用Azure DevOps做流水线编排的一套组合方案。CONAN 是 C/C 生态里目前比较成熟的依赖与包管理工具能帮你把第三方库的版本、编译选项、平台差异都管起来Azure DevOps 则提供了从代码托管到流水线执行的一整套能力对 C/C 项目的支持也比较友好。两者结合能搭出一套相对完整的持续集成链路。这篇文章适合谁看如果你正在维护一个 C/C 项目团队规模不大但已经感受到依赖混乱和构建不一致的痛或者你已经在用 Azure DevOps 但还没把 CONAN 接进去那这篇内容应该能给你一些可直接参考的思路。我会尽量把每一步的为什么这么做讲清楚而不是只丢一堆 YAML 让你抄。需要提前说明的是这套方案不是唯一解也不是银弹。CONAN 本身有一定学习曲线Azure DevOps 的流水线配置也有不少细节坑。我会在后面的章节里把踩过的坑和绕过的弯都摊开讲让你少走点弯路。2. CONAN 到底解决了 C/C 依赖管理的哪些痛点2.1 从手动下载库到声明式依赖的转变在没有 CONAN 之前C/C 项目管依赖的方式基本就几种把第三方库源码直接塞进仓库、用系统包管理器装、或者手动下载编译好再配路径。这几种方式各有各的问题。源码塞仓库会让仓库体积爆炸而且升级库版本时 diff 惨不忍睹系统包管理器装出来的库版本往往和项目要求对不上不同机器上装的版本还可能不一致手动下载就更不用说了换台机器就得重新来一遍。CONAN 的思路是把依赖变成声明式的。你在conanfile.txt或conanfile.py里写清楚需要哪些库、什么版本、什么编译选项CONAN 负责去远端仓库拉取或者本地编译然后把头文件路径、库文件路径、编译定义都准备好供构建系统使用。这样一来依赖信息就变成了代码的一部分跟着仓库走谁拉下来都能复现。举个实际的例子。假设你的项目依赖某个 JSON 解析库和某个网络库用conanfile.txt大概长这样[requires] nlohmann_json/3.11.2 openssl/3.1.4 [generators] CMakeDeps CMakeToolchain [options] openssl/*:sharedFalse[requires]段声明了依赖和版本[generators]段告诉 CONAN 生成哪种构建系统需要的文件。CMakeDeps会生成 CMake 的find_package配置文件CMakeToolchain会生成工具链文件把编译器、编译选项、依赖路径都串起来。[options]段可以控制依赖库的编译方式比如这里让 OpenSSL 编译成静态库。这套机制的核心价值在于可复现性。只要conanfile不变任何人、任何机器上拉下来的依赖都是一样的。版本号写死了编译选项写死了平台差异由 CONAN 的 profile 机制处理。这就把环境问题从不可控变成了可控。2.2 CONAN 的 profile 机制让同一份代码适配不同平台C/C 项目经常要跨平台构建Windows 上用 MSVCLinux 上用 GCC 或 Clang可能还要交叉编译到 ARM。不同平台下依赖库的编译方式、ABI、运行时库都不一样。CONAN 用profile来抽象这些差异。一个 profile 文件描述了在什么环境下构建和构建出来的东西跑在什么环境下。比如一个典型的 Linux x86_64 的 profile[settings] osLinux archx86_64 compilergcc compiler.version11 compiler.libcxxlibstdc11 build_typeRelease [conf] tools.cmake.cmaketoolchain:generatorNinja[settings]段定义了操作系统、架构、编译器、C 标准库、构建类型。CONAN 会根据这些设置去匹配或编译对应的二进制包。[conf]段可以传一些工具链相关的配置比如这里指定用 Ninja 作为 CMake 的生成器。实际使用中你通常会有一个defaultprofile 用于本机构建再准备几个针对 CI 环境的 profile。比如 CI 上可能同时跑 Linux 和 Windows 两个 job那就分别用linux-release和windows-release两个 profile。CONAN 在安装依赖时会读取 profile决定是下载预编译好的包还是从源码编译。这里有个经验点profile 里的compiler.version一定要和 CI 环境里实际用的编译器版本严格对应。我见过有人本地用 GCC 11CI 镜像里默认是 GCC 9profile 里写的是 11结果 CONAN 去拉 GCC 11 编译的二进制包链接的时候各种符号找不到。这种问题排查起来很费时间因为报错信息往往指向链接阶段而不是编译器版本。2.3 本地缓存与远端仓库CONAN 的包是怎么流转的CONAN 的工作模式是本地缓存 远端仓库。当你执行conan install时CONAN 先检查本地缓存里有没有符合要求的包。有就直接用没有就去远端仓库拉。如果远端也没有且允许从源码构建CONAN 会下载源码在本地编译。本地缓存默认在用户目录下的.conan2文件夹里CONAN 2.x 版本。缓存里按包名、版本、用户、频道、settings 等维度组织同一个库的不同编译配置会存成不同的包。这个设计的好处是同一台机器上多个项目可以共享缓存不用每个项目都重新编译一遍依赖。远端仓库可以是你自己搭的私有仓库也可以用公共的。团队协作时通常会搭一个私有远端把团队内部编译好的包推上去CI 环境直接从私有远端拉速度比从源码编译快很多。公共仓库适合拉一些通用的开源库但要注意版本和编译选项是否匹配你的需求。在 CI 场景下本地缓存的管理是个关键点。Azure DevOps 的托管代理每次都是干净环境缓存不会保留。如果不做缓存每次流水线都要重新下载或编译所有依赖时间会很长。解决办法是用 Azure DevOps 的Pipeline Caching功能把 CONAN 的缓存目录缓存起来key 用conanfile的哈希值这样依赖没变时直接命中缓存变了才重新拉。3. Azure DevOps 流水线的骨架怎么搭3.1 从零开始一个最小可用的 pipeline 结构Azure DevOps 的流水线用 YAML 定义放在仓库根目录的azure-pipelines.yml里。一个最小可用的 C/C 项目流水线结构大概是这样trigger: branches: include: - main - develop pool: vmImage: ubuntu-22.04 variables: CONAN_USER_HOME: $(Pipeline.Workspace)/.conan2 BUILD_TYPE: Release stages: - stage: Build jobs: - job: LinuxBuild steps: - checkout: self - script: | pip install conan conan --version displayName: 安装 CONAN - script: | conan install . \ --output-folderbuild \ --buildmissing \ -s build_type$(BUILD_TYPE) displayName: 安装依赖 - script: | cmake -S . -B build \ -DCMAKE_TOOLCHAIN_FILEbuild/conan_toolchain.cmake \ -DCMAKE_BUILD_TYPE$(BUILD_TYPE) cmake --build build --parallel displayName: 配置并编译trigger段定义了哪些分支的提交会触发流水线。pool段指定用哪个代理镜像这里用的是 Ubuntu 22.04。variables段定义了两个变量CONAN_USER_HOME指向流水线工作目录下的.conan2这样缓存的时候好定位BUILD_TYPE设为 Release。stages下面是具体的执行步骤。第一步装 CONAN第二步用conan install安装依赖--buildmissing表示如果远端没有匹配的二进制包就从源码编译--output-folderbuild把生成的文件放到build目录。第三步用 CMake 配置和编译关键是要把CMAKE_TOOLCHAIN_FILE指向 CONAN 生成的工具链文件这样 CMake 才能找到依赖。这个骨架跑通之后你就有了一个最基本的 CI 流程提交代码自动装依赖、编译。但离好用还有距离后面几节会逐步加上缓存、多平台、测试等环节。3.2 缓存策略让流水线从十分钟降到两分钟前面提到Azure DevOps 的托管代理每次都是干净环境CONAN 的本地缓存不会保留。如果不做缓存每次流水线都要重新下载或编译依赖。对于一个依赖十几个库的项目这一步可能就要花五到十分钟。Azure DevOps 提供了Cache2任务来做缓存。用法是在步骤里加一段- task: Cache2 inputs: key: conan | $(Agent.OS) | conanfile.txt restoreKeys: | conan | $(Agent.OS) path: $(CONAN_USER_HOME) displayName: 缓存 CONAN 依赖key是缓存的唯一标识这里用了conanfile.txt的内容哈希。只要conanfile.txt没变key 就不变缓存就能命中。restoreKeys是回退键当精确 key 没命中时会尝试用前缀匹配的缓存这样即使conanfile.txt变了也能复用一部分旧缓存减少重新下载的量。path指向 CONAN 的缓存目录。这里有个细节要注意缓存 key 里最好带上Agent.OS。因为不同操作系统的依赖包不通用Linux 上缓存的包在 Windows 上没法用。带上 OS 标识不同平台的缓存就分开了。另一个经验点是conan install的时候加--buildmissing会让 CONAN 在找不到二进制包时从源码编译。如果缓存命中了这一步会很快如果没命中编译依赖的时间会算在流水线里。对于依赖较多的项目可以考虑在团队内部搭一个 CONAN 私有远端把常用依赖的二进制包推上去CI 直接从远端拉比从源码编译快得多。3.3 多平台构建一份代码多个 job 并行跑C/C 项目经常需要跨平台。Azure DevOps 的strategy和matrix可以很方便地定义多平台构建。比如同时跑 Linux 和 Windowsjobs: - job: Build strategy: matrix: Linux: imageName: ubuntu-22.04 conanProfile: linux-release Windows: imageName: windows-2022 conanProfile: windows-release pool: vmImage: $(imageName) steps: - checkout: self - script: | pip install conan conan profile detect --force conan install . --output-folderbuild --buildmissing -pr $(conanProfile) displayName: 安装依赖 - script: | cmake -S . -B build -DCMAKE_TOOLCHAIN_FILEbuild/conan_toolchain.cmake cmake --build build --config Release --parallel displayName: 编译matrix定义了两个变体每个变体有自己的imageName和conanProfile。Azure DevOps 会为每个变体创建一个独立的 job 并行执行。conan profile detect --force会自动检测当前环境的编译器信息并生成默认 profile然后-pr $(conanProfile)指定用哪个 profile。这里有个坑Windows 和 Linux 的 CMake 命令在细节上有差异。Windows 上 MSVC 是多配置生成器cmake --build需要加--config ReleaseLinux 上单配置生成器不需要。如果命令写死了在另一个平台上就会出问题。解决办法是用条件判断或者干脆把构建脚本抽成一个跨平台的脚本文件在脚本里处理平台差异。另外conan profile detect生成的默认 profile 可能不完全符合你的需求比如默认的compiler.libcxx在 Linux 上可能是libstdc而不是libstdc11。建议在仓库里维护自己的 profile 文件CI 里直接用仓库里的而不是依赖自动检测。4. 把 CONAN 和 Azure DevOps 接起来的关键细节4.1 conanfile 的写法txt 还是 pyCONAN 支持两种conanfile格式conanfile.txt和conanfile.py。前者简单适合依赖列表固定的场景后者灵活可以写自定义逻辑比如根据平台条件选择依赖、在构建前后执行脚本等。对于大多数项目conanfile.txt够用了。但如果你的项目有以下需求就得用conanfile.py需要根据操作系统或编译器选择不同的依赖版本需要在安装依赖时执行一些自定义命令需要把项目本身也打包成 CONAN 包供其他项目使用一个conanfile.py的基本结构from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMakeDeps class MyProjectConan(ConanFile): name myproject version 1.0.0 settings os, compiler, build_type, arch def requirements(self): self.requires(nlohmann_json/3.11.2) if self.settings.os Linux: self.requires(openssl/3.1.4) def generate(self): deps CMakeDeps(self) deps.generate() tc CMakeToolchain(self) tc.generate()requirements方法里可以根据self.settings.os做条件判断实现平台相关的依赖。generate方法里生成 CMake 需要的文件。这种写法比conanfile.txt灵活但也要注意别把逻辑写得太复杂否则维护成本会上升。我的建议是如果conanfile.txt能搞定就别上conanfile.py。简单的东西用简单的工具出问题的概率小。等真的遇到需要条件判断的场景再迁移也不迟。4.2 工具链文件的生成与 CMake 的配合CONAN 2.x 里CMakeToolchain生成器会生成一个conan_toolchain.cmake文件里面包含了编译器设置、依赖路径、编译定义等信息。CMake 配置时通过-DCMAKE_TOOLCHAIN_FILEbuild/conan_toolchain.cmake引入这个文件就能自动找到 CONAN 安装的依赖。这里的关键是CMakeDeps和CMakeToolchain要配合使用。CMakeDeps为每个依赖生成xxx-config.cmake文件让find_package能找到CMakeToolchain生成工具链文件设置全局的编译选项和路径。两个都生成CMake 才能完整地识别依赖。在CMakeLists.txt里你正常写find_package就行cmake_minimum_required(VERSION 3.20) project(MyProject CXX) find_package(nlohmann_json REQUIRED) find_package(OpenSSL REQUIRED) add_executable(myapp main.cpp) target_link_libraries(myapp PRIVATE nlohmann_json::nlohmann_json OpenSSL::SSL)CONAN 生成的配置文件会让find_package找到正确的路径target_link_libraries里的目标名也是 CONAN 约定好的。这样CMakeLists.txt本身不需要知道 CONAN 的存在保持了构建脚本的干净。有个容易踩的坑conan install的--output-folder和 CMake 的-B目录要一致。如果conan install把文件生成到buildCMake 也配置到build那工具链文件就在build/conan_toolchain.cmake。如果两边目录不一致CMake 就找不到工具链文件报错信息通常是找不到某个依赖但根因其实是路径没对上。4.3 版本锁定与依赖升级的节奏控制CONAN 的版本声明支持几种写法精确版本nlohmann_json/3.11.2、版本范围nlohmann_json/[3.10 4]、以及带用户和频道的nlohmann_json/3.11.2user/channel。在 CI 场景下建议用精确版本。用版本范围的好处是能自动拿到小版本更新但坏处是构建结果不可复现。今天拉到的可能是 3.11.2明天可能就变成 3.11.3如果新版本有行为变化CI 可能突然就挂了而你什么都没改。这种莫名其妙挂了的问题排查起来很痛苦。精确版本的好处是可复现坏处是升级要手动改。我的做法是在conanfile里写精确版本然后定期比如每两周手动检查一次依赖更新在单独的分支上升级、跑 CI、验证没问题再合入。这样既保证了可复现性又不会让依赖版本落后太多。对于团队协作还可以用 CONAN 的lockfile机制。conan lock create会生成一个锁文件记录当前所有依赖的精确版本和配置。CI 里用conan install --lockfile安装确保每次构建用的依赖完全一致。这在多平台构建时特别有用因为不同平台可能解析出不同的依赖版本lockfile 能强制统一。5. 实际跑起来之后才会遇到的坑5.1 编译器版本不匹配导致的链接错误前面提过 profile 里编译器版本要写对这里展开说一下为什么。CONAN 在匹配二进制包时会把 profile 里的compiler.version作为匹配条件之一。如果 profile 写的是 GCC 11但 CI 环境实际是 GCC 9CONAN 会去拉 GCC 11 编译的包。这些包的 ABI 可能和 GCC 9 不兼容链接时就会出现undefined reference或者更隐蔽的运行时崩溃。排查这类问题的思路是先确认 CI 环境里实际的编译器版本再检查 profile 里的版本是否一致。在流水线里加一步gcc --version或clang --version把版本打出来和 profile 对比。如果用的是conan profile detect自动生成的 profile通常没问题如果是手写的 profile就要格外注意。另一个相关的问题是C 标准库的 ABI。GCC 5.1 之后引入了新的libstdc11ABI和旧的libstdc不兼容。如果 profile 里写的是libstdc但依赖库是用libstdc11编译的链接时就会出问题。CONAN 的默认 profile 检测通常会给出正确的值但手写 profile 时容易忽略这一项。5.2 缓存命中率低key 设计不当的后果缓存 key 的设计直接影响命中率。如果 key 太细比如把conanfile.txt的内容、profile 的内容、环境变量全都拼进去那只要有任何一点变化缓存就失效等于没缓存。如果 key 太粗比如只用conan作为 key那不同分支、不同平台的缓存会互相覆盖可能拉到错误的依赖。我的经验是key 用conanfile的内容哈希 操作系统标识restoreKeys用操作系统标识做前缀。这样conanfile没变时精确命中变了时还能复用同平台的旧缓存。profile 的内容不建议放进 key因为 profile 变化频率低而且 profile 变了通常意味着构建配置变了重新拉依赖也是合理的。还有一个细节缓存路径要指向 CONAN 的缓存目录而不是项目目录。CONAN 2.x 的缓存默认在CONAN_USER_HOME下的.conan2里。把CONAN_USER_HOME设成流水线工作目录下的一个路径缓存这个路径就能把整个 CONAN 缓存都存下来。如果只缓存项目里的build目录那只是缓存了生成的文件依赖包本身没缓存下次还是要重新下载。5.3 流水线超时与并行编译的取舍Azure DevOps 的托管 job 默认有超时限制免费额度下单个 job 最长 60 分钟。对于依赖多、编译慢的 C/C 项目这个时间可能不够。解决办法有几个一是用自托管代理没有超时限制二是优化编译速度比如用ccache、并行编译、增量构建三是把构建拆成多个 job每个 job 负责一部分。并行编译是最直接的优化手段。CMake 的--parallel参数会让编译并行执行充分利用多核 CPU。Azure DevOps 的托管代理通常有 2 到 4 个核--parallel不加参数会用所有核。但要注意并行编译会显著增加内存占用如果代理内存不足可能会触发 OOM。对于大型项目可以限制并行度比如--parallel 4。ccache是另一个利器。它缓存编译结果相同的源文件第二次编译时直接命中缓存速度极快。在 CI 里用ccache需要把缓存目录也缓存起来key 可以用源文件的哈希。配置起来比 CONAN 缓存稍复杂但对于编译时间长的项目收益很明显。6. 这套方案跑顺之后还能怎么扩展6.1 把单元测试接进流水线编译通过只是第一步代码能不能正确运行还得靠测试。把单元测试接进流水线每次提交都跑一遍能尽早发现逻辑错误。C/C 项目常用的测试框架有 GoogleTest、Catch2、doctest 等这些库在 CONAN 里都有现成的包。接测试的步骤大概是这样在conanfile里加上测试框架的依赖在CMakeLists.txt里定义测试目标然后在流水线里加一步运行测试。Azure DevOps 支持把测试结果以 JUnit XML 格式发布这样在流水线页面上就能看到哪些测试通过了、哪些失败了。- script: | cd build ctest --output-on-failure --output-junit test-results.xml displayName: 运行测试 - task: PublishTestResults2 inputs: testResultsFormat: JUnit testResultsFiles: **/test-results.xml condition: always()condition: always()保证即使前面的步骤失败了测试结果也会被发布方便排查问题。--output-on-failure让 ctest 在测试失败时输出详细信息--output-junit生成 JUnit 格式的结果文件。6.2 静态分析与代码质量门禁C/C 项目里静态分析能在编译之前发现潜在问题比如空指针解引用、内存泄漏、未初始化变量等。常用的工具有 Clang-Tidy、Cppcheck、PVS-Studio 等。把这些工具接进流水线可以设置质量门禁比如有严重级别的问题就阻断合并。Clang-Tidy 和 CMake 的集成比较方便可以在 CMake 配置时加-DCMAKE_CXX_CLANG_TIDYclang-tidy编译时自动跑分析。Cppcheck 可以单独跑输出 XML 格式的结果再用 Azure DevOps 的任务发布。质量门禁的设置要循序渐进。一开始可以把严重问题设为阻断警告级别的问题只报告不阻断。等团队适应了再逐步收紧。如果一上来就卡得很严可能会拖慢开发节奏引起抵触。6.3 制品归档与发布流程的衔接CI 跑通之后下一步自然是 CD持续交付。编译出来的二进制文件、库文件、安装包需要归档起来供后续发布使用。Azure DevOps 的PublishBuildArtifacts任务可以把构建产物上传供后续阶段或人工下载。对于 C/C 项目制品通常包括可执行文件、静态库或动态库、头文件、以及可能的调试符号文件。归档时要注意区分不同平台和构建类型比如linux-release、windows-debug避免混淆。可以用变量拼接路径让制品名带上平台和构建类型标识。如果项目本身也要作为 CONAN 包发布那还需要在流水线里加conan create和conan upload步骤把编译好的包推到私有远端。这样其他项目就能直接依赖你的包形成内部的依赖生态。这一步涉及包的版本管理、频道划分等策略可以单独作为一篇文章来聊。整套方案从最基础的编译流水线到缓存优化、多平台构建、测试接入、静态分析、制品归档是一个逐步演进的过程。不建议一上来就把所有环节都堆上去那样配置复杂、排查困难。先把编译跑通再加缓存再加测试一步一步来每一步都验证稳定了再往下走。我在实际项目里就是这么推进的每次只加一个环节出问题的时候范围小好定位。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/16 1:29:57
Android ImageView蒙版与高斯模糊协同实现原理与性能优化
2026/10/16 1:24:56
Qt网络编程实战:企业微信群机器人Webhook消息推送与异步请求实现
2026/10/16 1:24:56
Anaconda与Spyder配置指南:半小时搭建pandas数据分析环境
2026/10/16 3:20:05
BERT中文文本分类实战:基于pytorch-transformers的完整工程与避坑指南
2026/10/16 3:20:05
Winpcap网络嗅探器实战:从抓包原理到协议解析与避坑指南
2026/10/16 3:20:05
Linux输出重定向完全指南:从文件描述符到Shell脚本实战
2026/10/16 3:20:05
PaddlePaddle手势识别实战:从训练到部署的完整指南
2026/10/16 3:20:05
BERT中文文本分类实战:从代码跑通到避坑进阶
2026/10/16 3:15:05
国产加速卡-应知应会
2026/10/16 0:04:49
HCIA-AI V3.5 模拟题库从零搭建:知识点标签、组卷算法与错题分析
2026/10/16 0:04:49
游戏项目Shader清理实战:静态扫描与动态验证双轨并行
2026/10/16 0:04:49
比特币白皮书深度拆解:从双重支付到共识机制的核心设计
2026/10/16 1:17:36
流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南
2026/10/16 1:17:35
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别
2026/10/16 1:17:35
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容
2026/10/16 1:17:35
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/15 5:07:59
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/15 8:10:01
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)