背景
在当下软件应用的开发过程当中,单枪匹马或者小作坊式的模式已经很少见了,协作式的开发成为主流。相应的,应用的代码也不再是从零开始,而是基于或引用很多已有的、共享的模块,如各种开源的框架和共用库,或者协作团队中开发的自研库,这就是软件开发中常说的“依赖”。为了更好地管理这种依赖关系,各种开发语言都逐渐发展出了自己的依赖管理系统,如 Java 的 Maven、NodeJS 的 NPM、Python 的 Pypi 等。这些依赖管理系统的日渐完善和广泛应用,使得相应语言的应用开发更加简洁、高效,大大推动了软件应用的发展和普及。 然而相对的,作为软件开发重要组成部分的 C/C++ 语言,由于其编译型语言的特性,即应用最终要编译成为目标机器可直接执行的程序,使得 C/C++ 的依赖管理一直是一个众所周知的难题和痛点。这主要体现在: 应用二进制接口不兼容。C/C++ 的依赖不仅仅体现在代码上,还包括操作系统、架构、编译器等环境配置上,为了确保一个共享库与其他库、整个应用的兼容性,必须通过各种配置来描述这些配置的依赖信息。 编译构建慢。由于头文件和预处理机制,以及上面提到的兼容性,需要额外的机制来提升编译效率,才能保证只编译那些需要重新编译的代码。 代码链接和内嵌。一个静态的 C/C++ 库能够被另一个库通过头文件包含的方式引用,而一个共享库也能嵌入另一个静态库。在两种情形中,当任何依赖变更时,都必须管理哪些库是需要重新构建的。 生态系统的快速发展。针对不同平台、不同构建任务及应用场景的编译器、构建系统层出不穷,导致解决上述问题的工作量不断地增加。 当然,针对 C/C++ 的依赖管理,业界也开展了很多研究和实践工作,可惜大多数的效果并不理想。而本文将会介绍一种当前已逐步得到业界关注和认可,并得到大力推广的 C/C++ 依赖管理方案--Conan,以及如何基于 Conan、持续集成(CI)工具 Jenkins 和全语言制品库 Artifactory,实现 C/C++ 应用的持续交付流水线。 Conan--C/C++ 的包管理器 Conan 是一个开源项目(FOSS,Free Open Source Software),https://conan.io ,为 C/C++ 的依赖包管理构建了非中心化的管理架构,开发者可以像 Docker Registry 一样搭建自己专属的依赖包仓库。Conan 是跨平台的解决方案,同时兼容各种构建工具,以依赖包的二进制文件为基础来定义、管理依赖关系,使得依赖包的获取和消费更加符合目标环境和配置的特点,提升了整体编译的效率。Conan 是基于 Python 语言的,上手门槛较低,也易于扩展。在被 JFrog 收购之后,Conan 在保持开源特性的同时,还结合了 JFrog 产品在制品管理、DevOps 工具集成等方面的能力,为开发者提供了更为全面的支持。Conan 在当前 C/C++ 依赖管理领域已得到广泛关注和快速发展。 通常之前介绍的像 Maven 这样的依赖管理系统,都包含以下几个部分: 依赖包仓库,用以存储需要引用的依赖包,即各种通用框架或共享模块; 依赖访问协议,用以描述如何在依赖包仓库中定位、获取或上传共享模块; 依赖描述语言,用以描述如何定义依赖关系,以及后台对依赖关系的自动解析; 客户端,根据描述语言中的定义,遵照访问协议,从仓库中获取或上传相应的依赖包。 Conan 也在这几方面提供了成熟的解决方案: 依赖包仓库 Conan 在 JFrog 的公网制品分发平台 JFrog Bintray 上搭建了公共的依赖包仓库conan-center(https://conan.bintray.com),开发者可以直接在这里获取所需的各种公共依赖包。 同时,开发者还可以在本地搭建原生的 conan_server,或者直接利用 JFrog Artifactory 制品仓库,做为私有的依赖包仓库。 依赖访问协议 Conan 通过“<包名>/<版本号>@<所有者>/<成熟度>”的命名规则来定位一个依赖包,其中“<所有者>/<成熟度>”(user/channel)定义了一个类似于命名空间(NameSpace)的机制,用于区分针对同一个共享库的不同实现。 每一个依赖包都分为 recipe 和 package 两个部分。 recipe 定义了依赖包的基本信息、依赖关系、构建方法等基本信息,package 则根据目标环境和配置,如操作系统、架构、编译器等(即 Conan 中的 setting)的不同,保存对应的二进制实现。这样,客户端访问时,先根据命名规则定位到 recipe,再根据目标 setting 的不同选择对应的二进制 package 来下载、使用。 在 conan-center 中,各种公共库都根据目标 setting 的不同提供了大量的二进制 package 供开发者直接使用,大大提高了 C/C++ 应用的编译效率。 依赖描述语言 Conan提供了简单明了的依赖关系描述方式,在后续的示例中会做详细解读。 客户端 Conan 的客户端提供了丰富的命令行命令,能够方便地实现依赖关系的解析和依赖包的管理。 Conan 客户端的安装也非常简便,大家可以参考文档自己实践(https://docs.conan.io/en/latest/installation.html)。 安装好之后,我们可以运行第一个命令: 可以看到 Conan 客户端已经预先配置好了与公共库 conan-center 的连接,开发者可以直接使用其中的公共依赖包。 本文后续将通过示例来展示 Conan 如何利用这些解决方案来提供服务。 Conan 应用示例 本文将基于一个简单的 C++ 应用来展示如何使用 Conan,其代码可以在https://github.com/xingao0803/demo-poco-timer.git 中获得,供大家参考。 描述依赖关系 示例中的 C++ 应用,timer.cpp,是一个简单的 timer 程序,引用了公共库 POCO: 为了在 Conan 中描述与 POCO 的依赖关系,需要编写 conanfile.txt,这就是上一节提到的依赖描述语言。 其中,[requires] 部分列出了本应用需要的依赖包,这里是 POCO,1.8.0.1版本,而且是由 pocoproject 提供的稳定(stable)版本。 [generators] 则列出了编译本应用使用的编译器类型。Conan 提供的公开示例大多是基于 cmake 的,这里改用更为通用的 compiler_args,不限定编译器的类型。 当然,这里只是一个简单的例子,conanfile.txt 的更多内容请参考 Conan 官方文档https://docs.conan.io/en/latest/reference/conanfile_txt.html。 我们知道,在软件开发中,除了代码中的直接依赖,还会有各种传递依赖。而 conanfile.txt 里只列出了直接依赖。Conan 的客户端提供了“conan info”命令来解析所有的依赖关系。 在 conanfile.txt 所在目录执行: 本应用相关的各种依赖传递关系就会在 denpendencies.html 里展示出来,如下图: 此时再执行: 可以看到,相关依赖包的 recipe 已经下载到本地 cache 里了。 下载依赖包二进制文件 “conan info” 命令只是下载了依赖包的 recipe。要下载对应 setting 的 package 二进制包,用以编译,还需要执行 “conan install” 命令。 在 conanfile.txt 所在目录创建构建子目录,并执行 “conan install”: 从运行结果可以看出,Conan 的客户端根据本地的 setting 设置和依赖包的 recipe,自动从 conan-center 上获取对应的二进制 package,下载到本地 cache 里。 当然,由于 C/C++ 生态系统的快速发展,Conan 现有的二进制 package 不一定能够覆盖所有的 setting 组合。“—build=missing” 参数就是指定在没有 setting 对应的二进制 package 时,根据 recipe 中定义的方法,自动从依赖包的源代码编译出对应的二进制包,存储在本地 cache 中。例如:当我在 macbook 上执行上述操作时,POCO 就需要进行重新编译: 当我们再次运行 “conan install” 命令时可以发现,此时相关的依赖包已经可以直接从本地 cache 获取了,避免了重复的网络访问和编译工作。 上传到私有依赖包仓库 在当前团队协作开发的模式下,仅仅把依赖包下载到本地 cache 是远远不够的,我们还需要让整个团队都能够分享这些依赖包。这样即避免了重复的网络访问和编译,又保证了团队中依赖引用的一致性。此时就需要引入私有的依赖包仓库。 Conan 提供了原生的 conan_server 作为本地化部署的私有仓库,可以参考文档https://docs.conan.io/en/latest/uploading_packages/running_your_server.html。 而这里我们要推荐 JFrog 的 Artifactory 全语言制品仓库。Artifactory 不仅仅可以做为 Conan 仓库,其全语言的支持能力还使其能够同时提供 Maven、NPM、Docker 等依赖仓库的服务。 Artifactory 还提供元数据的能力,也就是可以在仓库存储制品的属性上记录整个 DevOps 过程中的关键数据。此外,Artifactory 还可以和 Jenkins 紧密集成,在 Jenkins Pipeline 中提供针对各种开发语言的 DSL,方便持续交付流水线的开发与编排。Artifactory 全语言、元数据,以及集成 Jenkins 的优势在后续示例中都能够得到体现。 Artifactory 的相关信息可以参见文档https://www.jfrog.com/confluence/display/RTF/Welcome+to+Artifactory ,“JFrog 杰蛙 DevOps” 微信公众号上也有很多相关文章和视频课程供大家参考。此外,JFrog Artifactory 还针对 Conan 的应用推出了社区版-- Artifactory CE,https://docs.conan.io/en/latest/uploading_packages/artifactory_ce.html,供大家使用。 为了使用 Artifactory 建设私有的 Conan 依赖包仓库,我们需要在 Artifactory 创建一个 Conan 类型的 local repository。创建方法参见https://www.jfrog.com/confluence/display/RTF/Conan+Repositories 。 然后,我们需要执行 Conan 的客户端命令 “conan remote add” 和 “conna user” 加入这个私有仓库。具体的执行方法在 Artifactory Repository 的 “Set Me Up” 部分有清晰的描述: 其中 <REMOTE> 作为这个私有仓库的别名,会在后续的命令中用来指定使用该仓库。 私用仓库创建好之后,我们可以使用 “conan upload” 命令来上传下载到本地 cache 的依赖包: 其中 -r <REMOTE> 用以指定目标私有仓库,--all 指定同时上传 recipe 和 package。 运行之后,在 Artifactory 的 Conan repository 里就可以看到上传的依赖包了。 之后当再次需要使用这些依赖包时,就可以在执行 “conan install” 时利用 “-r <REMOTE>” 参数指定从私有仓库获取了。 编译C++应用 执行 “conan install” 之后,除了会下载相应依赖包的二进制 package 之外,还会自动生成编译相关的参数引用文档。如在前面的示例中,build 目录下会自动生成 conanbuildinfo.args 文件,其中包含了编译过程中如何引用相应依赖包的参数设置。当编译时,可以直接引用这些参数: 可以看出,基于 Conan 的依赖管理,只要通过 conanfile.txt 描述依赖关系,通过 conan install 命令下载依赖包 package,我们就可以便捷地完成 C++ 应用的编译。 管理C++应用 当然,从应用开发过程来看,仅仅完成编译还是不够的,我们还需要管理好编译产出的可执行程序,供后续测试、部署、发布等环节使用。 通常会把应用编译好的可执行程序存储到代码管理系统,如 git 或 svn 中,或者文件服务器,如 ftp 上。但这种方式会丢失掉应用程序特定的 setting 信息,而且会对现有系统造成性能上的影响。 这里我们还是推荐使用 Artifactory,利用其 Generic repository 来存储可执行程序。Artifactory 全语言的支持使得能够在同一个仓库中统一管理 Conan 依赖包和编译好的可执行程序。而其企业级高可用的特性能够保证制品访问的性能和稳定。 在 Artifactory 里创建一个 Generic 的 local repository,在其 “Set Me Up” 部分可以看到如何利用 Artifactory 的 rest api 来上传和下载相应的可执行程序: 同时,我们可以在 repository 里通过设置不同的目录来记录可执行程序的版本。 针对可执行程序特定的 setting,也可以通过Artifactoy 的元数据能力记录到对应程序的属性上。 在执行完 “conan install” 之后,build 目录里还会自动生成 conaninfo.txt 文件,记录了本应用及其依赖相关的各种配置参数,其中 [settings]、[full_settings] 部分记录了相关的 setting 信息: 可以将这些 setting 参数提取出来,利用 Artifactory 的 rest api 写入属性: 在 Artifactory 的 repository 里就可以看到这些属性了: 此外,Artifactory 还提供了 AQL(Artifactory Query Language),使得我们能够基于这些元数据属性设置查询条件,从而定位与目标 setting 匹配的可执行程序。 设置 aql 文件 1timer.aql 如下: 再利用 Artifactory 的查询 rest api,我们就可以定位和下载符合 setting 需求的可执行程序,供后续的测试、部署等环节使用了。 同时,我们还可以利用 Artifactory 元数据的能力,将后续环节的关键数据,如测试结果,也记录在该程序的属性中,从而为质量监控审批、出错回溯检查等提供数据支持。 C/C++持续交付流水线 前面的示例,我们利用 Conan 依赖管理和 Artifactory 私有仓库实现了完整的 C++ 应用开发流程。在当前 DevOps 应用背景下,我们还需要能够自动化地重复执行这一流程,以实现 C++ 应用的持续交付。这就需要利用 Conan、Artifactory 和 Jenkins 这一类工具的集成来实现。 Artifactory 提供了 Jenkins 的插件,支持 Jenkins Pipeline 中针对 Artifactory 和 Conan 的 DSL,可以很方便、直观的实现 Artifactory 和 Conan 的各种操作。前面示例中的各种操作,都可以利用相应的 DSL 语句,集成到 Jenkins Pipeline 当中,如: 完整的 Jenkins Pipeline 代码在https://github.com/xingao0803/demo-poco-timer.git 中可以找到,供大家参考。 在 Jenkins 中执行该 Pipeline,就可以实现 timer 应用的自动化持续交付流水线: 总结 C/C++ 的依赖管理一直是软件开发领域的痛点。本文介绍了当前得到广泛关注和迅速发展的 C/C++ 依赖管理解决方案--Conan 的基本原理和应用流程。同时,利用 timer 应用示例,展示了如何基于 Conan、Artifactory 和 Jenkins,实现C/C++应用的持续交付流水线。 当然,本文中的示例只展示了 Conan 最基本的应用场景,大家可以根据相关资料,学习和研究 Conan 更多的案例。我们也会陆续推出后续文章,为大家进一步展示 Conan 的特性和应用方式。 文章作者:高欣,JFrog中国高级架构师 参考文献 Conan 官网https://conan.io JFrog 官网 http://www.jfrogchina.com Artifactory 文档 https://www.jfrog.com/confluence/display/RTF/Welcome+to+Artifactory Artifactory社区版 https://docs.conan.io/en/latest/uploading_packages/artifactory_ce.html 示例代码 https://github.com/xingao0803/demo-poco-timer.git