C/C++ 基于 Jenkins、Conan 和 Artifactory 的持续交付
C/C++在很多重要的行业都有应用,比如操作系统、嵌入式系统、金融系统、科研系统、汽车制造、机器人及游戏等等。在这些行业里,性能是非常关键的考量因素,而其他的语言又无法满足要求。作为一个如此重要的语言,C/C++ 的生态面临着一些严峻的挑战:
大型工程 - 当代码行数达到百万级别时,如果没有现代化的工具,将很难管理大型工程。
应用二进制接口不兼容 - 为了确保一个库与其他库、整个应用的兼容性,必须在通过各种配置来描述具体依赖信息,比如操作系统、架构和编译器等。
编译构建慢 - 由于头文件包含和预处理,以及上面提到的这些挑战,需要额外的机制来提升编译效率,保证只编译那些需要重新编译的代码。
代码链接和内嵌 - 一个静态的C/C++库能够被另一个库通过头文件包含的方式引用。一个共享库也能嵌入一个静态库。在两种情形中,当任何依赖变更时,都必须管理哪些库是需要重新构建的。
生态系统的快速发展 - 针对不同平台、不同构建任务及应用场景的编译器、构建系统层出不穷。
这篇文章会介绍如何通过Jenkins CI、Conan C/C++包管理器以及JFrog Artifactory通用制品仓库实现C/C++持续交付的最佳实践。
Conan - 强大的 C/C++ 包管理器
Conan 就是为了解决这些问题而诞生的。
Conan使用一个基于Python的清单(译者注:通常译为配方,表达更为准确),这个清单描述了如何通过显式调用任意构建系统来构建一个库,并描述库使用者所需的信息(包括目录,库名等)。为了管理不同的配置和ABI兼容性,Conan使用”Setting”(操作系统,架构,编译器…),当”Setting”发生变化的时候,Conan会为同一个库生成不同的二进制版本:
构建的二进制文件可以上传到JFrog Artifactory或Bintray,与您的团队或整个社区共享。团队中的开发人员不需要再次重建库,Conan将仅从配置的远程仓库(分布式模型)中获取与用户配置匹配的所需二进制包。这个过程中仍有一些挑战需要解决:
- 如何管理C/C++项目的开发和发布过程?
- 如何分发您的C/C++库?
- 如何测试您的C/C++项目?
- 如何为不同的配置生成多个包?
- 当其中一个库发生变化时, 如何管理库的重建?
Conan 生态系统
Conan生态系统正在快速增长,C/C++语言的DevOps现已成为现实:
- JFrog Artifactory管理完整的开发和发布周期。
- JFrog Bintray是通用分发中心。
- Jenkins自动化项目测试,生成Conan包的不同二进制配置,并自动化重建库。
Jenkins Artifactory 插件
- 提供Conan DSL,这是一种非常通用但功能强大的方法,可以从Jenkins Pipeline脚本中调用Conan。
- 使用Artifactory实例管理远程配置,隐藏身份验证详细信息。
- 收集任何Conan操作(安装/上传包)所涉及的工件信息,以生成buildInfo并将其上传到Artifactory。
这是带有Artifactory插件的Conan DSL的示例。首先, 我们配置Artifactory仓库,然后检索依赖项并最终构建它:
def artifactory_name = "artifactory"
def artifactory_repo = "conan-local"
def repo_url = 'https://github.com/memsharded/example-boost-poco.git'
def repo_branch = 'master'
node {
def server
def client
def serverName
stage("Get project"){
git branch: repo_branch, url: repo_url
}
stage("Configure Artifactory/Conan"){
server = Artifactory.server artifactory_name
client = Artifactory.newConanClient()
serverName = client.remote.add server: server, repo: artifactory_repo
}
stage("Get dependencies and publish build info"){
sh "mkdir -p build"
dir ('build') {
def b = client.run(command: "install ..")
server.publishBuildInfo b
}
}
stage("Build/Test project"){
dir ('build') {
sh "cmake ../ && cmake --build ."
}
}
}
您可以在上面的示例中看到Conan DSL非常高效。它对常见操作有很大帮助,但也允许强大的自定义集成。这对于C/C++项目非常重要,因为每个公司都有非常具体的项目结构,定制化的集成方式等。
复杂的Jenkins Pipeline操作:托管和并行化库的构建
正如我们在本博文开头所看到的那样,在构建C/C++项目时节省时间至关重要。以下是优化流程的几种方法:
- 只重新构建需要重建的库。这些是受其依赖的库的变更所影响到的库。
- 如果可能的话,并行构建。如果项目图中的两个或多个库之间没有关系,则可以并行构建它们。
- 并行构建不同的配置(os,编译器等)。如果需要,使用不同的从节点。
让我们看一个使用Jenkins Pipeline特性的例子:
上图表示我们的项目P及其依赖项(A-G)。我们希望为两个不同的体系结构x86和x86_64分发项目。
如果我们修改库A会发生什么
如果我们将A的版本变为v1,不会有什么问题,我们可以更新B的依赖信息,并且也将B的版本变为v1,以此类推。整个完整的流程如下:
- 推送A(v1)版本到Git, Jenkins 会构建x86和x86_64库。Jenkins 会上传所有包到Artifactory。
- 手动将B的版本升级到v1, 现在依赖A1, 推送到Git, Jenkins 会使用Artifactory返回的A(v1)来构建x86和x86_64架构下的B(v1)。
- 为C, D, F, G重复相同的流程,最终完成整个项目的构建。
但是如果我们在开发环境对应的仓库中开发我们的库, 在每一次git push的时候,我们或许要依赖最新版本的A或者覆盖A(v0)包, 并且我们想自动化地重新构建受影响的包, 在这里指的就是 B, D, F, G 和 P。
如何通过 Jenkins Pipelines 实现
首先我们需要知道哪些库需要重新被构建。命令”conan info –build_order” 能够识别项目修改了哪些库,并且告诉我们哪些可以并行构建。
因此, 我们创建两个Jenkins Pipeline任务:
“简单构建” 任务构建每一个需要单独构建的库。类似上面第一个使用 Conan DSL 和 Jenkins Artifactory 插件的例子。它是一个带参数的构建任务, 参数就是需要构建的包。
“多任务构建” 任务编排和启动”简单构建”任务, 如果可以并行构建则通过并行的方式执行。
我们还有一个存放配置文件(configuration yml)的仓库,Jenkins 任务会通过这个yml文件来知晓每个库的”配方”的所在,以及即将使用的不同profile,这里的profile指的是x86和x86_64。
leaves:
PROJECT:
profiles:
- ./profiles/osx_64
- ./profiles/osx_32
artifactory:
name: artifactory
repo: conan-local
repos:
LIB_A/1.0:
url: https://github.com/lasote/skynet_example.git
branch: master
dir: ./recipes/A
LIB_B/1.0:
url: https://github.com/lasote/skynet_example.git
branch: master
dir: ./recipes/b
…
PROJECT:
url: https://github.com/lasote/skynet_example.git
branch: master
dir: ./recipes/PROJECT
如果我们改变并推送库A到制品库, “多任务构建” 任务会被触发。首先会通过”conan info”命令检查哪些库需要重建。Conan 会返回如下列表: [B, [D, F], G]
这意味着我们需要先重新构建B, 然后我们才可以并行重新构建 D 和 F, 最后我们重新构建G。我们注意到C并不需要重新构建,因为A的变化并没有影响到它。
“多任务构建” Jenkins pipeline 脚本会创建一个包含并发调用”简单构建”任务的闭包, 最终以并发的形式启动这个组内的任务。
//for each group
tasks = [:]
// for each dep in group
tasks[label] = { -> build(job: "SimpleBuild",
parameters: [
string(name: "build_label", value: label),
string(name: "channel", value: a_build["channel"]),
string(name: "name_version", value: a_build["name_version"]),
string(name: "conf_repo_url", value: conf_repo_url),
string(name: "conf_repo_branch", value: conf_repo_branch),
string(name: "profile", value: a_build["profile"])
]
)
}
parallel(tasks)
最终将呈现如下效果:
- 两个”简单构建”任务将被触发, 都是为了构建库B, 一个是为x86架构,另一个是x86_64架构。
- 当 “A” 和 “B” 构成成功, “F” 和 “D” 的构建将被触发, 4 个”简单构建”任务将会以并发的方式执行(分别对应x86, x86_64架构)。
- 最终 “G” 会被构建出来。 因此 2 个”简单构建”会以并发形式执行。
Jenkins 任务执行各阶段视图如下所示:
MultiBuild
SimpleBuild
我们可以将”基本构建”任务配置为在不同的节点(Windows, OSX, Linux…)上运行,并且可以在Jenkins配置中控制任务执行线程数。
结论
很多企业C/C++的DevOps仍然处于摸索阶段。它需要投入大量的时间,但是从长远来看,在开发和发布生命周期中能够节省大量的时间。
从更高的维度看,它可以提升C/C++项目的交付质量和可靠性。在不久的将来,落地C/C++项目的DevOps将是刚需。
这篇博客中描述的Jenkins案例很好地阐述了如何仅通过Groovy代码和简便的yml文件来控制库的并发构建。其强大之处不在于这个案例和代码本身,而在于为个性化定制构建流程提供了可能性。感谢Jenkins Pipeline, Conan 和 JFrog Artifactory提供的强大功能。
原文链接: Continuous Integration for C/C++ Projects with Jenkins and Conan
推荐阅读: Announcing JFrog Artifactory Community Edition for C/C++
上一篇: LeetCode 24. Swap Nodes in Pairs
下一篇: cdh详细安装文档