产品管道:带锁定文件的分布式完整管道

本节将展示一个多产品、多配置的分布式 CI 管道的完整实现。它将涵盖重要的实现细节

  • 使用锁定文件确保所有配置的依赖项集保持一致和固定。

  • 将构建的包上传到 `products` 仓库。

  • 捕获“包列表”并使用它们来执行最终的促销。

  • 如何以编程方式迭代“构建顺序”

像往常一样,我们从清理本地缓存并定义正确的仓库开始。

# First clean the local "build" folder
$ pwd  # should be <path>/examples2/ci/game
$ rm -rf build  # clean the temporary build folder
$ mkdir build && cd build # To put temporary files

$ conan remove "*" -c  # Make sure no packages from last run
# NOTE: The products repo is first, it will have higher priority.
$ conan remote enable products

与我们在 `packages pipeline` 中所做的类似,当我们希望确保在构建不同配置和产品时依赖项完全相同时,第一步是计算一个 `conan.lock` 锁定文件,我们可以将其传递给不同的 CI 构建代理,以强制在所有地方使用相同的依赖项集。这可以针对不同的 `products` 和配置进行增量计算,并将其聚合到最终的单个 `conan.lock` 锁定文件中。这种方法假定 `game/1.0` 和 `mapviewer/1.0` 都将使用相同版本和修订版的通用依赖项。

$ conan lock create --requires=game/1.0 --lockfile-out=conan.lock
$ conan lock create --requires=game/1.0 -s build_type=Debug --lockfile=conan.lock --lockfile-out=conan.lock
$ conan lock create --requires=mapviewer/1.0 --lockfile=conan.lock --lockfile-out=conan.lock
$ conan lock create --requires=mapviewer/1.0 -s build_type=Debug --lockfile=conan.lock --lockfile-out=conan.lock

注意

请记住,`conan.lock` 参数大部分是可选的,因为它是默认的锁定文件名。第一个命令可以输入为 `conan lock create --requires=game/1.0`。此外,所有命令,包括 `conan install`,如果找到现有的 `conan.lock` 文件,它们将自动使用它,而无需显式的 `--lockfile=conan.lock`。本教程中的命令显示为完整,是为了完整性和教学目的。

然后,我们可以计算每个产品和配置的构建顺序。这些命令与上一节中的命令相同,唯一区别是添加了 `--lockfile=conan.lock` 参数。

$ conan graph build-order --requires=game/1.0 --lockfile=conan.lock --build=missing --order-by=recipe --format=json > game_release.json
$ conan graph build-order --requires=game/1.0 --lockfile=conan.lock --build=missing -s build_type=Debug --order-by=recipe --format=json > game_debug.json
$ conan graph build-order --requires=mapviewer/1.0 --lockfile=conan.lock --build=missing --order-by=recipe --format=json > mapviewer_release.json
$ conan graph build-order --requires=mapviewer/1.0 --lockfile=conan.lock --build=missing -s build_type=Debug --order-by=recipe --format=json > mapviewer_debug.json

同样,`build-order-merge` 命令也将与上一个相同。在这种情况下,由于此命令实际上不计算依赖项图,因此不需要 `conan.lock` 参数,不解决依赖项。

$ conan graph build-order-merge --file=game_release.json --file=game_debug.json --file=mapviewer_release.json --file=mapviewer_debug.json --reduce --format=json > build_order.json

到目前为止,这个过程几乎与上一节相同,只是在捕获和使用锁定文件方面有所不同。现在,我们将解释 `products` 管道的“核心”:迭代构建顺序并分发构建,以及收集生成的已构建包。

下面是一个执行顺序迭代的 Python 代码示例(实际的 CI 系统会在不同的代理之间并行分发构建)。

build_order = open("build_order.json", "r").read()
build_order = json.loads(build_order)
to_build = build_order["order"]

pkg_lists = []  # to aggregate the uploaded package-lists
for level in to_build:
    for recipe in level:  # This could be executed in parallel
        ref = recipe["ref"]
        # For every ref, multiple binary packages are being built.
        # This can be done in parallel too. Often it is for different platforms
        # they will need to be distributed to different build agents
        for packages_level in recipe["packages"]:
            # This could be executed in parallel too
            for package in packages_level:
                build_args = package["build_args"]
                filenames = package["filenames"]
                build_type = "-s build_type=Debug" if any("debug" in f for f in filenames) else ""
                run(f"conan install {build_args} {build_type} --lockfile=conan.lock --format=json", file_stdout="graph.json")
                run("conan list --graph=graph.json --format=json", file_stdout="built.json")
                filename = f"uploaded{len(pkg_lists)}.json"
                run(f"conan upload -l=built.json -r=products -c --format=json", file_stdout=filename)
                pkg_lists.append(filename)

注意

  • 此代码特定于 `--order-by=recipe` 构建顺序,如果选择 `--order-by=configuration`,则 JSON 会不同,并且需要不同的迭代。

