C/C++在很多重要的行业都有应用,比如操作系统、嵌入式系统、金融系统、科研系统、汽车制造、机器人及游戏等等。在这些行业里,性能是非常关键的考量因素,而其他的语言又无法满足要求。作为一个如此重要的语言,C/C++ 的生态面临着一些严峻的挑战:
大型工程 - 当代码行数达到百万级别时,如果没有现代化的工具,将很难管理大型工程。
应用二进制接口不兼容 - 为了确保一个库与其他库、整个应用的兼容性,必须在通过各种配置来描述具体依赖信息,比如操作系统、架构和编译器等。
编译构建慢 - 由于头文件包含和预处理,以及上面提到的这些挑战,需要额外的机制来提升编译效率,保证只编译那些需要重新编译的代码。
代码链接和内嵌 - 一个静态的C/C++库能够被另一个库通过头文件包含的方式引用。一个共享库也能嵌入一个静态库。在两种情形中,当任何依赖变更时,都必须管理哪些库是需要重新构建的。
生态系统的快速发展 - 针对不同平台、不同构建任务及应用场景的编译器、构建系统层出不穷。
这篇文章会介绍如何通过Jenkins CI、Conan C/C++包管理器以及JFrog Artifactory通用制品仓库实现C/C++持续交付的最佳实践。
Conan 就是为了解决这些问题而诞生的。
Conan使用一个基于Python的清单(译者注:通常译为配方,表达更为准确),这个清单描述了如何通过显式调用任意构建系统来构建一个库,并描述库使用者所需的信息(包括目录,库名等)。为了管理不同的配置和ABI兼容性,Conan使用”Setting”(操作系统,架构,编译器…),当”Setting”发生变化的时候,Conan会为同一个库生成不同的二进制版本:
构建的二进制文件可以上传到JFrog Artifactory或Bintray,与您的团队或整个社区共享。团队中的开发人员不需要再次重建库,Conan将仅从配置的远程仓库(分布式模型)中获取与用户配置匹配的所需二进制包。这个过程中仍有一些挑战需要解决:
Conan生态系统正在快速增长,C/C++语言的DevOps现已成为现实:
这是带有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++项目非常重要,因为每个公司都有非常具体的项目结构,定制化的集成方式等。
正如我们在本博文开头所看到的那样,在构建C/C++项目时节省时间至关重要。以下是优化流程的几种方法:
让我们看一个使用Jenkins Pipeline特性的例子:
上图表示我们的项目P及其依赖项(A-G)。我们希望为两个不同的体系结构x86和x86_64分发项目。
如果我们将A的版本变为v1,不会有什么问题,我们可以更新B的依赖信息,并且也将B的版本变为v1,以此类推。整个完整的流程如下:
但是如果我们在开发环境对应的仓库中开发我们的库, 在每一次git push的时候,我们或许要依赖最新版本的A或者覆盖A(v0)包, 并且我们想自动化地重新构建受影响的包, 在这里指的就是 B, D, F, G 和 P。
首先我们需要知道哪些库需要重新被构建。命令”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)
最终将呈现如下效果:
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++