ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Conan入门实战:用C++包管理器终结第三方依赖难题

Conan入门实战:用C++包管理器终结第三方依赖难题 我去年接手一个跨平台C项目时最头疼的不是业务代码而是第三方依赖。Windows上装库用vcpkgLinux靠aptmacOS找Homebrew同一个库三个平台三个版本集成脚本写了几百行还是到处漏。后来全团队把依赖管理切到Conan上这个问题才真正收敛。Conan是目前C/C领域最主流、生态最完整的开源包管理器核心思路一句话让你像用pip、npm那样用一份声明文件搞定第三方库的下载、编译、版本锁定和传递依赖管理。这篇教程不打算照抄官方文档我按实际项目里从0到1用得上的路径来写把Conan的核心概念、日常操作、自定义打包、多平台配置和我踩过的坑一次讲透适合正在被C/C依赖问题折磨的开发者。1. Conan是什么C/C开发者为什么需要包管理器1.1 没有包管理器的日子有多痛C/C这门语言和Java、Python、Go不一样语言规范里压根没有“包”这个概念。库的形态五花八门可能是源码编译完就是几十个.o文件也可能是动态库得管.so、.dll的运行时路径还有可能是头文件加静态库的组合。更麻烦的是编译器版本、C标准、宏定义、ABI兼容性这些参数都会影响最终产物同一个库在gcc 12下编译和MSVC下编译二进制完全不能互相通用。这就导致没有一个统一的地方能像Maven中央仓库那样“下载即用”。传统做法是要么把第三方源码直接拷进自己的仓库要么写一堆shell脚本和CMake脚本去拉源码、编译、安装。这两种方式我都经历过问题非常现实。拷源码是最省事的但版本升级痛苦哪天库作者修了一个安全漏洞你得人肉去比对脚本方式看起来灵活但一旦依赖图复杂起来A要Boost 1.7xB又必须用1.7x之前的接口脚本里的判断逻辑就会膨胀到没人敢改。我见过最夸张的项目代码只有几万行ThirdParty目录里有近1个G的源码一次完整构建要40分钟。这种项目的维护成本已经高到影响发布节奏了。Conan这类包管理器出现本质就是把这层复杂度标准化每个依赖都对应一个recipe配方recipe里写清楚怎么获取源码、怎么编译、怎么安装、如何声明依赖关系然后按settings和options组合出唯一的二进制包。同一个库你换一个编译器或者改一个选项它自动生成不同的二进制ID各取所需互不干扰。1.2 Conan vs vcpkg vs apt vs FetchContent差别在哪很多刚接触Conan的人都会问不是已经有vcpkg和FetchContent了吗我的看法是它们解决问题的层次不一样但如果你需要跨平台一致、可发布可复用、可控的C/C依赖方案Conan是目前综合能力最强的选择。方案定位明显优势主要短板vcpkg源码构建为主、微软生态友好上手门槛低Windows体验好自定义配方能力弱多平台版本一致性难保证apt / Homebrew系统级二进制包安装快、系统集成好版本旧很难按项目锁版本ABI层面不可控CMake FetchContentCMake官方源码拉取与CMake天然结合写起来简单没二进制缓存每次都编译传递依赖一多就乱Conan通用包管理器可自定义recipe、有二进制缓存、跨平台一致概念多学习曲线比vcpkg稍陡举个例子你就懂了。两个项目都依赖OpenSSL项目A用旧版本项目B要新版本apt环境下你只能装一个这是系统级包管理器的天花板FetchContent能装两个但每次clean都重编而且OpenSSL的依赖链一旦出现两个版本CMake里处理起来非常酸爽Conan这时就很自然——两个版本在仓库里就是两个独立的包各自带依赖互不干扰你只需要在各自的conanfile里声明要哪个版本就行。还有一个很关键的差异是“上游协作”。用vcpkg如果你需要的库没人port就得自己给vcpkg提PR周期长用Conan你可以为自己的内部组件写私有recipe甚至可以只给自己团队用不需要推给任何人。这让Conan在商业项目和私有工具链的场景里特别吃香。2. 核心概念recipe、package、profile到底都是什么Conan最劝退新手的地方就是概念多recipe、package、remote、profile、settings、options……新手上来看文档容易直接懵。我建议你先别管所有细节抓住三件事就够了recipe怎么写、package怎么生成、profile怎么配。2.1 三个名词理清Conan的底层逻辑第一个词是recipe可以理解成“配方”。它是一份描述如何构建和安装某个库的文件在Conan里就是conanfile.py里面写清楚了源代码从哪来、构建系统是什么、需要什么依赖、有哪些可配置选项。一个recipe对应一个“逻辑包”比如spdlog这个库它的recipe描述的是spdlog所有版本、所有平台、所有选项的构建方式。第二个词是package指某个recipe在特定条件下编译出的二进制产物。比如spdlog/1.13.0在Linux gcc 12 Release 非shared模式下编出来的东西就是一个package。每个package有一个package ID由settings、options、依赖版本等一起计算出来。概念上很像Docker镜像recipe是Dockerfilepackage是build出来的镜像只要输入条件一致构建结果理论上一致。第三个词是remote就是远程仓库。Conan官方维护了conancenter里面收录了上千个主流库的recipe你也可以建自己的私有remote放内部库或二次开发的版本。安装包时Conan会先去本地缓存找找不到再去remote拉跟Docker Hub找镜像的工作方式非常相似。这些概念里最容易忽略、也最值得花时间设计的是package ID。它直接决定你的二进制缓存能用率。如果recipe里有个选项没纳入package ID计算A机器编出来的包B机器明明配置不一样却复用了同一个缓存跑起来就可能出问题。反过来把不该纳入的配置也算进去又会造成缓存命中率极低每台机器都重新编一遍。这个平衡在第四节我会细说。2.2 profile为什么是整个体系的灵魂profile是Conan里非常核心但经常被忽略的设计。它是一组描述“当前构建环境”的配置项包括操作系统、架构、编译器、编译版本、标准库实现、构建类型等等。你可以把profile理解成一套“环境指纹”。为什么Conan要专门做profile而不是直接读系统环境变量因为同一台机器上完全可以存在多套构建环境同一份代码我可以在本地用gcc 12编一个Debug版还要用clang 17编一个Release版甚至要交叉编译到ARM板子上。系统环境变量只能表达“当前默认环境”而profile可以同时存在多份构建时随时切换。举一个实际例子我本地电脑上常年放着这些profiledefault是日常开发的Linux gcc 12 Releasedebug是Linux gcc 12 Debug跑测试用win-msvc是Windows MSVC 17用来检查跨平台兼容性arm-cross是Linux armv8交叉编译用于嵌入式板卡。每个profile只是一个很小的文本文件。构建时指定不同的profileConan就能自动算出对应的依赖版本和二进制。理解了profile你才算真正理解Conan“一次声明到处构建”的实现逻辑。2.3 conanfile.txt还是conanfile.py按场景选就对了很多新人会在这两个文件之间纠结觉得是不是必须写.py才显得专业。实际不是。Conan 2.x里面这两个文件的定位非常清晰conanfile.txt是“消费端”配置适合你只想“用别人的库”的场景。文件内容很简单列出依赖和生成器不需要写任何Python逻辑。conanfile.py是“完整配方”既能描述包的构建发布生产者也能描述项目的依赖消费消费者。一旦你需要写自定义逻辑比如改选项、加编译参数、定制输出目录、把依赖关系传下去就必须用.py。我的建议很简单项目一开始只是引用几个第三方库就老老实实用conanfile.txt别一上来就整.py反而把事情搞复杂。等你的组件本身需要被其他项目复用、需要作为包发布时再升级成conanfile.py。这两种文件的语法底层是共通的用.txt跑通流程后改.py只是换个表达形式不会推翻重来。这里要特别提醒一点Conan 1.x和2.x的语法差异很大。1.x里常见的cmake生成器和conan_basic_setup()宏在Conan 2.x已经废弃换成了CMakeDeps和CMakeToolchain两个新生成器。搜教程时一定要确认内容基于哪个大版本。我现在的项目全部跑在2.x上下面的操作也以Conan 2.x为准。3. 5分钟实战用Conan给C项目装上第一个依赖这一节我直接带你跑通一个最小项目。目标很简单写一个用spdlog打日志的小程序依赖完全由Conan管理最后用CMake编译运行。3.1 安装Conan并生成默认profileConan本身是Python写的安装最直接的方式就是pippython -m pip install conan装完确认版本conan --version。能输出版本号就说明成功了。如果提示找不到conan命令多半是Python Scripts目录没进PATHWindows用户尤其常见把Python安装目录下的Scripts目录加进PATH就好或者直接用python -m conan调用。接着生成默认profileconan profile detect这条命令会自动探测当前机器的操作系统、架构、默认编译器和构建类型生成一个名为default的profile。你可以用conan profile show查看具体内容。生成的profile里有个关键字段compiler.libcxx。Linux上检测出来可能是libstdc11Windows上是msvc相关。这个值决定二进制兼容性后面排查问题经常用到。3.2 声明依赖conanfile.txt怎么写在项目根目录创建conanfile.txt内容如下[requires] spdlog/1.13.0 [generators] CMakeDeps CMakeToolchain [layout] cmake_layout三个小节各有作用。[requires]就是依赖声明写法是包名/版本号这里我指定了spdlog 1.13.0。Conan 2.x里已经基本不使用1.x时代的user/channel后缀直接写包名和版本就行。[generators]告诉Conan要生成哪些对接文件CMakeDeps负责生成各依赖的CMake配置文件让find_package能找到包CMakeToolchain生成一份toolchain文件CMake用它获取编译器路径和所有环境参数。[layout]用来规范输出目录cmake_layout会把所有生成物放在build文件夹里这个设计能大幅减少手工传参数的混乱。然后在项目里放一个非常普通的main.cpp#include spdlog/spdlog.h int main() { spdlog::info(hello from conan demo); return 0; }再写一个标准CMakeLists.txtcmake_minimum_required(VERSION 3.15) project(conan_demo LANGUAGES CXX) find_package(spdlog REQUIRED) add_executable(demo main.cpp) target_link_libraries(demo PRIVATE spdlog::spdlog)这里最关键的一行就是find_package(spdlog REQUIRED)。平时你要自己装好spdlogCMake才能找到它现在靠Conan生成的CMake配置文件就能直接找到版本、路径都由Conan统一管理不会再出现“我这编译能过你那边不行”的路径差异。3.3 生成构建文件并编译运行先跑这条核心命令conan install . --output-folderbuild --buildmissing命令分成三段理解conan install .是说“根据当前目录下的conanfile来生成依赖环境”--output-folderbuild是把生成的所有文件统一放到build目录避免污染项目根目录--buildmissing的作用是“如果conancenter没有现成预编译好的二进制那就现场用源码编译”。这个参数在本地第一次使用某个包时几乎必加因为很多包的二进制不是所有平台都预编译好了。如果你不加遇到缺包时会直接报错。执行成功后build目录下会出现一个conan_toolchain.cmake文件。接下来用CMake配置项目cd build cmake .. -DCMAKE_TOOLCHAIN_FILEconan_toolchain.cmake -DCMAKE_BUILD_TYPERelease cmake --build .注意这个-DCMAKE_TOOLCHAIN_FILEconan_toolchain.cmake。CMake在配置阶段需要知道编译器、编译选项、头文件和库的路径这些信息全部由Conan生成的toolchain文件提供。这个参数不能漏漏了大概率会find_package找不到包或者编译器配置不对。编译完成后运行./demo如果你看到spdlog输出的日志说明整条链路已经通了Conan拉取依赖、生成CMake配置、CMake编译、链接库文件一气呵成。很多新手卡在这一步往往是Conan和CMake版本不匹配或者toolchain路径写错第六节我会专门列排查清单。3.4 可复现性用lockfile锁住依赖环境这一小节很多人没意识到重要性但我强烈建议从第一天就养成习惯。上面我们用spdlog/1.13.0锁定了一个版本但spdlog自己还有传递依赖fmt。fmt如果版本范围很宽你上个月构建和这个月构建拿到的fmt可能不一样这就引入了“环境漂移”。今天编好的程序下个月队友拉代码重新构建可能跑出完全不同的链接结果。Conan 2.x提供了lockfile机制来解决这个问题。在install之前先创建lockfileconan lock create conanfile.txt执行后生成一个conan.lock文件里面锁定了完整依赖图中每个包的具体版本。之后构建时用conan install . --lockfileconan.lock就能完全复现之前的依赖环境。我一般会把lockfile提交到git整个团队构建环境完全一致CI和本地几乎不存在差异。lockfile的更新是显式行为不会有人悄无声息改掉你的依赖版本。4. 进阶手写conanfile.py把项目变成可复用包如果你的工作只停留在“用别人的库”上一节的内容已经够用。但Conan真正厉害的地方在于你可以把公司内部的组件、自己封装的算法库也用Conan管理起来让其他项目通过[requires]直接引用。这在C团队里是体验上的巨大提升。4.1 一个最小可用recipe长什么样下面是一个典型的、同时支持CMake构建和安装的conanfile.py。假设我们要给一个叫mydemo的静态库写recipefrom conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout class MyDemoConan(ConanFile): name mydemo version 1.0.0 license MIT settings os, arch, compiler, build_type options {shared: [True, False]} default_options {shared: False} exports_sources CMakeLists.txt, src/* def layout(self): cmake_layout(self) def generate(self): tc CMakeToolchain(self) tc.generate() def build(self): cmake CMake(self) cmake.configure() cmake.build() def package(self): cmake CMake(self) cmake.install() def package_info(self): self.cpp_info.libs [mydemo]几个核心点解释一下。settings声明了影响编译的四个维度缺一不可因为package ID的生成依赖这些字段。options和default_options声明包级选项这里定义了一个shared控制编动态库还是静态库。exports_sources告诉Conan构建时需要分发哪些源码文件否则它
返回列表