以上 Python 代码执行的任务是:

  • 对于构建顺序中的每个 `package`,都会发出一个 `conan install --require=<pkg> --build=<pkg>` 命令,并且此命令的结果将存储在 `graph.json` 文件中。

  • `conan list` 命令将此 `graph.json` 转换为一个名为 `built.json` 的包列表。请注意,此包列表实际上存储了已构建的包和必需的传递依赖项。这样做是为了简化,因为稍后将使用这些包列表来执行促销,我们还希望促销在 `packages pipeline` 中构建的依赖项,而不是此作业构建的依赖项。

  • `conan upload` 命令将包列表上传到 `products` 仓库。请注意,`upload` 首先检查仓库中已存在哪些包,避免在它们已存在时进行昂贵的传输。

  • `conan upload` 命令的结果捕获在一个名为 `uploaded<index>.json` 的新包列表中,我们稍后会将其累积起来,用于最终的促销。

实际上,这会转化为以下命令(您可以执行这些命令以继续教程)。

# engine/1.0 release
$ conan install --requires=engine/1.0 --build=engine/1.0 --lockfile=conan.lock --format=json > graph.json
$ conan list --graph=graph.json --format=json > built.json
$ conan upload -l=built.json -r=products -c --format=json > uploaded1.json

# engine/1.0 debug
$ conan install --requires=engine/1.0 --build=engine/1.0 --lockfile=conan.lock -s build_type=Debug --format=json > graph.json
$ conan list --graph=graph.json --format=json > built.json
$ conan upload -l=built.json -r=products -c --format=json > uploaded2.json

# game/1.0 release
$ conan install --requires=game/1.0 --build=game/1.0 --lockfile=conan.lock --format=json > graph.json
$ conan list --graph=graph.json --format=json > built.json
$ conan upload -l=built.json -r=products -c --format=json > uploaded3.json

# game/1.0 debug
$ conan install --requires=game/1.0 --build=game/1.0 --lockfile=conan.lock -s build_type=Debug --format=json > graph.json
$ conan list --graph=graph.json --format=json > built.json
$ conan upload -l=built.json -r=products -c --format=json > uploaded4.json

在此步骤之后,新构建的包将位于 `products` 仓库中,并且我们将有 4 个 `uploaded1.json` - `uploaded4.json` 文件。

简化不同的发布和调试配置,我们的仓库状态将如下所示。

digraph repositories { node [fillcolor="lightskyblue", style=filled, shape=box] rankdir="LR"; subgraph cluster_0 { label="Packages server"; style=filled; color=lightgrey; subgraph cluster_1 { label = "packages\n repository" shape = "box"; style=filled; color=lightblue; "packages" [style=invis]; "ai/1.1.0\n (Release)"; "ai/1.1.0\n (Debug)"; } subgraph cluster_2 { label = "products\n repository" shape = "box"; style=filled; color=lightblue; "products" [style=invis]; "ai/promoted" [label="ai/1.1.0\n(new version)"]; "engine/promoted" [label="engine/1.0\n(new binary)"]; "game/promoted" [label="game/1.0\n(new binary)", fillcolor="lightgreen"]; node [fillcolor="lightskyblue", style=filled, shape=box] "game/promoted" -> "engine/promoted" -> "ai/promoted"; } subgraph cluster_3 { rankdir="BT"; shape = "box"; label = "develop repository"; color=lightblue; rankdir="BT"; node [fillcolor="lightskyblue", style=filled, shape=box] "game/1.0" -> "engine/1.0" -> "ai/1.0" -> "mathlib/1.0"; "engine/1.0" -> "graphics/1.0" -> "mathlib/1.0"; "mapviewer/1.0" -> "graphics/1.0"; "game/1.0" [fillcolor="lightgreen"]; "mapviewer/1.0" [fillcolor="lightgreen"]; } { edge[style=invis]; "packages" -> "products" -> "game/1.0" ; rankdir="BT"; } } }

我们现在可以将不同的 `uploadedX.json` 文件累积到一个名为 `uploaded.json` 的包列表中,其中包含所有内容。

$ conan pkglist merge -l uploaded0.json -l uploaded1.json -l uploaded2.json -l uploaded3.json --format=json > uploaded.json

最后,如果一切顺利,并且我们认为这个新版本的软件和新的包二进制文件已准备好供开发人员和其他 CI 作业使用,那么我们可以从 `products` 仓库执行最终促销到 `develop` 仓库。

从 products->develop 促销
# Promotion using Conan download/upload commands
# (slow, can be improved with art:promote custom command)
$ conan download --list=uploaded.json -r=products --format=json > promote.json
$ conan upload --list=promote.json -r=develop -c

我们的最终 `develop` 仓库状态将是。

