从无到有,构建可启动镜像:tine 构建系统

Lobsters Hottest 工具

摘要

本文介绍了 tine——一个基于 Buck2 构建系统,专为构建可启动操作系统镜像而设计,支持完全独立、可重现的构建,并兼容大型代码仓库(monorepo)的迭代工作流。

<p><a href="https://lobste.rs/s/c3t81d/from_thin_air_bootable_images_tine_build">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/09/24 15:02

# 从无到有:tine 构建系统与可启动镜像生成 来源:https://amutable.com/blog/tine-build-system 2026年9月24日 · 13分钟阅读 作者:Daan De Meyer 和 Martin Pitt 本文是我们近期开源工作系列文章的一部分。今天,我们正式介绍并发布*tine*(https://github.com/amutable-systems/tine),这是我们基于 Buck2 开发的全新构建系统。 ## 我们的需求 构建一个具有密码学可验证完整性的操作系统,必须从一个具备这些核心属性的构建系统开始。同时,作为开源社区的一员,我们希望既能贡献也能尽可能重用现有成果,并追求高效、快速迭代的开发流程。 这大致转化为以下对构建系统的要求: - **最小化宿主依赖。** 系统必须自包含,尽量减少外部依赖,以便能在任何环境中运行。 - **完全控制输入。** 必须能够精确固定产品中使用的每个软件。应支持选择上游发行版(Fedora、CentOS、Arch、Debian等),并尽可能重用现有软件包,同时仍能快速响应CVE,并在必要时(临时或永久地)与上游打包决策产生分歧。 - **集成化包管理机制。** 必须提供用于导入、更新和合并导入软件包的工具。 - **低成本的全局重建。** 必须能够按需重建整个世界,例如在GCC版本升级后。 - **密封、可重现的构建。** 所有组件和镜像构建必须在密封环境中进行,并产生比特级可重现的输出。 - **原生镜像构建。** 必须能够原生并并发地构建可启动的操作系统镜像和systemd sysext(https://www.freedesktop.org/software/systemd/man/latest/systemd-sysext.html)镜像。 - **基于单体仓库的迭代。** 必须支持在单一顶层单体仓库中维护操作系统,以实现快速的端到端迭代。对导入的RPM包或Go/Rust组件的更改应能立即跨所有镜像集进行构建和测试,无需中间提交或推送,也无需复杂的版本或依赖声明。 - **一流的一级自定义组件。** 必须能原生、高效地从固定的外部仓库构建Go和Rust项目,例如kubernetes(https://github.com/kubernetes/kubernetes)或varlink-http-bridge(https://github.com/systemd/varlink-http-bridge)。 - **扫描器兼容性。** 构建的镜像必须能与标准SBOM(https://en.wikipedia.org/wiki/Software_bill_of_materials)工具以及安全扫描器(如syft(https://github.com/anchore/syft)/grype(https://github.com/anchore/grype)或trivy(https://github.com/aquasecurity/trivy))协同工作。 - **缓存机制。** 构建必须能够从本地和/或全局缓存中检索未更改的组件。从头构建所有内容可能需要数小时,而开发者通常只处理单个组件。 ## 现有工具 在构建我们自己的构建工具之前,我们评估了几个选项。 ### mkosi 鉴于我们的团队包括了`mkosi`(https://github.com/systemd/mkosi)的创建者和维护者,它自然是第一个评估对象。但我们很快得出结论,它存在一些根本性的缺陷。它在基于上游软件包构建单个镜像方面非常出色,但在构建多个弱相关镜像或需要对构成镜像的制品进行更多控制时,就显得力不从心。构建多个镜像仅限于那些作为“主”镜像一部分交付的镜像。 我们需要以统一、健壮的方式构建多种不同类型的制品,而不仅仅是镜像。因此,构建过程需要由一个通用且灵活的工具来编排。主要的构建文件应该是一种能够调用*库函数*(如“编译一个cargo crate”或“构建一个UKI(https://uapi-group.org/specifications/specs/unified_kernel_image/)”)的语言。mkosi恰恰相反,它是一个*框架*:它知道如何构建镜像,但对于其他类型的构建,只提供自由格式的不透明钩子。当您想构建镜像以外的更多内容时,这会导致糟糕的体验。 ### Open Build Service Open Build Service(https://openbuildservice.org/)(OBS)是一个功能强大的完全集成构建系统,主要被SUSE和openSUSE项目用来生产其所有制品,从软件包到ISO以及许多其他镜像格式。它具有强大的依赖关系跟踪能力,并支持种类繁多的发行版。 然而,它也是“最小化宿主依赖”的反面。其服务器端是必需的、中心化的,且自我托管并非易事。它也不是一个通用构建系统,这意味着任何新的制品类型要么需要建模为软件包,要么需要对OBS进行大量修改。我们得出的结论是,这种灵活性的缺乏加上其整体架构,使得OBS难以满足我们的要求。 ### BuildStream Apache BuildStream(https://buildstream.apache.org/)将操作系统镜像描述为YAML“元素”的图,每个元素都有自己的源代码、依赖关系和构建命令。BuildStream在bubblewrap(https://github.com/containers/bubblewrap)沙箱中构建每个元素,并将结果缓存在其所有输入的哈希值下,这与Buck2类似。它很成熟,用于构建freedesktop-sdk(https://gitlab.com/freedesktop-sdk/freedesktop-sdk)、GNOME OS(https://os.gnome.org/)和WebKitGTK(https://webkitgtk.org/)。 我们对BuildStream的担忧主要围绕引导启动和可扩展性。BuildStream是一个带有编译扩展和其他依赖的Python应用程序。它依赖于一组单独的辅助程序,以及来自宿主的沙箱工具。每个部分都可以固定版本,但通过不同的机制,即便如此结果仍然依赖于宿主的Python。实践中,您通常从一个固定的容器镜像运行它,但这样您仍然依赖于一个您无法控制的完整容器运行时。 BuildStream的YAML是纯数据格式,并通过Python插件扩展。YAML没有函数,因此最终您会在项目中不断复制粘贴。最终,我们决定选择一个具有更好引导和固定机制以及更灵活语言的工具。 ### Antlir Antlir(https://facebookincubator.github.io/antlir/)是Meta的操作系统镜像构建器,建立在Buck2(https://buck2.build/)构建系统引擎之上。Buck2是Meta的开源构建系统(https://engineering.fb.com/2023/04/06/open-source/buck2-open-source-large-scale-build-system/),强调正确性、灵活性和尽可能多的缓存。Antlir使用Buck2实现了各种构建镜像的规则。 由于Antlir是一个专注于Meta内部仓库的高级工具,它自然非常主观,并且是为Meta的内部用例设计的。例如,它需要btrfs并强烈专注于单一单体仓库。 虽然我们决定不使用Antlir本身,但其底层引擎Buck2被证明非常合适,我们最终选择它作为自己构建系统的基础。 ## 我们的构建系统:tine 本质上,tine是一组经过精心设计的Buck2规则,用于构建RPM、Rust crate和Go模块组件、UKI(https://uapi-group.org/specifications/specs/unified_kernel_image/)以及镜像;它可以使用硬件密钥通过PKCS#11(https://en.wikipedia.org/wiki/PKCS_11)或本地生成的密钥对镜像进行签名。其意图是将Antlir和mkosi的最佳理念结合到一个工具中。 tine对构建主机只有三个要求:git、python3(仅用于其自身引导,不用于生产构建)和用户命名空间。从此,它根据固定的声明来引导启动构建所需的一切,以获得一个可重现且独立的构建环境。这可以是您需要的任何年代或版本的发行版。 一个重要的tine概念是“**box**”,它是一个声明并固定下来用于运行构建任务的环境。可以理解为容器或distrobox(https://distrobox.it/),但使用Buck2的语言原生声明,并利用Buck2的缓存和重建规则,因此它们构建快速自然、保持可重现,且运行无需额外依赖。tine本身定义了用于运行rpmbuild、go或cargo的box,或者一个名为`fedora.rawhide.box`的更大的多用途box,其中包含例如用于构建镜像的systemd-ukify和用于运行虚拟机的QEMU。您自己的项目也可以定义自己的box。 ### tine 的引擎:Buck2 为了更好地理解本文和示例,这里为熟悉Make(https://www.gnu.org/software/make/)或Meson(https://mesonbuild.com/)的用户提供一个一分钟的Buck2入门: - **构建文件:**BUCK文件是目录中的Makefile等价物。它用一种称为Starlark(https://starlark-lang.org/)的Python方言编写,声明了所有可构建的*目标*。 - **Cell:**命名的构建图根;这些大致遵循git仓库的边界:`//`是您自己的顶层项目(您要构建的操作系统),`tine//`是您项目拉取的tine检出。 - **Target:**构建图中一个命名的节点,即您想要构建的特定东西。它们通过格式为`cell//directory/sub:name`的绝对路径来寻址(https://buck2.build/docs/concepts/target_pattern/),引用在`cell`的`directory/sub/BUCK`文件中定义的目标名。在cell内部,您也可以使用相对路径,如`subdir:name`,或仅为当前目录中的目标使用`:name`。单个目标可以发布多个输出变体(“子目标”),例如`:my_cool_os[qcow2]`或`:my_cool_os[sbom]`。 - **Rule:**等同于meson中的`*_target()`,或Makefile规则的结构:一个将目标转换为一组*操作*及其参数的Starlark表达式。它本身不运行任何内容。例如,`bootable_disk(name = "myos", param1 = ...)`规则定义了一个`myos`目标,并调用`bootable_disk`规则将其转换为诸如“安装rpm”、“运行`systemd-repart`”等操作。 - **Action:**一个带有其声明的输入和输出的构建命令,等同于Makefile规则中的命令。这种抽象允许在仅能看到这些输入的沙箱中一致地运行所有操作。当Starlark不够用时,这些规则可以用完整的Python功能来实现。 更多信息可以在Buck2的关键概念页面(https://buck2.build/docs/concepts/key_concepts/)找到。 与Make或Meson不同,Buck2从不根据时间戳决定重建什么。一个操作的键由其所有输入的哈希值决定:源代码、工具二进制文件、构建平台配置和命令行本身。这使得其增量构建正确且可信,并且还允许从共享缓存中获取操作的结果,而不是重新运行它。 ## 演练:构建可启动镜像 让我们演练一下如何在您自己的项目中使用tine。我们将从头构建一个非常基本的示例:一个基于Fedora Rawhide的可启动镜像,其中包含一个Go项目,并启动它。在这个例子中,我们将使用duf(https://github.com/muesli/duf),一个在文本终端中以漂亮的ASCII艺术形式显示空闲/已用磁盘空间的CLI工具。 让我们按照tine的README(https://github.com/amutable-systems/tine#consuming-the-cell)来设置一个新的演示git仓库,该仓库拉取tine并对其进行初始化。 ``` git init tine-demo cd tine-demo git submodule add https://github.com/amutable-systems/tine tine tine/bin/tine init git add . git commit -m "initialize" ``` 现在让我们添加一个BUCK文件。我们将分几个部分讲解,但这些都在同一个文件中。首先,我们需要导入一些定义。这是Starlark(https://starlark-lang.org/),类似于Python的import语句。 ``` load("@tine//box:defs.bzl", "box") load("@tine//git:defs.bzl", "git") load("@tine//go:defs.bzl", "go") load("@tine//image:defs.bzl", "image") ``` 接下来,我们需要为Go编译器定义一个构建环境。tine已经提供了一个Fedora rawhide目录。那么,让我们直接使用它(因此是`tine//` cell)和Fedora的golang软件包。一个真正的项目可能会自己定义和跟踪其父操作系统目录,而不是盲目跟随tine的。 ``` box.new( name = "go.box", packages = ["golang"], release = "tine//catalog:fedora.rawhide.release", ) ``` 声明我们想要构建的duf Go项目git仓库。tine要求精确固定每个输入,因此我们指定了一个git commit ID。然后,该git仓库作为输入传递给`go.package()`规则,该规则将上述`go.box`和git检出绑定在一起,两者都作为相对目标(见上文)引用,因此使用冒号分隔符。 ``` git.fetch( name = "duf.git", repo = "https://github.com/muesli/duf", rev = "4636deb4a7b707a9f04c602db033f9837e50b3f6", ) go.package( name = "duf", box = ":go.box", # 当前目录中的一个目标 src = ":duf.git", # 另一个目标 ) ``` 有了这个,我们就可以构建并执行该二进制文件了。 ``` tine/bin/tine buck run :duf # [...] # BUILD SUCCEEDED - starting your binary # 5 local devices # [...] ``` 现在是最后也是最重要的部分:可启动镜像。就像Go box一样,我们复用tine目录中的包管理器,从Fedora Rawhide获取软件包。这是能够在虚拟机中启动的最小集,加上bash。作为额外的操作(ops),它会安装上述go规则中构建好的hello二进制文件。 ``` image.bootable_disk( name = "demo", package_manager = "tine//catalog:fedora.rawhide.package-manager", definitions = image.DEFAULT_USR_VERITY_PARTITIONS, version = "0.0.0", package_sets = ["bootable"], packages = ["bash"], ops = [ image.copy(":duf[duf]", "/usr/bin/duf"), ], ) ``` 我们可以询问Buck2某个cell中所有可用的构建目标。 ``` tine/bin/tine buck targets //... # root//:demo # root//:demo.initrd # root//:duf # root//:duf.git # root//:example.git # root//:go.box # root//:go.box.exec ``` 完成所有这些之后,我们现在可以构建镜像了。然而,更有趣的是在QEMU中实际看到它运行。让我们添加一个虚拟机定义,为方便起见设置自动登录,使用tine自己的Rawhide目录中的QEMU及相关工具启动我们闪亮的新demo镜像。 ``` image.vm( name = "demo-vm", autologin = "root", # 复用安装了所有QEMU等工具的tine rawhide box box = "tine//catalog:fedora.rawhide.box", image = ":demo", credentials = { "firstboot.timezone": "UTC", } ) ``` 从干净的源码树执行这个单一命令,将构建Go项目、镜像,并启动它。 ``` tine/bin/tine buck run :demo-vm ``` 您应该看到以下内容。 ``` [ OK ] Reached target graphical.target - Graphical Interface. Fedora Linux 46 (Rawhide Prerelease) Kernel 7.3.0-0.rc3.260916g9b87fdc9af2f.34.fc46.x86_64 on an x86_64 (hvc0) fedora login: root (automatic login) -bash-5.3# duf --help Usage of duf: --all include pseudo, duplicate, inaccessible file systems [...] -bash-5.3# ... -bash-5.3# systemctl poweroff ``` 查看tine的examples/目录(https://github.com/amutable-systems/tine/tree/main/examples/),了解各种场景的BUCK和辅助文件,例如使用生成或硬件密钥签名的SecureBoot/verity操作系统、如何构建Go/Rust项目、tine提供的各种规则和自定义方式,以及如何编写集成测试。 ## 免费的 SBOM tine 为您提供了更多免费功能。例如,它集成了Syft SBOM生成器(https://github.com/anchore/syft),因此您可以要求它构建CycloneDX标准SBOM(https://cyclonedx.org/)。

相似文章

自举构建:方法与原因

Hacker News Top

这篇文章涵盖了一场在FOSSY会议上的演讲,解释了自举构建,该构建方法能够从最小可信源构建整个系统,以增强软件安全性和可重现性。

后现代构建系统

Lobsters Hottest

一篇博客文章,探讨理想中的'后现代'构建系统的设计,该系统优先考虑可信的增量构建、最大化计算复用和分布式构建,并以Nix作为参考。