digraph repositories { node [fillcolor="lightskyblue", style=filled, shape=box] rankdir="LR"; subgraph cluster_0 { label="Packages server"; style=filled; color=lightgrey; subgraph cluster_1 { label = "packages\n repository" shape = "box"; style=filled; color=lightblue; "packages" [style=invis]; "ai/1.1.0\n (Release)"; "ai/1.1.0\n (Debug)"; } subgraph cluster_2 { label = "products\n repository" shape = "box"; style=filled; color=lightblue; "products" [style=invis]; } subgraph cluster_3 { rankdir="BT"; shape = "box"; label = "develop repository"; color=lightblue; rankdir="BT"; node [fillcolor="lightskyblue", style=filled, shape=box] "game/1.0" -> "engine/1.0" -> "ai/1.0" -> "mathlib/1.0"; "engine/1.0" -> "graphics/1.0" -> "mathlib/1.0"; "mapviewer/1.0" -> "graphics/1.0"; "game/1.0" [fillcolor="lightgreen"]; "mapviewer/1.0" [fillcolor="lightgreen"]; "ai/promoted" [label="ai/1.1.0\n(new version)"]; "engine/promoted" [label="engine/1.0\n(new binary)"]; "game/promoted" [label="game/1.0\n(new binary)", fillcolor="lightgreen"]; "game/promoted" -> "engine/promoted" -> "ai/promoted" -> "mathlib/1.0"; "engine/promoted" -> "graphics/1.0"; } { edge[style=invis]; "packages" -> "products" -> "game/1.0" ; rankdir="BT"; } } }

`develop` 仓库的这种状态将具有以下行为。

  • 安装 `game/1.0` 或 `engine/1.0` 的开发人员将默认解析到最新的 `ai/1.1.0` 并使用它。他们还会找到依赖项的预编译二进制文件,并且可以使用最新的一组依赖项继续开发。

  • 使用锁定文件锁定 `ai/1.0` 版本的开发人员和 CI 仍然可以继续使用该依赖项而不会中断,因为新版本和包二进制文件不会破坏或使先前存在的二进制文件无效。

此时,可能会出现关于如何处理 CI 中使用的锁定文件的疑问。请注意,`conan.lock` 现在包含 `ai/1.1.0` 版本。可能有不同的策略,例如将此锁定文件存储在“products”git 仓库中,使其在开发人员检出这些仓库时易于访问。但请注意,此锁定文件匹配 `develop` 仓库的最新状态,因此开发人员检出“products”git 仓库之一并针对 `develop` 服务器仓库执行 `conan install` 将自然解析到锁定文件中存储的相同依赖项。

至少将此锁定文件存储在任何发布包中是一个好主意,如果“products”以某种方式打包(安装程序、debian/rpm/choco/etc 包),以包含或附加到此打包版本中,以便软件的最终用户可以使用生产它的锁定文件,这样,无论开发人员存储库发生什么变化,这些锁定文件都可以从发布信息中恢复。

最后的说明

正如本 CI 教程的引言中所述,这并不是一个完美的解决方案,也不是一个您可以直接部署到贵组织中的 CI 系统。本教程到目前为止介绍了一个开发人员的持续集成过程的“最佳路径”,以及他们作为更大产品一部分的包中的更改如何作为这些产品的一部分进行测试和验证。

本 CI 教程的重点是介绍一些重要的概念、最佳实践和工具,例如:

  • 定义组织“产品”的重要性,即需要针对开发人员创建的新依赖项版本进行检查和构建的主要交付物。

  • 开发人员的新依赖项版本在被验证之前不应上传到主开发仓库,以免破坏其他开发人员和 CI 作业。

  • 如何使用多个仓库来构建 CI 管道,该管道可以隔离未经验证的更改和新版本。

  • 使用 `conan graph build-order` 如何在 CI 中高效地构建大型依赖项图,以及如何将不同配置和产品的构建顺序合并在一起。

  • 在存在并发 CI 构建时,为什么在 CI 中需要 `lockfiles`。

  • 版本控制的重要性,以及 `package_id` 在大型依赖项图中仅重新构建必要内容的作用。

  • 不使用 `user/channel` 作为 CI 管道中变化的包的可变和动态限定符,而是使用不同的服务器仓库。

  • 在验证了新的包版本后,在服务器仓库之间运行包促销(复制)。

本教程尚未涵盖许多实现细节、策略、用例和错误场景。

  • 如何集成需要新的破坏性主版本的包的破坏性更改。

  • 不同的版本控制策略,使用预发布版,使用版本或在某些情况下依赖配方修订版。

  • 锁定文件如何跨不同构建进行存储和使用,以及是否以及在哪里持久化它们。

  • 不同的分支和合并策略,夜间构建,发布流程。

我们计划扩展此 CI 教程,包括更多示例和用例。如果您有任何问题或反馈,请在 https://github.com/conan-io/conan/issues 上创建一个工单。